Skip to content

fix: yield_on_full correctness on wuffs branch (#68)#72

Merged
197g merged 1 commit intoimage-rs:wuffsfrom
lilith:wuffs-yield-on-full-fix
Apr 14, 2026
Merged

fix: yield_on_full correctness on wuffs branch (#68)#72
197g merged 1 commit intoimage-rs:wuffsfrom
lilith:wuffs-yield-on-full-fix

Conversation

@lilith
Copy link
Copy Markdown
Contributor

@lilith lilith commented Apr 13, 2026

Summary

Three fixes for yield_on_full correctness on the wuffs branch, plus regression tests.

Fixes

  1. Unconditional buffer drain (yield_on_full + small output buffer: false NoProgress loses data #68): The buffer drain at the start of advance() was gated on code_link.is_some(). After a clear code sets code_link = None, buffered data was silently lost. Same root cause as on master, same fix (PR test: failing regression tests for yield_on_full data loss (#68) #71).

  2. debug_assert in non-broken-burst path: debug_assert!(self.next_code <= size_switch_at) fires in TIFF mode when the table is full (next_code = 4096 exceeds size_switch_at = 4094). Guard: self.table.is_full() || ....

  3. reconstruct_simple overwrite guard: The relaxed 8-byte-chunk overwrite can clobber bytes past the code length. With yield_on_full or tight buffers, those bytes may be returned to the caller before subsequent codes overwrite them. Guard: require >= 8 bytes of slack past the code length.

Regression tests

Three tests in tests/yield_on_full_regression.rs:

  • regression_yield_on_full_fuzz_crash: minimized fuzz input (size=7, MSB)
  • regression_yield_on_full_small_buffers: buffer sizes 1-64
  • yield_on_full_sweep_sizes: all code sizes 2-12

Status

Fixes 1 and 2 pass. Fix 3 resolves the overwrite concern but small_buffers with buf=1 still fails — the inline burst reconstruction has a separate data-loss path when yield_on_full causes mid-burst returns. The burst loop writes directly into out during validation, and when the burst breaks mid-way, some of that written data may not be properly accounted for. This needs further investigation specific to the wuffs burst loop restructuring.

Test plan

  • regression_yield_on_full_fuzz_crash passes
  • yield_on_full_sweep_sizes passes
  • regression_yield_on_full_small_buffers (buf=1) — still fails, needs separate fix
  • All pre-existing tests pass (14 unit + 4 roundtrip + 2 roundtrip_vec + 19 streaming_parity)

Three fixes for the wuffs burst decoder:

1. Unconditional buffer drain (image-rs#68): the buffer drain at the start
   of advance() was gated on code_link.is_some(). After a clear code
   sets code_link=None, buffered data was lost. Also caused false
   NoProgress with buf=1 when the buffer had undrained data.

2. debug_assert fix: the non-broken-burst path asserted
   next_code <= size_switch_at, which is wrong when the table is
   full in TIFF mode (next_code=4096 > size_switch_at=4094).

3. reconstruct_simple overwrite guard: require 8 bytes of slack
   past the code length before using the relaxed 8-byte-chunk
   overwrite, preventing data corruption with tight buffers.

All three regression tests pass, including buf=1. No perf impact —
the fix is in the per-call buffer drain, not in the burst hot loop.
@lilith lilith force-pushed the wuffs-yield-on-full-fix branch from 54e9f3c to 5460037 Compare April 13, 2026 03:50
lilith added a commit to lilith/image-gif that referenced this pull request Apr 13, 2026
Three valid interlaced GIFs (7x7, 9x9, 47x63) produced by gifsicle 1.95
are rejected with "image truncated". All decode correctly with giftopnm
(netpbm) and are valid per the GIF89a spec.

Root cause: when decoding interlaced frames, read_into_buffer passes
per-row slices to the LZW decoder. For small images (7-47 byte rows),
weezl's NoProgress status triggers two bugs in DecodeSubBlock:
1. Input data is skipped instead of retried with a smaller buffer
2. NoProgress during flush is treated as end-of-stream

Related: image-rs/weezl#72
lilith added a commit to lilith/image-gif that referenced this pull request Apr 13, 2026
Three valid interlaced GIFs (7x7, 9x9, 47x63) produced by gifsicle 1.95
are rejected with "image truncated". All decode correctly with giftopnm
(netpbm) and are valid per the GIF89a spec.

Root cause: when decoding interlaced frames, read_into_buffer passes
per-row slices to the LZW decoder. For small images (7-47 byte rows),
weezl's NoProgress status triggers two bugs in DecodeSubBlock:
1. Input data is skipped instead of retried with a smaller buffer
2. NoProgress during flush is treated as end-of-stream

Related: image-rs/weezl#72
lilith added a commit to lilith/image-gif that referenced this pull request Apr 13, 2026
Three valid interlaced GIFs (7x7, 9x9, 47x63) produced by gifsicle 1.95
are rejected with "image truncated". All decode correctly with giftopnm
(netpbm) and are valid per the GIF89a spec.

Root cause: when decoding interlaced frames, read_into_buffer passes
per-row slices to the LZW decoder. For small images (7-47 byte rows),
weezl's NoProgress status triggers two bugs in DecodeSubBlock:
1. Input data is skipped instead of retried with a smaller buffer
2. NoProgress during flush is treated as end-of-stream

Related: image-rs/weezl#72
lilith added a commit to lilith/image-gif that referenced this pull request Apr 13, 2026
When decoding interlaced frames, read_into_buffer passes per-row slices
to the LZW decoder. For small images (7-47 byte rows), weezl returns
NoProgress because the output buffer is too small for its internal state
machine to advance.

Two bugs fixed in DecodeSubBlock:

1. Input data skipped (left > 0): When NoProgress with consumed=0 and
   bytes_len=0, the decoder executed `consumed = n`, skipping all
   remaining sub-block input without processing it.

2. Premature frame end (left = 0): During flush, NoProgress with 0
   output was treated as end-of-stream, but weezl still had buffered
   data.

Fix: retry LZW decode with a 1-byte temp buffer (lzw_drain_one) to
unstick weezl, then append the byte to the caller's output.

Related: image-rs/weezl#72
}

debug_assert!(self.next_code <= size_switch_at);
debug_assert!(self.table.is_full() || self.next_code <= size_switch_at);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, right. 👍

Copy link
Copy Markdown
Member

@197g 197g left a comment

Choose a reason for hiding this comment

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

I had planned for this to go into 0.2 but this probably deserves a backport. Also now I'm wondering if the logic of the initial block can be cleaned up with this. Alas, good fix.

@197g 197g merged commit 24f875a into image-rs:wuffs Apr 14, 2026
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.

2 participants