VP8L encoder emits a valid-but-wrong stream (84% pixels corrupt) — shipping in 0.4.4 at default settings
@lilith is already working on this.
Since Jul 15, 2026.
- Dominant language
- Rust
- Stars
- 15
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
Published zenwebp 0.4.4 can silently emit a corrupt VP8L stream — at default settings. The stream is well-formed, so decoders accept it and return garbage pixels rather than an error. Fixed on main but not yet published, so every crates.io user is currently exposed.
Filing because the fix is unreleased and there is no regression gate on the trigger, so it can silently come back.
Impact
vp8l-m4-ql25 on a 512x320 rendition: 138,316 of 163,840 pixels wrong (84.42%), max |delta| 255, wrong from pixel (0,0) across all 320 rows. Whole-image garbage, not an edge/stride artifact.
Not ql25-specific and not method-specific — observed at ql100 and at default quality 75 with m6:
| rendition | cells | manifestation |
|---|---|---|
o_8148.scale512x320 |
m4-ql25, m6-ql25 | silent corruption |
o_3014.scale512x424 |
m4-ql25, m6-ql25 | BitStreamError |
o_8113.scale896x560 |
m4-ql25, m6-ql25 | BitStreamError |
o_8341.scale384x512 |
m4-ql25, m6-ql25 | BitStreamError |
o_9231.scale597x896 |
m4-ql100, m6-ql100 | BitStreamError |
o_8384.scale192x256 |
m6, default ql75 | BitStreamError |
Trigger is a content x clustering lottery, not a dimension or a dial: for o_8148.scale512x320 a full 7x21 grid (q0-q100 step 5) fails at exactly m4/m5/m6 @ q=25 and nowhere else, and 1-pixel crops (511x320, 512x319) flip it to BitStreamError.
Mechanism
Phase 5 allocates one final histogram per active cluster, including clusters the remap stranded with zero mapped tiles, so the encoder writes Huffman trees for an unreferenced trailing cluster. Decoders size the group list from the entropy image's max symbol (max+1), so the extra trees get parsed as pixel data and the rest of the bitstream shifts.
Published v0.4.4:src/encoder/vp8l/meta_huffman.rs:883:
// Phase 5: Rebuild final histograms from remapped tiles
let mut final_histos: Vec<Histogram> = active_indices
.iter() // one slot per ACTIVE cluster...
.map(|_| Histogram::new(cache_bits)) // ...even with zero tiles mapped to it
.collect();
main fixes this in build_final_histograms (src/encoder/vp8l/meta_huffman.rs:904) by compacting to referenced clusters with dense renumbering. Introduced 3626d22 (2026-02-03), fixed 6930c53 (2026-07-13, squashed into the m0 low-effort commit), noted in CHANGELOG.md under [Unreleased]. Bisect confirms 6930c53 first-good, parent dd871b2 bad. Full 147-cell grid on main: 147/147 bit-exact.
The decoder is innocent
Three decoders — old zenwebp, current zenwebp, and libwebp — reproduce the identical 138,316-pixel corruption, and libwebp decodes main's output bit-exactly. So this is an encoder emitting a valid stream that encodes the wrong pixels; efddb6c (the VP8L decoder refill fix) is not involved.
Two things worth doing
- Publish the fix. 0.4.4 is the current crates.io release and can silently corrupt lossless output at default settings. Lossless users have no signal that anything went wrong.
- Add a regression gate. Nothing currently covers the trigger:
tests/lossless_fast_tier.rs(added with the fix) only testsmethod(0);tests/lossless_roundtrip.rspins quality 75.examples/sweep_validate.rshas the zero-tolerance gate but runs 3 CID22 images and never saw this content. A gate over m4/m5/m6 x the ql axis on content known to strand a cluster would have caught it.
Provenance
Found while rolling up a 5.74M-encode zenmetrics sweep for the coefficient viewer: exactly 2 of 40,464 lossless rows scored below 100 (ssim2 -1613.79, zensim -56.66). The parquet understates it — cells that hard-failed wrote no row at all, so the 2 visible rows are the ones that corrupted quietly rather than the full damage.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.