sdp: harden SDPFragment.Unmarshal against out-of-range accesses - #1767
Merged
Conversation
SDPFragment.Unmarshal indexed into a line knowing only that it was
non-empty, so a bare "m" line panicked on line[2:] and a bare "a" line
panicked on line[1]. Both are reachable from a client supplied WHIP
Trickle ICE fragment. Length check before indexing, and require an m=
line to carry a value since Marshal drops the m= line when info is
empty.
While in the file:
- GetIP dereferenced ConnectionInformation.Address, which is a
pointer and can be nil, after only checking ConnectionInformation
itself.
- PatchICECredentialAndCandidatesIntoSDP removed candidate attributes
while ranging over the same slice by index. range captured the
original length, so after the first removal every index was stale:
consecutive candidates were skipped and stale trailing elements
re-read, leaving old candidates behind on an ICE restart. Replaced
with an in-place filter.
- s.media.ice was dereferenced unguarded in ExtractICECredential and
PatchICECredentialAndCandidatesIntoSDP, where a sibling check for
it already existed.
Adds a malformed fragment table test and a fuzz target that checks
Unmarshal never panics and that a parsed fragment re-parses equal after
Marshal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 09cccd3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
paulwe
approved these changes
Sep 3, 2026
This was referenced Sep 3, 2026
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.
SDPFragment.Unmarshalindexed into a line knowing only that it was non-empty:line[2:]on anmline — a bare"m"line panicked withslice bounds out of range [2:1]line[1]on analine — a bare"a"line panickedBoth are reachable from a client-supplied WHIP Trickle ICE fragment. Both are now length-checked before indexing. An
m=line is additionally required to carry a value, sinceMarshaldrops them=line wheninfo == ""and an empty media info therefore broke Marshal/Unmarshal symmetry.The rest of the function was already safe —
delimIndexcomes fromstrings.Index, and thebundleIDs[1]access is behind alen > 1check.Also in the file
GetIP— nil deref.ConnectionInformation.Addressis a*sdp.Addressand can be nil for ac=line the parser accepted without an address; onlyConnectionInformationitself was checked.PatchICECredentialAndCandidatesIntoSDP— mutation while ranging. The candidate cleanup didmd.Attributes = append(md.Attributes[:idx], md.Attributes[idx+1:]...)while ranging over the same slice by index. No panic (the reslices stay within cap), butrangecaptured the original length, so after the first removal every index is stale: consecutive candidates get skipped and stale trailing elements get re-read. Real consequence is old candidates surviving an ICE restart. Replaced with an in-place filter.s.media.icenil derefs inExtractICECredentialandPatchICECredentialAndCandidatesIntoSDP, where a sibling check for it already existed. Defensive — every current construction path setsice.Left alone:
ExtractFingerprint,ExtractStreamID,GetMediaStreamTrack,GetSimulcastRids,GetBundleMid— their slice accesses are already behind explicit length checks.One deliberate non-change:
"a=group:BUNDLE"with no mids still parses without error (the bundle-mid check is skipped when there is only one token). That is a validation gap rather than a memory-safety one, and it matchesGetBundleMid's lenient behavior.Tests
TestSDPFragmentUnmarshalMalformed— table of truncated/malformed fragments that must error rather than panic.FuzzSDPFragmentUnmarshal— fuzzesUnmarshal, then exercisesMid/Candidates/ExtractICECredential/Marshalon anything that parsed, and asserts the marshalled output re-parses to an equal fragment.go test ./sdp/passes; 30s / 12.2M fuzz execs clean. The two panics were confirmed against the pre-fix code.🤖 Generated with Claude Code