Skip to content

test(fuzz): NPY parser harness (+ 2 UB fixes it found) - #85

Merged
balwierz merged 1 commit into
mainfrom
test/fuzz-npy
Jul 25, 2026
Merged

test(fuzz): NPY parser harness (+ 2 UB fixes it found)#85
balwierz merged 1 commit into
mainfrom
test/fuzz-npy

Conversation

@balwierz

Copy link
Copy Markdown
Owner

Summary

Extends the libFuzzer scaffolding (#84) to the NumPy .npy parser — a hand-rolled binary format with a history of audit bugs (negative/overflowing shapes, shape-vs-body). The harness fuzzes the whole parse-and-build path (parse_npy_headerslab_to_arrow / build_1d/build_2d_table) under ASan+UBSan; since parse_npy_header validates the declared shape against the buffer, an accepted header can't drive the builders out of bounds, so this is a true-positive-only fuzz of the real path.

A VV_FUZZ-guarded npz::npy_fuzz_one mirrors NpzSource's shape dispatch; the 24.04 CI fuzz step now builds + runs both fuzz_colblock and fuzz_npy.

Bugs it found (both fixed, both crafted-.npz-reachable)

  1. Zero declared item size → OOB. A dtype like |b0 gave item_size = 0, which skipped the shape-fits check (item_size > 0), so slab_to_arrow read past the buffer. Fixed: pin item_size to the Arrow type's real element size (npy_element_bytes — numpy bool is 1 byte, not Arrow's 1 bit) before the check, so it can't be bypassed.
  2. Unaligned typed load. make_column cast the .npy data offset (arbitrary alignment) to const CType* and read p[i] — a misaligned load (UB; faults on aarch64). Fixed with memcpy (same class as ci: ASan + UBSan sanitizer gate (+ bigWig misaligned-read fix) #80's bigWig fix).

Verification

  • Found in ~700k and ~1.5M iterations; after the fixes the parser is clean over 1.4 M iterations under ASan+UBSan.
  • Legitimate .npz reads are unchanged (item_size already equals the type size for valid dtypes); all npz_* tests pass; suite 375.

🤖 Generated with Claude Code

Extend the libFuzzer scaffolding (from #84) to the NumPy .npy parser: fuzz the
whole parse-and-build path (parse_npy_header -> slab_to_arrow / build_1d/2d_table)
under ASan+UBSan. parse_npy_header validates the declared shape against the buffer
size, so an accepted header can't drive the builders out of bounds — the harness
exercises the real path. A VV_FUZZ-guarded npz::npy_fuzz_one mirrors NpzSource's
shape dispatch; the 24.04 CI fuzz step now runs both fuzz_colblock and fuzz_npy.

It found two undefined-behaviour bugs reachable via a crafted .npz, both fixed:
- A dtype with a zero declared item size (e.g. "|b0") skipped the shape-fits
  check (`item_size > 0`) and read past the buffer. Pin item_size to the Arrow
  type's real element size (npy_element_bytes) — numpy bool is 1 byte, not
  Arrow's 1 bit — before the bounds check, so it can never be bypassed.
- make_column loaded values via a typed pointer cast on the .npy data offset,
  which is not aligned to the element type — a misaligned load (UB; faults on
  aarch64). Read each element with memcpy.

Legitimate .npz reads are unchanged (item_size == the type size for valid dtypes);
the parser is clean over 1.4 M fuzz iterations. Suite 375.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@balwierz
balwierz merged commit a2b9716 into main Jul 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant