Skip to content

zstd: Fix crash on amd64 (no BMI) + Go fuzz test - #645

Merged
klauspost merged 10 commits into
masterfrom
port-zstd-fuzz-tests
Jul 20, 2022
Merged

zstd: Fix crash on amd64 (no BMI) + Go fuzz test#645
klauspost merged 10 commits into
masterfrom
port-zstd-fuzz-tests

Conversation

@klauspost

Copy link
Copy Markdown
Owner

Port zstd fuzz tests to Go 1.18 fuzz tests.

(May need time extension on Go 1.18 CI)

@klauspost

klauspost commented Jul 17, 2022

Copy link
Copy Markdown
Owner Author
2022-07-17T10:45:40.7398600Z unexpected fault address 0xb01dfacedebac1e
2022-07-17T10:45:40.7399010Z fatal error: fault
2022-07-17T10:45:40.7399390Z [signal SIGSEGV: segmentation violation code=0x1 addr=0xb01dfacedebac1e pc=0x120bfa5]
2022-07-17T10:45:40.7399630Z 
2022-07-17T10:45:40.7399790Z goroutine 669531 [running]:
2022-07-17T10:45:40.7400050Z runtime.throw({0x1272ed5?, 0xc0000d90e0?})
2022-07-17T10:45:40.7402570Z 	/Users/runner/hostedtoolcache/go/1.18.3/x64/src/runtime/panic.go:992 +0x71 fp=0xc0000d8fd0 sp=0xc0000d8fa0 pc=0x10341d1
2022-07-17T10:45:40.7402950Z runtime.sigpanic()
2022-07-17T10:45:40.7403430Z 	/Users/runner/hostedtoolcache/go/1.18.3/x64/src/runtime/signal_unix.go:825 +0x305 fp=0xc0000d9020 sp=0xc0000d8fd0 pc=0x104a665
2022-07-17T10:45:40.7403930Z github.com/klauspost/compress/zstd.sequenceDecs_decodeSync_safe_amd64(0xc000910298, 0xc00083a7b0, 0xc0000d90f8)
2022-07-17T10:45:40.7404430Z 	/Users/runner/work/compress/compress/zstd/seqdec_amd64.s:3008 +0x2c5 fp=0xc0000d9070 sp=0xc0000d9020 pc=0x120bfa5
2022-07-17T10:45:40.7404910Z github.com/klauspost/compress/zstd.(*sequenceDecs).decodeSyncSimple(0xc000910298, {0xc00649a326?, 0x0, 0x11cbc78?})
2022-07-17T10:45:40.7405380Z 	/Users/runner/work/compress/compress/zstd/seqdec_amd64.go:110 +0x29d fp=0xc0000d91f0 sp=0xc0000d9070 pc=0x11ce89d
2022-07-17T10:45:40.7406280Z github.com/klauspost/compress/zstd.(*sequenceDecs).decodeSync(0xc000910298, {0xc00649a326, 0x0, 0x10})
2022-07-17T10:45:40.7406810Z 	/Users/runner/work/compress/compress/zstd/seqdec.go:207 +0x4e fp=0xc0000d9358 sp=0xc0000d91f0 pc=0x11ccd4e
2022-07-17T10:45:40.7407270Z github.com/klauspost/compress/zstd.(*blockDec).decodeCompressed(0xc0005ce000, 0xc000910290)
2022-07-17T10:45:40.7407840Z 	/Users/runner/work/compress/compress/zstd/blockdec.go:491 +0x19a fp=0xc0000d93b0 sp=0xc0000d9358 pc=0x11a815a
2022-07-17T10:45:40.7408290Z github.com/klauspost/compress/zstd.(*blockDec).decodeBuf(0xc0005ce000, 0xc000910290)
2022-07-17T10:45:40.7409700Z 	/Users/runner/work/compress/compress/zstd/blockdec.go:258 +0x2e5 fp=0xc0000d9468 sp=0xc0000d93b0 pc=0x11a6d45
2022-07-17T10:45:40.7410420Z github.com/klauspost/compress/zstd.(*frameDec).runDecoder(0xc000910240, {0xc00649a000?, 0x326, 0xc0000961c0?}, 0xc0005ce000)
2022-07-17T10:45:40.7410950Z 	/Users/runner/work/compress/compress/zstd/framedec.go:377 +0x193 fp=0xc0000d94d8 sp=0xc0000d9468 pc=0x11c7153
2022-07-17T10:45:40.7412160Z github.com/klauspost/compress/zstd.(*Decoder).DecodeAll(0xc0005ca000, {0xc008192e10, 0xe6, 0xf0}, {0x0, 0x0, 0x0})
2022-07-17T10:45:40.7412680Z 	/Users/runner/work/compress/compress/zstd/decoder.go:381 +0x64a fp=0xc0000d96a0 sp=0xc0000d94d8 pc=0x11af46a
2022-07-17T10:45:40.7413280Z github.com/klauspost/compress/zstd.FuzzDecodeAll.func1(0xc000122000, {0xc008192e10, 0xe6, 0xf0})
2022-07-17T10:45:40.7414120Z 	/Users/runner/work/compress/compress/zstd/fuzz_test.go:44 +0xac fp=0xc0000d9770 sp=0xc0000d96a0 pc=0x11fba8c
2022-07-17T10:45:40.7414680Z runtime.call32(0xc00009cf90, 0xc0001647b0, 0x0, 0x0, 0x0, 0x20, 0xc0000d9cb0)
2022-07-17T10:45:40.7419040Z 	/Users/runner/hostedtoolcache/go/1.18.3/x64/src/runtime/asm_amd64.s:702 +0x49 fp=0xc0000d97a0 sp=0xc0000d9770 pc=0x1064969
2022-07-17T10:45:40.7419530Z runtime.reflectcall(0x122b380?, 0xc00855ed08?, 0x4?, 0x127686b?, 0x0?, 0x12?, 0x122b380?)
2022-07-17T10:45:40.7420090Z 	<autogenerated>:1 +0x3c fp=0xc0000d97e0 sp=0xc0000d97a0 pc=0x1068a7c
2022-07-17T10:45:40.7420530Z reflect.Value.call({0x1230540?, 0xc0001647b0?, 0x13?}, {0x1272c3e, 0x4}, {0xc00009c450, 0x2, 0x2?})
2022-07-17T10:45:40.7421040Z 	/Users/runner/hostedtoolcache/go/1.18.3/x64/src/reflect/value.go:556 +0x845 fp=0xc0000d9dd0 sp=0xc0000d97e0 pc=0x10aa9e5
2022-07-17T10:45:40.7421430Z reflect.Value.Call({0x1230540?, 0xc0001647b0?, 0x514?}, {0xc00009c450, 0x2, 0x2})
2022-07-17T10:45:40.7421930Z 	/Users/runner/hostedtoolcache/go/1.18.3/x64/src/reflect/value.go:339 +0xbf fp=0xc0000d9e48 sp=0xc0000d9dd0 pc=0x10aa0df
2022-07-17T10:45:40.7422480Z testing.(*F).Fuzz.func1.1(0xc00038e340?)
2022-07-17T10:45:40.7422950Z 	/Users/runner/hostedtoolcache/go/1.18.3/x64/src/testing/fuzz.go:337 +0x231 fp=0xc0000d9f70 sp=0xc0000d9e48 pc=0x10db951
2022-07-17T10:45:40.7423290Z testing.tRunner(0xc000122000, 0xc000118c60)
2022-07-17T10:45:40.7423760Z 	/Users/runner/hostedtoolcache/go/1.18.3/x64/src/testing/testing.go:1439 +0x102 fp=0xc0000d9fc0 sp=0xc0000d9f70 pc=0x10e4382
2022-07-17T10:45:40.7424270Z testing.(*F).Fuzz.func1.2()
2022-07-17T10:45:40.7424740Z 	/Users/runner/hostedtoolcache/go/1.18.3/x64/src/testing/fuzz.go:324 +0x2a fp=0xc0000d9fe0 sp=0xc0000d9fc0 pc=0x10db6ea
2022-07-17T10:45:40.7425070Z runtime.goexit()
2022-07-17T10:45:40.7425500Z 	/Users/runner/hostedtoolcache/go/1.18.3/x64/src/runtime/asm_amd64.s:1571 +0x1 fp=0xc0000d9fe8 sp=0xc0000d9fe0 pc=0x1066481
2022-07-17T10:45:40.7425880Z created by testing.(*F).Fuzz.func1
2022-07-17T10:45:40.7426360Z 	/Users/runner/hostedtoolcache/go/1.18.3/x64/src/testing/fuzz.go:324 +0x5b8

@klauspost

Copy link
Copy Markdown
Owner Author

@WojciechMula It seems this problem can be reliably triggered by disabling BMI

It seems like it is possible to get to Label(name + "_offset_nonzero") with an offset value > 3.

It could either be triggered by an invalid table or incorrect decoding.

@klauspost

Copy link
Copy Markdown
Owner Author

updateLength appears to be the problem.

@klauspost
klauspost force-pushed the port-zstd-fuzz-tests branch from 7b38327 to 5357b67 Compare July 20, 2022 10:27
@klauspost

klauspost commented Jul 20, 2022

Copy link
Copy Markdown
Owner Author

updateLength would output junk values if bitsRead > 64. adjustOffsetInMemory could then see offsetB <= 1, but offset being >3. This would cause Mem{Base: prevOffset, Index: CX, Scale: 8} to crash since CX was too big.

This would happen on corrupted input.

@klauspost klauspost changed the title tests: Port zstd fuzz test zstd: Fix crash on amd64 (no BMI) + Go fuzz test Jul 20, 2022
@klauspost
klauspost merged commit 4b4f3c9 into master Jul 20, 2022
@klauspost
klauspost deleted the port-zstd-fuzz-tests branch July 20, 2022 16:06
lizthegrey added a commit to honeycombio/compress that referenced this pull request Jul 18, 2026
The per-sequence space check in executeSingleTriple (used by the sync
decoder's inlined execute) requires only outPos+ll+ml <= cap(s.out). The
unsafe extended copies, however, write in 16-byte blocks and overrun the
logical end of a run by up to compressedBlockOverAlloc-1 (15) bytes. When a
stream's decoded length lands within the 16-byte over-allocation slack --
e.g. a malformed frame whose declared content size is a few bytes below what
its sequences actually produce -- the check passes but the final block copy
writes past cap(s.out).

The overrun is at most 15 bytes and Go's size-class rounding places it in
slop that -race and -asan do not poison, so it corrupts an adjacent live
allocation only intermittently, surfacing later as a crash in the garbage
collector rather than at the write. That is exactly the 'rare, random
crashes with fuzz testing' klauspost#644 disabled the unsafe path for in 2022; it was
never the bitReader overread fixed in klauspost#645, but this missing copy margin.

Reserve compressedBlockOverAlloc in the space check on the unsafe (non-safe)
path only, so the overrun stays within cap(s.out); a well-formed decode ends
at cap-16 and is unaffected, while an over-producing stream now errors with
error_not_enough_space instead of corrupting memory. The safe copies are
bounds-exact and keep the tight check. amd64 asm gains one ADDQ per unsafe
variant; the safe variants are unchanged.

TestDecodeSyncUnsafeOOB drives the unsafe asm with real captured sequences
into a canary-guarded buffer sized to model the under-reported case; it
fails (canary overwritten past cap) on the unpatched asm and passes here, on
amd64 and arm64. Validated additionally with the full zstd suite and
FuzzDecodeAll (default and -tags=nounsafe) on go1.25 + go1.26, both arches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
klauspost added a commit that referenced this pull request Jul 21, 2026
* zstd: Re-enable unsafe decodeSync memory copies (#1168)

decodeSyncSimple has hardcoded `const useSafe = true` since #644 (2022),
making the faster `sequenceDecs_decodeSync_{amd64,bmi2,arm64}` asm variants
dead code that nothing calls. The #644 comment cites "rare, random crashes
with fuzz testing" from the extended (16-byte-block) copies.

Those copies were only the amplifier. The root cause was an unguarded
bitReader overread in updateLength that produced out-of-range match
offsets/lengths; it was fixed three days later in #645, which also added
the Go fuzz corpus that has guarded this path ever since. With the source
of the bad values gone and the +compressedBlockOverAlloc (16-byte) slack
present on every output/literal buffer today, the dynamic useSafe guard —
identical in shape to the still-active one in executeSimple — is sound.

Restore the dynamic selection. On arm64 (Cortex-A72) DecodeAll improves
by +8.7% geomean throughput (n=6, p=0.002), up to +16% on text.

Because a stray 15-byte overrun lands in an adjacent live allocation,
neither -race nor plain fuzzing can observe it; add a CGO+clang asan fuzz
job over FuzzDecodeAll/FuzzDecAllNoBMI2 on amd64+arm64 so any future
maxSyncLen/allocation regression is caught deterministically.

Validated on amd64 and arm64: full test suite; asm-vs-noasm differential
fuzz (byte-identical output); and asan over the decode corpus plus
2.4M mutation execs with no reports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* zstd: fix out-of-bounds write in unsafe decodeSync space check

The per-sequence space check in executeSingleTriple (used by the sync
decoder's inlined execute) requires only outPos+ll+ml <= cap(s.out). The
unsafe extended copies, however, write in 16-byte blocks and overrun the
logical end of a run by up to compressedBlockOverAlloc-1 (15) bytes. When a
stream's decoded length lands within the 16-byte over-allocation slack --
e.g. a malformed frame whose declared content size is a few bytes below what
its sequences actually produce -- the check passes but the final block copy
writes past cap(s.out).

The overrun is at most 15 bytes and Go's size-class rounding places it in
slop that -race and -asan do not poison, so it corrupts an adjacent live
allocation only intermittently, surfacing later as a crash in the garbage
collector rather than at the write. That is exactly the 'rare, random
crashes with fuzz testing' #644 disabled the unsafe path for in 2022; it was
never the bitReader overread fixed in #645, but this missing copy margin.

Reserve compressedBlockOverAlloc in the space check on the unsafe (non-safe)
path only, so the overrun stays within cap(s.out); a well-formed decode ends
at cap-16 and is unaffected, while an over-producing stream now errors with
error_not_enough_space instead of corrupting memory. The safe copies are
bounds-exact and keep the tight check. amd64 asm gains one ADDQ per unsafe
variant; the safe variants are unchanged.

TestDecodeSyncUnsafeOOB drives the unsafe asm with real captured sequences
into a canary-guarded buffer sized to model the under-reported case; it
fails (canary overwritten past cap) on the unpatched asm and passes here, on
amd64 and arm64. Validated additionally with the full zstd suite and
FuzzDecodeAll (default and -tags=nounsafe) on go1.25 + go1.26, both arches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* zstd: assert TestDecodeSyncUnsafeOOB exercises the unsafe copy path

Review feedback on the regression test: it verified only that the asm path
was dispatched, relying implicitly on the buffer geometry to select the
unsafe (extended-copy) variant. If the useSafe conditions or the test
harness allocation ever changed, the test could silently degrade into
exercising the bounds-exact safe copies while still passing.

Extract decodeSyncSimple's selection into useSafeDecodeSync() (pure
refactor, no behavior change) and have the test call the same helper to
fail loudly if its buffer sizing would select the safe variant. The canary
validation and asm-path assertion are unchanged; the test still fails via
the canary against the unpatched assembly on both architectures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* zstd: fold unsafe-copy margin into the space-check LEAQ

Klaus's review on #1171 pointed out the compressedBlockOverAlloc margin
can be folded into the existing LEAQ displacement instead of a separate
ADDQ. Saves one instruction per unsafe variant on amd64/bmi2; the safe
path (addMargin=0) and arm64 codegen are unaffected in substance.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Klaus Post <klauspost@gmail.com>
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