Skip to content

fix silent truncation of long literal runs in inflate - #295

Open
spokodev wants to merge 1 commit into
101arrowz:masterfrom
spokodev:fix-inflate-literal-run-truncation
Open

fix silent truncation of long literal runs in inflate#295
spokodev wants to merge 1 commit into
101arrowz:masterfrom
spokodev:fix-inflate-literal-run-truncation

Conversation

@spokodev

Copy link
Copy Markdown

inflateSync (and the other auto-sizing inflate entry points) can return truncated output, with no error, for a valid DEFLATE stream that zlib and pako both decode in full.

The output buffer is grown at two points in inflt: at each block start and before each match, both reserving bt + 131072. A literal, though, is written straight to buf[bt++] with no capacity check. A block that emits a long run of literals with no intervening match runs past that reservation — the out-of-range Uint8Array writes are silently dropped while bt keeps counting, and the final subarray(0, bt) clamps back to the buffer length, so the tail is lost without a thrown error.

Repro

A raw-DEFLATE block whose literal/length alphabet is just {0x41, EOB} at one bit each encodes every byte in a single bit, so the output is ~8× the input and outgrows the sl * 3 initial buffer:

N literals    input     zlib / pako    fflate.inflateSync
   200000    25013 B       200000        150078   (truncated, no error)
  1000000   125013 B      1000000        375039   (truncated, no error)
  2000000   250013 B      2000000        750039   (truncated, no error)

The harness crafts the stream directly; zlib.inflateRawSync and pako.inflateRaw are the oracles, and both return the full output for every case.

Fix

Recheck capacity on the literal path the way the match path already does, and only when resizing:

if (sym < 256) {
  if (resize && bt == buf.length) cbuf(bt + 131072);
  buf[bt++] = sym;
}

resize is false when the caller passes a fixed out buffer, so that path and its documented exact-size behaviour are untouched; and a literal is the only thing that advances bt by one without a reservation, so bt == buf.length is the exact overflow point.

Cost is negligible. Best-of-3 over 8 MB payloads: realistic text (72× ratio) auto-size decode moved 11.36 → 11.53 ms/op with the user-buffer path unchanged; a literal-heavy payload (4.5× ratio) stayed within run-to-run noise. The known-size (out-buffer) fast path takes the branch zero times.

Test

test/6-inflate.ts crafts the literal-run stream and asserts inflateSync matches zlib.inflateRawSync in length and content. It fails on master (375039 vs 1000000) and passes with the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant