fix(mp4): reject undersized boxes instead of underflowing the size - #587
Conversation
25918b1 to
87a7f21
Compare
|
Updated after review feedback. The if hdr.payloadLen() < 4 {
return nil, fmt.Errorf("esds: payload size %d is too small, needs at least 4 bytes", hdr.payloadLen())
}
...
descSize := uint32(hdr.payloadLen() - 4)
This is latent today only because Worth noting separately: the same assumption is spread across 53 container call sites that pass Also dropped the explanatory comments — the error messages carry the information. |
A box whose declared size is smaller than the fixed fields preceding its children leaves startPos past endPos. dref, stsd and trep read their children from 16 bytes in and meta from a computed offset, none of them checking the size first, so endPos-startPos wrapped and the error reported a parent size of about 1.8e19. Guard in both DecodeContainerChildren and DecodeContainerChildrenSR, naming the declared and the minimum size, which also makes every subtraction after the guard safe. DecodeEsdsSR had the same shape in hdr.Size-12. The wrapped value went to DecodeESDescriptor, which ignores the parameter today, so nothing misbehaved yet. Both the check and the descriptor size now measure the payload rather than hdr.Size, which hardcoded an 8-byte header and would be wrong for a box carrying an extended size. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
87a7f21 to
79de58d
Compare
Follow-up to #580, which brought the two container-children loops into
agreement and in doing so made the reader path reach a size underflow the SR
path had been hitting on its own.
The underflow
DecodeContainerChildrenandDecodeContainerChildrenSRboth report amismatch with
startPosandendPosareuint64. Callers that read their children frompast the 8-byte header —
dref.go,stsd.goandtrep.goatstartPos+16,meta.goat a computed offset — never checkhdr.Sizefirst, so a boxdeclaring a size below that offset leaves
startPos > endPosand thesubtraction wraps:
Guarding once before the loop also makes every later
endPos-startPosandpos-startPossafe, sinceposonly grows fromstartPos.This is a bad error message, not a crash: no panic, no allocation driven by the
wrapped value, and the decode already failed either way.
esds
DecodeEsdsSRhas the same shape indescSize := uint32(hdr.Size - 12)with nominimum-size check. The wrapped value is handed to
DecodeESDescriptor, whichignores the parameter entirely today — so nothing misbehaves, and this is a
latent hazard rather than a live bug. Validated so it cannot bite if that
parameter is ever used.
What else was checked
I swept the rest of the box decoders for the same pattern. Everything else is
either already guarded or signed:
ssix.go:77hdr.Size-16if hdr.Size < 16uuid.go:254hdr.Size-16if hdr.Size < 16+8senc.go:151,158payloadLen-8if hdr.payloadLen() < 8elng.go:61plLen-4plLen < 7early returnmdat.go:57hdr.Size-hdr.HdrlenDecodeHeaderguaranteesSize >= Hdrlenvisualsampleentry.go:167endPos-posfor pos < endPosemsg.go,labl.go,stpp.go,eventmessage.gopayloadLen()returns signedint; a negativemaxLenmakesReadZeroTerminatedStringset "did not find terminating zero" rather than wrapbits/fixedslicereader.gos.len-Ns.lenisint; negative compares correctlyReader/SR asymmetry is not a concern for these:
DecodeSsix,DecodeSenc,DecodeEsds,DecodeElng,DecodeEmsgandDecodeLablall delegate to theirSR versions via
readBoxBody, so a guard in the SR path covers both. Thecontainer-children pair is unusual in having two genuinely separate
implementations, which is why it drifted.
Tests
TestDecodeUndersizedContainerNoSizeUnderflowcoversdref,stsdandtrepat sizes 8, 12 and 15, on both decode paths, asserting the error does not
contain a wrapped value. All 18 assertions fail on master.
TestDecodeUndersizedEsdsNoSizeUnderflowasserts the box is rejected as asize, not incidentally by the descriptor tag check that runs first without the
guard — without the fix it fails with
got tag 0 instead of ESDescriptorTag 3.Verification
go build,go vet,gofmt, fullgo test -count=1 ./...green.go test -fuzz FuzzDecodeBox -fuzztime 40s: 2.76M execs, no crash.🤖 Generated with Claude Code