Repository navigation
fix(codegen-go): bound decoded sequence lengths - #398
Open
YuanYuYuan wants to merge 8 commits into
Open
YuanYuYuan wants to merge 8 commits into
YuanYuYuan wants to merge 8 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 Changes recommended
Nested-message decoding still permits excessive allocations before rejecting hostile input.
1 open issue
What changed in this PR
This PR hardens generated Go CDR decoders against excessive allocations caused by hostile sequence lengths.
Changes:
- Adds pre-allocation length checks for primitive, string, time, and duration sequences.
- Uses manifest-derived minimum encoded sizes to limit nested-message reservations.
- Adds hostile-input, valid-boundary, and 32-bit regression probes.
| File | Description |
|---|---|
crates/hiroz-codegen-go/roundtrip_integration_test.go |
Adds runtime allocation probes and cross-architecture checks. |
crates/hiroz-codegen-go/main.go |
Registers manifest messages before generation. |
crates/hiroz-codegen-go/main_test.go |
Tests generated bounds and minimum-size calculations. |
crates/hiroz-codegen-go/generator.go |
Emits sequence checks and size-aware nested reservations. |
Review effort: Balanced
A sequence element of a type with a huge fixed array is a huge Go array, so var elem reserved it even when no bytes were left to decode. Reject the element first when fewer bytes are left than its known minimum size.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
The generated Go decoder allocates a slice from a length that it reads off the wire, with no limit. A hostile or corrupt message with a huge length makes the decoder allocate until the process runs out of memory. This change bounds every such reservation by the bytes left in the input.
Key Changes
makewith itmakemin(count, bytes left / minimum element size)elements, and rejects an element before it allocates it when fewer bytes are left than its known minimum sizeminCDRSizecomputes a lower bound on the encoded size of one messageA bound on the element count is not a bound on memory. The bound for primitives, strings, times and durations counts one byte per element. A wide element type can therefore still reserve several times the input size, 8 times for
float64. Nested messages use the minimum element size below. An element that holds auint8[65536]field takes at least 65536 bytes. A count of 65536 with 65536 bytes left passes a count bound and still reserves 4 GiB.The emitted bound for a sequence of nested messages is:
The divisor is the minimum encoded size of one element, at least 1. The generator computes it from the manifest:
Before each
var elem, the loop checks that at least that many bytes are left. A Go array field is part of its element. An element with auint8[1073741824]field is therefore a 1 GiB value, even when no bytes remain to decode it. The slice bound does not prevent that allocation. The check applies to an element with a known minimum size of 1 byte or more.The generator ignores alignment padding, which only adds bytes. A type that is not in the manifest counts as 0.
What stays unbounded: an element of zero bytes, which is a message with no fields, or a type that is not in the manifest. A sequence of such elements loops
counttimes, which costs CPU time. A message with no fields has a Go size of zero, so its reservation costs no memory. A type that is not in the manifest gets no byte-aware bound: the code reservesmin(count, bytes left)elements of the real element size, which is large when that type holds a big array. The generator does not refuse such a type.What fails without this
TestCustomSequenceReservationIsByteAwareTestHostileBigElements(insideTestRoundtripRejectsHostileSequenceLength)TestHostileHugeElement(insideTestRoundtripRejectsHostileSequenceLength)TestMinCDRSizeRed for the per-element check: with the check disabled in the generator,
TestHostileHugeElementfails onf3de75a50. Red:go test ./... -count=1 -vincrates/hiroz-codegen-gofails at the test-only commit6bb72c9. Green: the fix commit is68cce85. The last commitbafe726keeps the tests 32-bit clean. Two earlier commits pin and fix the length bound for primitives, strings, times and durations: the testb6a2157, then the fix06fe8d5.Cross-arch probes
Two probes build the decoder with
GOARCH=386, whereint(uint32)of0xFFFFFFFFis negative. The test now classifies the host first.go tool dist listhas noGOOS/386pair (for exampledarwin/386)exec format error)linux/386)On the head commit
f3de75a50, on Linux,go test ./... -count=1 -vincrates/hiroz-codegen-gopasses: 18 top-level tests and 28 subtests, 0 failed. Both 386 probes ran and passed.go vet ./...andgofmt -lprint nothing.GOARCH=386 CGO_ENABLED=0 go vet ./...passes too, so the test package compiles for 32-bit targets. The skip path ran only in the unit test of the classifier, not on macOS. The pair list on Linux haslinux/386and has nodarwin/386.Breaking Changes
None. A well-formed message decodes as before.