Skip to content

Security Issue in libqrencode: Heap Buffer Overflow in QRinput_List_newEntry #232

Description

@pranavmb

Dear Maintainers,

We are a graduate student team working on applied software security analysis, and during our project on fuzz testing open-source libraries, we identified a vulnerability in libqrencode that we’d like to responsibly disclose.

Issue Summary:
While fuzzing libqrencode using a custom harness and AFL++, we encountered multiple heap-buffer-overflow crashes, all consistently pointing to the function:

memcpy(entry->data, data, (size_t)size); // in QRinput_List_newEntry

This is due to improper memory allocation for entry->data, especially when malformed or edge-case inputs are passed via the encoding API (QRcode_encodeString, QRcode_encodeData).

Fuzzing Details:
Fuzzer: AFL++

Harness: Custom C++ harness calling QRcode_encodeString and QRcode_encodeData based on mutated input.

Below is the harness code used:

#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <qrencode.h>
#include <limits.h>

// Maximum size limit for payload to prevent slowdowns or heap overflows
#define MAX_PAYLOAD_SIZE 1024

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    if (!data || size < 6 || size > MAX_PAYLOAD_SIZE + 6) return 0;

    // Extract last 6 bytes as fuzzing parameters
    int version        = data[size - 6] % 41;                     // 0–40
    QRecLevel eclevel  = (QRecLevel)(data[size - 5] % 4);         // L, M, Q, H
    QRencodeMode mode  = (QRencodeMode)(data[size - 4] % 4);      // MODE_8, MODE_KANJI, etc.
    int case_sensitive = data[size - 3] % 2;
    int use_string     = data[size - 2] % 2;
    int pad_null       = data[size - 1] % 2;

    // Actual data for input buffer
    size_t payload_size = size - 6;
    if (payload_size == 0) return 0;

    char *buffer = (char *)malloc(payload_size + 1);
    if (!buffer) return 0;

    memcpy(buffer, data, payload_size);
    buffer[payload_size] = pad_null ? '\0' : 'X';  // Ensure null-termination if needed

    QRcode *qrcode = nullptr;

    if (use_string) {
        // Use string-based encoding
        buffer[payload_size] = '\0';  // Just to be safe
        qrcode = QRcode_encodeString(buffer, version, eclevel, mode, case_sensitive);
    } else {
        // Use binary data encoding
        qrcode = QRcode_encodeData(payload_size, (unsigned char *)buffer, version, eclevel);
    }

    if (qrcode) {
        QRcode_free(qrcode);  // Free result to prevent memory leaks
    }

    free(buffer);  // Clean up input buffer
    return 0;
}

Below is stack trace observed:

./fuzz_qrencode out/default/crashes/id:000019,sig:06,src:000196,time:41604,execs:42884,op:havoc,rep:2
Running LLVMFuzzerInitialize ...
continue...
Reading 53 bytes from out/default/crashes/id:000019,sig:06,src:000196,time:41604,execs:42884,op:havoc,rep:2
=================================================================
==39702==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x604000000400 at pc 0x000103101984 bp 0x00016d42ea70 sp 0x00016d42e220
READ of size 3 at 0x604000000400 thread T0
    #0 0x000103101980 in memcpy+0x480 (libclang_rt.asan_osx_dynamic.dylib:arm64+0x51980)
    #1 0x0001029d9f30 in QRinput_List_newEntry+0xf8 (fuzz_qrencode:arm64+0x100009f30)
    #2 0x0001029d9d3c in QRinput_append+0x48 (fuzz_qrencode:arm64+0x100009d3c)
    #3 0x0001029e2fac in Split_eat8+0x3a8 (fuzz_qrencode:arm64+0x100012fac)
    #4 0x0001029e220c in Split_splitString+0x1e0 (fuzz_qrencode:arm64+0x10001220c)
    #5 0x0001029d7904 in QRcode_encodeStringReal+0x108 (fuzz_qrencode:arm64+0x100007904)
    #6 0x0001029d6078 in LLVMFuzzerTestOneInput+0x238 (fuzz_qrencode:arm64+0x100006078)
    #7 0x0001029d5dd0 in ExecuteFilesOnyByOne+0xfc (fuzz_qrencode:arm64+0x100005dd0)
    #8 0x0001029d5c78 in LLVMFuzzerRunDriver+0x510 (fuzz_qrencode:arm64+0x100005c78)
    #9 0x0001029d574c in main+0xc4 (fuzz_qrencode:arm64+0x10000574c)
    #10 0x00018137c270  (<unknown module>)
    #11 0xf313fffffffffffc  (<unknown module>)

0x604000000400 is located 0 bytes after 48-byte region [0x6040000003d0,0x604000000400)
allocated by thread T0 here:
    #0 0x0001031034f0 in malloc+0x70 (libclang_rt.asan_osx_dynamic.dylib:arm64+0x534f0)
    #1 0x0001029d5f80 in LLVMFuzzerTestOneInput+0x140 (fuzz_qrencode:arm64+0x100005f80)
    #2 0x0001029d5dd0 in ExecuteFilesOnyByOne+0xfc (fuzz_qrencode:arm64+0x100005dd0)
    #3 0x0001029d5c78 in LLVMFuzzerRunDriver+0x510 (fuzz_qrencode:arm64+0x100005c78)
    #4 0x0001029d574c in main+0xc4 (fuzz_qrencode:arm64+0x10000574c)
    #5 0x00018137c270  (<unknown module>)
    #6 0xf313fffffffffffc  (<unknown module>)

SUMMARY: AddressSanitizer: heap-buffer-overflow (fuzz_qrencode:arm64+0x100009f30) in QRinput_List_newEntry+0xf8
Shadow bytes around the buggy address:
  0x604000000180: fa fa 00 00 00 00 00 05 fa fa 00 00 00 00 00 00
  0x604000000200: fa fa 00 00 00 00 00 05 fa fa 00 00 00 00 00 00
  0x604000000280: fa fa 00 00 00 00 00 07 fa fa 00 00 00 00 00 00
  0x604000000300: fa fa 00 00 00 00 00 00 fa fa 00 00 00 00 00 00
  0x604000000380: fa fa 00 00 00 00 00 05 fa fa 00 00 00 00 00 00
=>0x604000000400:[fa]fa 00 00 00 00 00 fa fa fa fa fa fa fa fa fa
  0x604000000480: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x604000000500: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x604000000580: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x604000000600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x604000000680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==39702==ABORTING
zsh: abort      ./fuzz_qrencode  

Crashes reproducible with multiple mutated inputs of size ~21–53 bytes.

AddressSanitizer reports consistent out-of-bounds reads at QRinput_List_newEntry.

Sample Crash Input:
We can share sanitized crash inputs (e.g., [id:000018,sig:06,...]) and the full harness code upon request.

Recommendation:
We believe adding bounds checks or validating data size before memcpy, and ensuring proper allocation of entry->data, could mitigate this issue.

Please let us know if there’s a preferred channel for submitting PoCs or further technical details.

Thank you for your time and for maintaining this valuable open-source project.

Sincerely,
Pranav Madhav Bagepalli
University of Utah
u1471494@umail.utah.edu

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions