Skip to content

Write the last output byte the way C's AddBit does when the output is empty - #220

Open
kunpai wants to merge 1 commit into
google:masterfrom
kunpai:add-bit-empty-output
Open

kunpai wants to merge 1 commit into
google:masterfrom
kunpai:add-bit-empty-output

Conversation

@kunpai

@kunpai kunpai commented Oct 28, 2026 •

Copy link
Copy Markdown

Write the last output byte the way C's AddBit does when the output is empty

Problem

AddBit (C, Rust add_bit) appends one bit to the output. C appends a fresh byte only when the bit pointer is 0, then ORs the bit into (*out)[*outsize - 1]. When the output is empty and the caller passes a non-zero bit pointer, there is no last byte, so C writes to out[-1], before the buffer. The port also appends a byte when the output is empty, so it never writes before the buffer. For example, ZopfliDeflate with an empty output and bp = 146 (so bp & 7 = 2):

C Rust
empty output, bp ≠ 0 writes out[-1], before the buffer, and segfaults (SIGSEGV) pushes a new byte and writes into it

Question

Which behaviour do we want: C's or the Rust port's? Specifically, when the output is empty and the bit pointer is non-zero, should AddBit write to the byte before the buffer (like the C source), or start a new output byte (like the Rust port)?

This PR implements C's behaviour.

Fix

add_bit appends a byte only when *bp == 0 (dropping the || out.is_empty()), then writes the last byte through an unchecked pointer, so an empty output writes out[-1] as C does:

-    if *bp == 0 || out.is_empty() {
+    if *bp == 0 {
         out.push(0);
     }
-    let last_idx = out.len() - 1;
-    out[last_idx] |= bit << *bp;
+    let last_idx = out.len().wrapping_sub(1);
+    unsafe {
+        *out.as_mut_ptr().wrapping_add(last_idx) |= bit << *bp;
+    }

Replayed on the empty-output, bp = 146 record, before and after:

C Rust main Rust patched
trap SIGSEGV (11) none (writes a new byte) SIGSEGV (11)

Both sides now write before the buffer and segfault, and the divergence closes.

Downstream check. The normal path is untouched: ZopfliGzipCompress on the repository's README gives the same 831-byte stream from the port before and after this patch, and gzip.decompress restores the file. C's stream is also 831 bytes but differs in a few bytes, for a separate reason (#213).

Notes for review:

  • This patch reproduces C's undefined behaviour and must not be merged. It writes one byte before the output buffer (out[-1]) through a raw pointer, which is memory-unsafe; safe Rust starts a new byte instead. The PR exists only to show the divergence and ask whether AddBit should be called with a non-zero bit pointer and an empty output, not to ship.
  • A related divergence (bit pointer ≥ 8) is in the same AddBit path: for bp ≥ 8, C shifts the BFINAL bit out of the byte, and the port masks the bit pointer first and keeps it. Write BFINAL with the caller's bit pointer, as C's AddBit does #219 covers it.

C appends a byte only when the bit pointer is 0, so an empty output
with a non-zero bit pointer writes out[-1]. The port also appended a
byte when the output was empty. This reproduces C's out-of-bounds write
and is not meant to be merged.
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.

1 participant