Skip to content

fix(delta): compute the distance in int, not byte - #488

Open
youdie006 wants to merge 1 commit into
bodgit:mainfrom
youdie006:delta-distance-256
Open

fix(delta): compute the distance in int, not byte#488
youdie006 wants to merge 1 commit into
bodgit:mainfrom
youdie006:delta-distance-256

Conversation

@youdie006

Copy link
Copy Markdown

internal/delta/reader.go:87 computes the delta distance in byte arithmetic:

delta: int(p[0] + 1),

7-Zip stores the distance as distance - 1, so a property byte of 0xff means a distance of 256.
p[0] + 1 is evaluated as byte, so 0xff + 1 wraps to 0, and delta becomes 0. The decode loop
in Read (internal/delta/reader.go:57) then never advances:

for i := 0; i < n; {
    for j = 0; j < rc.delta && i < n; i++ {   // rc.delta == 0, so the body never runs
        ...
    }                                          // i never changes -> the outer loop spins
}

An archive using a Delta filter with distance 256 parses cleanly and then hangs the extractor:

parsed ok: files=1 name="a" size=16
HANG: File.Open + io.Copy did not return within 5s

I built an 85-byte archive by hand to check this - a Copy -> Delta folder with the delta property
set to 0xff - rather than mutating an existing one, so it is a well-formed archive rather than a
corrupt blob. Happy to attach it if that is useful for the repo's testdata, though the unit test
below covers the same ground without shipping a file.

The fix

-  delta: int(p[0] + 1),
+  delta: int(p[0]) + 1,

256 is exactly stateSize, so the widened value still fits the [256]byte state and buffer - which
is what the surrounding code was already written for.

Test

internal/delta/reader_test.go, table-driven over both ends of the property byte: 0x00 -> distance
1 and 0xff -> distance 256, asserting the distance and that a Read returns.

I mutation-checked it in both directions:

  • revert to int(p[0] + 1) -> panic: test timed out after 15s (the hang)
  • over-correct to int(p[0]) + 2 -> fails on the minimum case, expected: 1

so the test pins the distance rather than just the absence of a hang. With +2 on the maximum case
the state slice also goes out of range ([:257] with length 256), which is the other side of why 256
is the ceiling.

go test ./... is green: ok github.com/bodgit/sevenzip 81.711s plus all subpackages, including the
existing delta.7z round trip. gofmt -l and go vet ./... clean.

Observable change: only for delta distance 256, which previously never produced output. Nothing
that decoded before decodes differently.

One observation, not part of this change

(*readCloser).Read swallows io.EOF - it returns n, nil on every path - so calling io.Copy
directly on a delta reader does not terminate even with this fix. It does not matter inside
sevenzip, which bounds the read by the known uncompressed size, and it is a separate root cause, so
I left it alone. Mentioning it because it is why the test above calls Read directly rather than
using io.Copy.


Disclosure: found and prepared with AI assistance (Claude). The root cause, the archive construction
and every figure above are mine and verified by hand on this branch.

The property byte holds distance-1, so 0xff means a distance of 256, but
int(p[0] + 1) wraps in byte arithmetic and yields 0. Read then never
advances its index and spins forever on an archive that parses cleanly.
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