← All reports

[5] libwebrtc derives send encodings from an unclamped remote layer count

HighWebKit bundled libwebrtcOOB

No remote peer required: the page writes its own WebRTC offer, and picks the count.

e8a476c

High. The encoding vector's length is chosen by whatever SDP the page feeds to setRemoteDescription, and the only bounds discipline at the consumer's entry point was a debug-only assertion compiled out of shipping builds. The out-of-bounds write is the commit's framing; WebKit sized it by trimming at the producer and promoting that assertion to a release check.

Simulcast lets one WebRTC sender publish several encodings of the same video track at different resolutions, and the count of those encodings comes from SDP — the text session description handed to setRemoteDescription. On the answering side, the offer/answer machinery in pc/sdp_offer_answer.cc turns the supplied description's declared receive layers into the local sender's send-encoding list, one encoding per layer. Downstream, the video-coding configuration layer copies that list into a VideoCodec struct whose per-stream array, SimulcastStream simulcastStream[kMaxSimulcastStreams], is a fixed three-element inline array — so the encoding count is expected never to exceed three.

The angle: any page can feed itself a crafted SDP offer through setRemoteDescription — no remote peer involved — and choose how many send encodings the local WebContent process configures, driving an attacker-chosen count into fixed three-slot inline storage in the video encoder configuration path.

GetSendEncodingsFromRemoteDescription() gains a trim so the derived encoding vector is capped at kMaxSimulcastStreams rather than growing to one entry per simulcast.receive_layers() element. Separately, in modules/video_coding/video_codec_initializer.cc, the entry-point bound on VideoCodecInitializer::SetupCodec() is promoted from RTC_DCHECK_LE to RTC_CHECK_LE.

Remote-supplied cardinality propagated into fixed-capacity inline storage, with the only entry-point bound expressed as a debug-only assertion.

SDP and simulcast attributes. A session description declares media streams and their parameters as a= attribute lines. a=rid:<id> recv declares a restriction identifier the peer is willing to receive; a=simulcast:recv lists which of those rids form simulcast layers, with ; separating distinct layers and , separating alternatives within one layer.

From SDP to sender parameters. pc/sdp_offer_answer.cc translates a negotiated description into RTP sender and receiver parameters. After addTrack, the receive layers the remote description declared become the local sender's send encodings — a std::vector<RtpEncodingParameters>.

From encodings to encoder configuration. Those encodings set VideoEncoderConfig::number_of_streams and simulcast_layers, which in turn size the std::vector<VideoStream> streams passed to VideoCodecInitializer::SetupCodec(). That function builds a function-local VideoCodec whose SimulcastStream simulcastStream[kMaxSimulcastStreams] is inline, fixed at three per api/video/video_codec_constants.h.

RTC_DCHECK versus RTC_CHECK. RTC_DCHECK_LE is debug-only and compiles out of shipping builds; RTC_CHECK_LE is unconditional and aborts the process on failure. Promoting one to the other converts a silent violation into a deterministic crash — which is why the promotion is meaningful even alongside a producer-side fix.

The root cause is a missing clamp at the producer. GetSendEncodingsFromRemoteDescription() derived the local sender's encoding list purely from the cardinality of the supplied simulcast description — one RtpEncodingParameters per entry in simulcast.receive_layers(), with no termination of the accumulation loop. The invariant the configuration layer assumes is that the number of send encodings never exceeds kMaxSimulcastStreams. The commit message states this cap was already applied on the addTransceiver path ("we trim them ... like done for addTransceiver") but not on the remote-SDP-derived path; the supplied context does not include the addTransceiver encoding path, so that half of the comparison is relayed from the commit message.

  Before:                              After:
  page SDP (N recv layers)             page SDP (N recv layers)
    +-> GetSendEncodingsFromRemote       +-> GetSendEncodingsFromRemote
          N encodings, unclamped               min(N, kMaxSimulcastStreams)
            +-> VideoEncoderConfig                +-> VideoEncoderConfig
                  number_of_streams = N                 number_of_streams <= 3
                  +-> SetupCodec()                      +-> SetupCodec()
                        RTC_DCHECK_LE (debug)                 RTC_CHECK_LE
                        +-> VideoCodec                        +-> VideoCodec
                              simulcastStream[3]                    simulcastStream[3]

The trigger is straightforward to construct. An SDP offer the page passes to setRemoteDescription, carrying N a=rid:<id> recv lines plus an a=simulcast:recv line listing N ;-separated simulcast layers, yields N receive layers — the regression test uses only the ; separator, so every rid becomes its own layer rather than an alternative within one. After addTrack, those N layers become the sender's send encodings, and N flows all the way into number_of_streams.

Where the write lands is worth stating precisely. The supplied trunk source of SetupCodec() bounds its per-stream loop with const size_t num_streams = std::min(streams.size(), static_cast<size_t>(kMaxSimulcastStreams));, and assigns numberOfSimulcastStreams with the same std::min — so the loop as it stands in the supplied context does not index past simulcastStream[2], and no overflowing write site appears there. Reaching an out-of-bounds write therefore depends on a write site the supplied context does not include: either the SetupCodec() variant on the branch the commit message names as the original landing (Originally-landed-as: 305413.820@safari-7624-branch), or another consumer of the untrimmed encoding vector.

What the supplied context does establish is threefold: the producer emits an unclamped-length vector; the only entry-point bounds discipline on SetupCodec() was a debug-only assertion absent from shipping builds; and WebKit judged the situation severe enough to both trim at the producer and promote the consumer assertion to a release check. The trim in GetSendEncodingsFromRemoteDescription() is the root-cause fix; the assertion promotion converts any residual case into a deterministic abort rather than a silent write.

This vulnerability weakens the boundary between session-description text and fixed-layout encoder configuration state inside the WebContent process — a surface reachable by any page that constructs its own offer and calls setRemoteDescription + addTrack + createAnswer.