Skip to content

Uninitialized device-memory read passed into CUB radix sort #7

Closed

Description

@TarekIbnZiad

Uninitialized device-memory read passed into CUB radix sort

Environment

  • cuJSON commit: 2ac7d3dcd7ad1ff64ebdb14022bf94c59b3b4953 (branch master)
  • OS: Ubuntu 22.04.5 LTS
  • GPU: NVIDIA A40 (Ampere, sm_86)
  • NVIDIA driver: 590.48.01
  • CUDA toolkit: 13.1 (nvcc V13.1.115)

Component

cuJSON — sort integration (parse_standard_json.cu:1584-1611, parse_json_lines.cu:1070-1093). Reproduces across cuJSONIterator, cuJSONLinesIterator, parse_json_lines, and parse_standard_json.

Severity

Medium (CWE-908, Use of Uninitialized Resource).

Description

cuJSON passes partially initialized key/value buffers into the thrust/CUB sorting stage. cub::DeviceRadixSortSingleTileKernel then loads uninitialized tail bytes while sorting structural positions.

Root cause

The buffers handed to the radix sort are sized for a padded element count, but only the populated structural/pair elements are initialized. CUB sorts the full allocated range, so it loads the uninitialized tail. This reproduces across all four entry points because they share the same sort integration.

Call path:

harness LLVMFuzzerTestOneInput
  -> parse_standard_json / parse_json_lines / iterator entry points
  -> CUB DeviceRadixSortSingleTileKernel   (uninitialized read of sort buffer tail)

Impact

Systemic parse-tree corruption across standard JSON, JSON Lines, and iterator entry points. Malformed inputs can produce nondeterministic parse results and sanitizer-visible device reads.

Reproduction

CUDA_HOME=${CUDA_HOME:-/usr/local/cuda}
nvcc -std=c++17 -arch=native -O2 -g -lineinfo \
  -I<cujson-src> -I${CUDA_HOME}/include \
  llm_harness_cuJSONIterator.cu harness_afl_main.cpp \
  --compiler-bindir g++-13 -L${CUDA_HOME}/lib64 -lcudart \
  -o /tmp/cujson_cub_sort_initcheck.bin

compute-sanitizer --tool=initcheck --report-api-errors=no --error-exitcode 99 \
  /tmp/cujson_cub_sort_initcheck.bin trigger_cuJSONIterator.bin

Sanitizer evidence

========= Uninitialized __global__ memory read of size 4 bytes
========= Uninitialized __global__ memory read of size 1 bytes  (x6)
=========   Uninitialized access to 0x7f3cf6400480 on access by cudaMemcpy source
=========   Uninitialized access to 0x1938000420 on access by cudaMemcpy source
========= ERROR SUMMARY: 9 errors

Suggested fix

Track the exact initialized element count passed to the sorting kernels, zero-fill padded storage, and avoid sorting beyond the populated structural/pair arrays.

Attachments

The following files from this finding are attached:

  • llm_harness_cuJSONIterator.cu
  • harness_afl_main.cpp
  • trigger_cuJSONIterator.bin
  • initcheck.log

Archive.zip

Activity

  1. self-assigned this
    on Jun 24, 2026
  2. added a commit that references this issue on Sep 10, 2026
  3. ashkanvg commented on Sep 10, 2026

    @ashkanvg
    Collaborator

    Closing as a false positive — this is not a cuJSON bug. Fixed/explained in #17.

    Root cause. The reported uninitialized reads all originate inside thrust::stable_sort_by_key over a zip_iterator whose value type is tuple<uint32_t, uint8_t>. That type is 8 bytes wide for 5 bytes of payload, so it carries 3 bytes of struct padding. Thrust materialises a temporary tuple array by assigning only the two members, then CUB's BlockLoad reads whole 8-byte tuples — including padding it never wrote.

    Proof. A standalone program containing no cuJSON code at all, in which every input byte is explicitly cudaMemset:

    cudaMemset(keys, 1, n);
    cudaMemset(oc,   '[', n);
    cudaMemset(idx,  0, n*sizeof(uint32_t));
    auto z = thrust::make_zip_iterator(thrust::make_tuple(idx, oc));
    thrust::stable_sort_by_key(thrust::cuda::par, keys, keys+n, z);
    sizeof(tuple<uint32_t,uint8_t>) = 8  (4+1 = 5 payload bytes)
    ========= ERROR SUMMARY: 325 errors
    

    The padding is never compared by the radix sort and never scattered back to the output arrays, so it cannot influence results.

    Real defects found while investigating this, fixed in #17:

    • mergeChunks allocated sizeof(uint32_t)*N*ROW2 + 3 = 8N+3 bytes but writes through element 2N+1, needing 8N+8 — a genuine 5-byte heap overflow on every non-empty JSON Lines merge, confirmed with a guard canary. ASan could not see it because the buffer is cudaMallocHost.
    • The pair_pos row was copied to the host partly uninitialized and published through cuJSONResult. Now zeroed.

    After #17, cuJSON-attributable initcheck findings are zero on both paths (standard 22 → 20, JSON Lines 60 → 53); every remaining report routes through sort.inl:125.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions