Repository navigation
pkg/profile: bounds-check DecodeInto - #6419
Open
guettlibot wants to merge 1 commit into
Open
guettlibot wants to merge 1 commit into
guettlibot wants to merge 1 commit into
Conversation
DecodeInto indexes and slices caller-supplied bytes with no length checks.
Feeding it every prefix of one real encoded location, 55 of the 66 truncations
panic. There is no recover() anywhere in pkg or cmd outside
pkg/symbol/addr2line, and no gRPC recovery interceptor, so that is not a failed
query -- it is a dead server.
This one is on the default path. DecodeInto is what pkg/parcacol/querier.go
uses to read locations back, so it runs for every query against the FrostDB
backend, on bytes that have been to storage and back. Corruption in a stored
block is a more plausible way to reach a malformed record than an in-process
round trip is.
Three distinct ways it could fault:
- data[offset] for the two flag bytes, past the end of a short record.
- data[offset:] for every varint, likewise.
- decodeString's data[n : n+int(length)]. The length is unsigned: one above
MaxInt converts to a NEGATIVE int, so n+int(length) lands BELOW n. That
satisfies a naive "n+int(length) > len(data)" guard and then panics on a
slice whose high bound is below its low one, so the check has to be made in
the space the length was read in.
Panicking is not even the worst outcome. These bytes usually arrive as an Arrow
value, and array.Binary.Value slices with cap running to the end of the whole
buffer -- so a read past len but inside cap does not fault, it returns the
bytes of the NEXT location, which then decode as this location's own mapping or
function. Plausible-looking, and nothing would flag it.
Each read now reports whether it succeeded and a record that runs out returns a
descriptive error naming the field and offset. The signature already returns an
error, so there is no reason to guess: a malformed location fails the query
that touched it rather than degrading quietly or killing the process.
decodeString itself is left alone. It has thirteen other call sites across
pkg/profile and pkg/symbolizer, several on decoders with their own
self-consistent formats, so changing its contract is a wider change than this
one; the checks live in DecodeInto, matching the shape already used by
DecodeSymbolizationInfo.
Behaviour on well-formed input is unchanged, which the existing round-trip
tests pin. The truncation sweep runs over five record shapes because each
reaches a different set of reads, and slices each prefix as encoded[:i:i]: a
two-index slice drops the original cap, and Go does not panic reading past len
while still inside cap, so the cheaper spelling silently passes on inputs that
really do over-read.
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.
Independent of #6415, #6416, #6417 and #6418 — branches from
main, touches onlypkg/profile/decode.go.This is the one on the default path. The other PRs in this batch are confined to the ClickHouse backend;
DecodeIntois whatpkg/parcacol/querier.go:1133uses to read locations back, so it runs for every query against the FrostDB backend.What
DecodeIntoindexes and slices caller-supplied bytes with no length checks at all. Feeding it every prefix of one real encoded location:And there is no safety net:
So a corrupt or truncated stored location is a dead server, not a failed query. These bytes have been to storage and back, which makes corruption a more plausible route to a malformed record than an in-process round trip.
Three ways it could fault
data[offset]for the two flag bytes, past the end of a short record.data[offset:]for every varint, likewise.decodeString'sdata[n : n+int(length)]. The length is unsigned: one aboveMaxIntconverts to a negativeint, son+int(length)lands belown. That satisfies a naiven+int(length) > len(data)guard and then panics on a slice whose high bound is below its low one — so the check has to be made in the space the length was read in.Panicking is not the worst outcome
These bytes usually arrive as an Arrow value, and
array.Binary.Valuereturns a two-index slice whosecapruns to the end of the whole buffer. A read pastlenbut insidecaptherefore does not fault — it returns the bytes of the next location, which then decode as this location's own mapping or function. Plausible-looking, and nothing would flag it.Fix
Each read reports whether it succeeded, and a record that runs out returns a descriptive error naming the field and offset:
The signature already returns an
error, so there is no reason to guess: a malformed location fails the query that touched it rather than degrading quietly or killing the process.decodeStringitself is left alone — it has thirteen other call sites acrosspkg/profileandpkg/symbolizer, several belonging to decoders with their own self-consistent formats, so changing its contract is a wider change than this one. The checks live inDecodeInto, matching the shape already used byDecodeSymbolizationInfo.Tests
Behaviour on well-formed input is unchanged (the existing
TestEncodeDecodeandTestDecodeFallsBackToSystemNamepin that). The truncation sweep runs over five record shapes, because each reaches a different set of reads, and slices each prefix asencoded[:i:i]— a two-index slice drops the originalcap, and Go does not panic reading pastlenwhile still insidecap, so the cheaper spelling silently passes on inputs that really do over-read. Oversized lengths (1<<20,1<<63,MaxUint64) are covered too.Verified the new tests fail on
main(should not panic).go test(incl.-race),go vet,gofmt, license check clean acrosspkg/profile,pkg/parcacol,pkg/normalizer,pkg/clickhouse,pkg/parca,pkg/symbolizer.Not in this PR
pkg/symbolizerhas its owndecodeStringanddecodeLineswith the same unchecked style. That pair is self-consistent withpkg/symbolizer/encode.goand has a round-trip test, so it is a separate, lower-risk case — happy to follow up if you'd like it hardened too.