Skip to content

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

@pranavmb

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

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