Skip to content

Decoder hardening: overflow, arena, and codebook fixes#16

Merged
kahrendt merged 6 commits into
mainfrom
decoder-hardening
Jul 7, 2026
Merged

Decoder hardening: overflow, arena, and codebook fixes#16
kahrendt merged 6 commits into
mainfrom
decoder-hardening

Conversation

@kahrendt

@kahrendt kahrendt commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Five correctness/memory-safety fixes in the forked Tremor decoder, each hardening a path reachable only through crafted or corrupt input. All touch only src/tremor/ and are documented in CHANGES.md.

  • Fix 32-bit overflow in DSP setup arena sizing: a crafted setup header (up to 64 modes × 16 submaps at one large-decodemap residue) could sum past 2³¹ and wrap the 32-bit long on ESP32, so _ogg_malloc succeeded undersized and res0_look wrote past the buffer. Sums in ogg_int64_t and rejects totals over DSP_ARENA_MAX_BYTES before allocating.
  • Fix block-arena undercount for res2 across multiple submaps: res2_inverse allocates its partword array per submap, all live within one packet, but the sizing reserved only one array. When info->end is the binding cap, the arena undercounts and _vorbis_block_alloc returns NULL (dereferenced unchecked). Scales the res2 term by min(channels, 16).
  • Reject truncated id header before shift in _vorbis_unpack_info: oggpack_read returns its −1 EOP sentinel on a truncated packet, so 1 << oggpack_read(...) evaluated 1 << -1 (UB) before the EOP check could fire. Reads the blocksize nibbles into locals and rejects a negative value before shifting.
  • Force EOP on codebook tree-walk fall-off: a tree-walk fall-off returned the 0xffffffff sentinel while trusting a byte-granular oggpack_adv to trip end-of-packet; slack in the final partial byte could leave EOP unset, letting the sentinel reach decode_map_apply (garbage output / OOB q_val read). Sets the overrun sentinel directly so every caller's EOP check rejects the vector.
  • Fix zero-entry codebook rejection on ESP-IDF: entries == 0 is a legal field, but the lengthlist/q_val allocations sized from it request 0 bytes; heap_caps_malloc(0) returns NULL on ESP-IDF (non-NULL on glibc), so the target rejected books the host decoded. Clamps the three sizes to at least one byte.

kahrendt added 5 commits July 3, 2026 11:23
A crafted setup header can point up to 64 modes * 16 submaps at a single
residue whose decodemap is hundreds of MB. Each per-mode term fits a long,
but their sum can exceed 2^31 and wrap a 32-bit long on the ESP32 target to
a small value. _ogg_malloc then succeeds undersized and res0_look's
unchecked arena allocations write past the buffer.

Sum the size in ogg_int64_t and reject any header whose total exceeds
DSP_ARENA_MAX_BYTES (2^31-1, the largest arena a long offset can address)
through the existing -1 init-failure path, before the allocation.
mapping0_inverse calls the residue inverse once per submap and
res2_inverse allocates its partword array each call, all live within
one packet (the block arena resets only between packets). The sizing
reserved just one max_partwords_2 array. When info->end is the binding
cap, each res2 submap allocates the full array rather than a
channel-scaled share, so the reservation undercounts by up to the
submap count and _vorbis_block_alloc returns NULL, which res012.c
dereferences unchecked.

Scale the res2 term by min(channels, 16), the most res2 submaps that
can be live at once (each needs >=1 channel; submaps is a 4-bit field).
oggpack_read returns its -1 EOP sentinel on a truncated identification
packet, so 1<<oggpack_read(opb,4) evaluated 1<<-1 (a negative shift
count, UB) before the existing EOP check could fire. Read both blocksize
nibbles into locals and reject a negative value before shifting, matching
the rangebits guard in floor1.c.

Only reachable through the exported vorbis_synthesis_headerin API; the
C++ wrapper's 30-byte id-header minimum keeps in-tree paths clear of it.
decode_packed_entry_number chases the decode tree through dec_maxlength
bits and, on a fall-off (no leaf reached), returned the 0xffffffff
sentinel while trusting oggpack_adv(b, read+1) to trip end-of-packet.
That advance's overflow check is byte-granular, so slack in the final
partial byte could leave EOP unset, letting the sentinel reach
decode_map_apply past its oggpack_eop guard: garbage output for dec_type
1 and an out-of-bounds q_val read for dec_type 2 (quantvals not a power
of two). Only a one-used-entry book reaches this path, and only on
corrupt input.

Set the overrun sentinel directly (oggpack_seteop) so every caller's EOP
check rejects the vector, covering all dec_types and the scalar
vorbis_book_decode path in one place.
entries==0 is a legal 24-bit codebook field, and _make_decode_table
already handles 0-sized books. But the two lengthlist allocations and
the maptype-2 dec_type-3 q_val allocation size themselves from
entries/used_entries, so a zero-entry book requests 0 bytes.
heap_caps_malloc(0) returns NULL on ESP-IDF (glibc returns non-NULL),
so the if(!ptr) checks rejected such a book on-target while the host
decoded it. Clamp the three sizes to at least one byte; the read/pack
loops that follow are empty when the count is zero.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the forked Tremor Vorbis decoder against crafted/truncated input by addressing undefined behavior, malformed codebook decoding, and arena-sizing corner cases, with corresponding documentation updates in src/tremor/CHANGES.md.

Changes:

  • Prevents UB in _vorbis_unpack_info by rejecting truncated headers before shifting blocksize nibbles.
  • Hardens codebook unpack/decoding by (a) forcing EOP on decode-tree fall-through and (b) allowing legal zero-entry codebooks on ESP-IDF by avoiding 0-byte allocations.
  • Fixes arena sizing: accounts for multiple res2 submaps in the block arena and computes DSP setup arena totals in 64-bit with an enforced cap.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/tremor/info.c Rejects truncated id headers before shifting to avoid negative-shift UB.
src/tremor/codebook.c Adds explicit EOP forcing on decode-tree fall-through and clamps allocations for zero-entry codebooks on ESP-IDF.
src/tremor/block.c Adjusts arena sizing (res2 multi-submap) and moves DSP setup arena sizing to 64-bit with a maximum cap.
src/tremor/CHANGES.md Documents the hardening fixes and related behavioral details.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/tremor/block.c
Comment thread src/tremor/block.c
Comment thread src/tremor/codebook.c Outdated
Comment thread src/tremor/CHANGES.md Outdated
@kahrendt kahrendt enabled auto-merge (squash) July 7, 2026 16:57
@kahrendt kahrendt merged commit 6a143ed into main Jul 7, 2026
10 checks passed
@kahrendt kahrendt deleted the decoder-hardening branch July 7, 2026 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants