Skip to content

sdp: harden SDPFragment.Unmarshal against out-of-range accesses - #1767

Merged
boks1971 merged 1 commit into
mainfrom
sdp-harden-unmarshal
Sep 3, 2026
Merged

sdp: harden SDPFragment.Unmarshal against out-of-range accesses#1767
boks1971 merged 1 commit into
mainfrom
sdp-harden-unmarshal

Conversation

@boks1971

@boks1971 boks1971 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

SDPFragment.Unmarshal indexed into a line knowing only that it was non-empty:

  • line[2:] on an m line — a bare "m" line panicked with slice bounds out of range [2:1]
  • line[1] on an a line — a bare "a" line panicked

Both 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, since Marshal drops the m= line when info == "" and an empty media info therefore broke Marshal/Unmarshal symmetry.

The rest of the function was already safe — delimIndex comes from strings.Index, and the bundleIDs[1] access is behind a len > 1 check.

Also in the file

  • GetIP — nil deref. ConnectionInformation.Address is a *sdp.Address and can be nil for a c= line the parser accepted without an address; only ConnectionInformation itself was checked.
  • PatchICECredentialAndCandidatesIntoSDP — mutation while ranging. The candidate cleanup did md.Attributes = append(md.Attributes[:idx], md.Attributes[idx+1:]...) while ranging over the same slice by index. No panic (the reslices stay within cap), but range captured 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.ice nil derefs in ExtractICECredential and PatchICECredentialAndCandidatesIntoSDP, where a sibling check for it already existed. Defensive — every current construction path sets ice.

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 matches GetBundleMid's lenient behavior.

Tests

  • TestSDPFragmentUnmarshalMalformed — table of truncated/malformed fragments that must error rather than panic.
  • FuzzSDPFragmentUnmarshal — fuzzes Unmarshal, then exercises Mid/Candidates/ExtractICECredential/Marshal on 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

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-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 09cccd3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
github.com/livekit/protocol Patch
@livekit/protocol Patch

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

@boks1971
boks1971 merged commit 0cf5ba0 into main Sep 3, 2026
9 checks passed
@boks1971
boks1971 deleted the sdp-harden-unmarshal branch September 3, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants