Fuzz findings¶
Every crash, panic, hang, overflow or unbounded allocation found by the robustness work (2026-10-27, branch claude/fuzz-hardening-2026-10-27), with the fix and its regression test. How to run the fuzzers: fuzzing guide. "Found by": fuzz = cargo-fuzz or a C fuzz driver, mutation = the seeded-mutation test with its allocation guard, audit = read while writing the targets, then reproduced with a hostile input.
Rust¶
| Target | Input class | Found by | Fix | Regression test |
|---|---|---|---|---|
Nso::parse (napple-core/.../nso.rs) |
Compressed segment whose header claims 512 MiB of output for a few bytes: lz4_flex reserved dsz up front (x3 segments) |
mutation | Refuse dsz > csz * 255 + 64 for LZ4 segments (LZ4 cannot expand more) |
mutation.rs::nso_huge_decompressed_size_is_not_preallocated |
NczSource block mode (ncz.rs) |
Block header with exponent 32 and section sizes of 4 GiB: zstd::bulk::decompress(raw, dlen) reserved 4 GiB for a tiny block |
mutation | Stream-decode with Decoder::take(dlen + 1) and a capacity bounded by the raw size |
mutation.rs::ncz_block_with_huge_declared_size_is_not_preallocated |
ZipArchive::open_entry (napple-build/.../zipread.rs, firmware zip) |
Deflate entry declaring 1 GiB uncompressed size: Vec::with_capacity(usize) |
mutation | Capacity min(usize, csize * 1032) (deflate's maximum ratio) |
napple-build/tests/mutation.rs::zip_deflate_with_huge_declared_size_is_not_preallocated |
BKTR IndirectSource::read_at/map, PatchedRomfs::delta_entries (bktr.rs) |
Relocation physical_off near u64::MAX: physical_off + (pos - virtual_off) overflows (panic in debug, wrong read in release) |
audit | checked_add, error / false on overflow |
bktr.rs::tests::hostile_physical_offsets_do_not_overflow |
relocs::decode_relr / collect, ModuleImage::function_starts (napple-rewrite) |
RELR address 0xFFFF_FFFF_FFFF_FFF8, bitmap cursor and DT_HASH/RELA/JMPREL/SYMTAB values near the top of the address space; eh_frame_hdr encoded value near i64::MAX: unchecked + |
audit, reproduced | checked_add/saturating_add everywhere an untrusted value is added; rd32/rd64 saturate their range end |
napple-rewrite/tests/mutation.rs::hostile_dynamic_values_do_not_overflow |
Nca::open, NCA0 (nca.rs::nca0_xts) |
NCA0 section starting inside the header (start < 0x400): abs - NCA0_SECTOR_BASE underflows |
fuzz (nca) |
Refuse an NCA0 section that starts below 0x400 |
napple-core/tests/mutation.rs::regress_nca0_section_below_header (tests/regress/nca0_section_below_header.bin) |
KeySet::parse (domain/keys.rs, derivation of missing keys) |
master_key_00 (or another source key) of 32 bytes: GenericArray::from_slice(key) asserts on a non-16-byte AES key |
fuzz (keyfile) |
dec derives nothing for a key that is not 16 bytes (the existing 16/32 length filter then drops it) |
keys.rs::tests::odd_length_master_keys_derive_nothing_instead_of_panicking |
No finding (clean in the mutation tests and the fuzz budget): PFS0/HFS0/XCI, NCA body and IVFC/romfs tables, NCZ stream mode, CNMT, NACP, tickets, NPDM, Mod IPS/pchtxt/cheats/manifest/config/.napmod, NRO/NTB1/BFTTF, Container::load, ingest. Accepted by design: ips::apply on a 5-byte IPS32 record may grow the target up to the documented 1 GiB cap.
C runtime¶
| # | Target | Input class | Fix (file:function) | Regression test |
|---|---|---|---|---|
| 1 | jpeg | 4-component frame header: stack write out of bounds | services/jpeg_core.c:napjpeg_info |
regress_jpeg.c |
| 2 | jpeg | over-subscribed Huffman table: heap write out of bounds | jpeg_core.c:huff_build |
regress_jpeg.c |
| 3 | movie_audio | Vorbis comment header with a 0xfffffff0 vendor length: 4 GiB allocation and loop |
services/movie_audio_core.c:add_header_ |
regress_movie_audio.c |
| 4 | movie_audio | codebook where pow() gives inf, cast to int |
third_party/stb_vorbis.c:lookup1_values |
regress_movie_audio.c |
| 5 | movie_audio | stream that decodes to NaN, cast to int16 | movie_audio_core.c:vorbispcm_packet |
regress_movie_audio.c |
| 6 | movie_audio | codebook entries x dimensions asks for hundreds of MB (capped at 4M values per codebook) | stb_vorbis.c:start_decoder |
regress_movie_audio.c |
| 7 | amiibo | stray } in an AmiiboAPI array: infinite loop |
services/amiibo_core.c:ami_jskip |
regress_amiibo.c |
| 8 | amiibo | key_retail.bin with magic size above 16: stack overflow |
amiibo_core.c:amiibo_keys_load, amiibo_crypto_core.c:prepare_seed |
regress_amiibo.c |
| 9 | json | literals and numbers read past the end of text that is not NUL-terminated | src/json.c:parse_value |
regress_json.c |
| 10 | json | every string kept a buffer sized to the rest of the text (80 KB of short strings held 80 MB) | json.c:parse_string |
regress_json.c |
| 11 | romfs_map | more than 64 files: NULL descriptor table walked on close | services/romfs_map.c:romfs_map_close |
regress_romfs.c |
| 12 | romfs_map | extent count the file cannot hold: multi-GB allocation | romfs_map.c:romfs_map_open |
regress_romfs.c |
| 13 | codec_headers | Exp-Golomb and QP signed overflow | services/codec_headers_core.c:br_uei,br_sei |
regress_codec_headers.c |
| 14 | codec_headers | PPS builder overflow on guest QP values | codec_headers_core.c:bw_se,h264_build_pps |
regress_codec_headers.c |
| 15 | ipc | descriptor, handle and data counts read past the 0x100-byte message buffer | src/ipc.c:parse |
regress_ipc.c |
| 16 | ipc | no room for the domain/CMIF header; object-id offset past the message | ipc.c:dispatch |
regress_ipc.c |
| 17 | ipc | misaligned IPC_IN load on TIPC arguments, wrapping off + n |
ipc.h:IPC_IN, ipc.c:ipc_in |
regress_ipc.c |
| 18 | ipc (bsd) | Select timeout overflow on guest int64 values | services/offline.c |
regress_ipc.c |
| 19 | ipc (time) | guest time snapshot arithmetic overflow | services/time_core.c:time_snapshot_span, services/time.c |
regress_ipc.c (span only; st_user_clock_difference, st_monotonic_base have no embedded test) |
Not present in the C runtime, so nothing to fuzz: WebArgs TLV and IPS patching (IPS lives in Rust: napple-mods). libapplet (library-applet storages) is the closest TLV-like parser. The ipc fixes came from the first fuzz rounds and are a start, not a complete sweep of the services (the target reached about 2000 edges); hid_core and audio decoders are not covered, fs.c command handlers only through ipc.
Budgets run (mutation driver, ASan + UBSan, 300 s per target after the last fix to that target)¶
| Target | Executions | Result |
|---|---|---|
| codec_headers | 6.3M | clean |
| amiibo | 5.9M | clean |
| jpeg | 526K | clean |
| json | 14.3M | clean |
| romfs_map | 5.5M | clean |
| movie_audio | 144K | clean |
| libapplet | 38M | clean |
| romfs_overlay | 5.8M | clean |
| nro | 6.2M | clean |
| ipc | 389K | clean (plus 4 seeds x 60 s) |
The Rust budget (24 cargo-fuzz targets, 301 s each, about 183M executions) ended with zero crashes after the fixes above. The libFuzzer entries of the C targets compile in the Makefile but were not run (all C runs used the mutation driver).