← WebKit Silent-Fix Report — 2026-W22

8af0c2d1ea  [GStreamer][WebRTC][Rice] webrtc/datachannel/multiple-connections.html still flaky crashing in Debug

severity medium class Race confidence 0.60 WebCore GStreamer WebRTC exploitable-grade
Philippe Normand Thu May 28 00:39:20 2026 -0700 full: 8af0c2d1ea007242f9f1b4b4f36e650c5d9d3b5e bug report ↗ view on GitHub ↗
Primitive: use of ICE agent after close on async callback
Triage note: Adds closed-state checks in async callbacks to avoid operating on a torn-down agent, a lifetime/race crash fix.
Contents

The bug at a glance

A use-after-close / lifetime race in the GStreamer WebRTC ICE backend: an asynchronous incoming-data callback scheduled during ICE agent configuration can run after the agent has been closed, operating on torn-down agent state. The observed symptom is a flaky Debug crash in webrtc/datachannel/multiple-connections.html; the class is a race on a torn-down object reachable when a peer connection with data channels is closed while ICE activity is in flight. Medium/0.6: real crash and clear closed-state race, but it is in the GStreamer (non-Apple) port and the demonstrated impact is a flaky crash rather than a proven controllable primitive.

webkitGstWebRTCIceAgentConfigure() installs an asynchronous callback that, on incoming data, resolves the agent via a weak pointer and applies it to a stream. The weak-pointer check only proves the object still exists, not that the ICE agent is still open. If the agent was closed between scheduling and running the callback, the code proceeds to find a stream and handle incoming data on a closed agent – a use-after-close race. The fix adds a closed-state check.

Root cause

WebKitGstIceAgent manages ICE for a GStreamer-backed RTCPeerConnection. Its lifecycle includes an explicit close, tracked by priv->state == AgentState::Closed under priv->stateLock, and a webkitGstWebRTCIceAgentClosed() path that clears the close promise.

webkitGstWebRTCIceAgentConfigure() sets up an asynchronous callback to deliver incoming data. That callback captures a weak reference (weakThis) and begins with auto self = weakThis.get(); if (!self) return;. The weak check guards against the agent object having been destroyed, but it does not guard against the agent having been closed while still alive. Because the callback runs asynchronously (driven by the GStreamer main context / run loop), there is a window in which the ICE agent is closed after the callback was scheduled but before it executes. In that window self is non-null, so the old code proceeded into findStreamAndApply(self.get(), streamId, …) and webkitGstWebRTCIceStreamHandleIncomingData(…), operating on the internal stream/agent state of an agent that has already been torn down by close – a use-after-close on that state, manifesting as a flaky Debug crash in the multiple-connections datachannel test.

The fix introduces a helper, webkitGstWebRTCIceAgentIsClosed(agent), that takes priv->stateLock and returns whether priv->state == AgentState::Closed. This refactors the same locked state read that webkitGstWebRTCIceAgentClose() already performed inline in its wait loop (which the patch replaces with a call to the helper). Critically, the configure callback now adds if (webkitGstWebRTCIceAgentIsClosed(self.get())) return; immediately after the weakThis liveness check, so a callback that fires after close bails out before touching any stream state. This closes the race by making liveness and open-state both preconditions for handling incoming data.

Key code

GStreamerIceAgent.cpp: closed-state guard in the async incoming-data callback

static bool webkitGstWebRTCIceAgentIsClosed(WebKitGstIceAgent* agent)
{
    Locker locker { agent->priv->stateLock };
    return agent->priv->state == AgentState::Closed;
}

// ... in webkitGstWebRTCIceAgentConfigure()'s async callback:
        auto self = weakThis.get();
        if (!self)
            return;
        if (webkitGstWebRTCIceAgentIsClosed(self.get()))
            return;
        findStreamAndApply(self.get(), streamId, [protocol, from = WTF::move(from), to = WTF::move(to), data = WTF::move(data)](const auto* stream) mutable {
            webkitGstWebRTCIceStreamHandleIncomingData(stream, protocol, WTF::move(from), WTF::move(to), WTF::move(data));
        });

Patch walkthrough

  • Source/WebCore/Modules/mediastream/gstreamer/GStreamerIceAgent.cpp — Adds static bool webkitGstWebRTCIceAgentIsClosed(WebKitGstIceAgent*) that locks priv->stateLock and returns state == AgentState::Closed. webkitGstWebRTCIceAgentClose()’s inline locked check-and-compare inside its 2-second wait loop is replaced by a call to the new helper (pure refactor). In webkitGstWebRTCIceAgentConfigure(), after the existing auto self = weakThis.get(); if (!self) return; liveness check, a new if (webkitGstWebRTCIceAgentIsClosed(self.get())) return; is added so the asynchronous incoming-data callback does not run findStreamAndApply / webkitGstWebRTCIceStreamHandleIncomingData on an agent that has been closed.

Background

WebKitGstIceAgent / AgentState — The GStreamer WebRTC ICE agent object; priv->state tracks lifecycle including AgentState::Closed, protected by priv->stateLock. Close tears down agent/stream state that in-flight callbacks must no longer touch.

weakThis liveness vs open-state — The async callback captures a WeakPtr; weakThis.get() returns null only if the object was destroyed. It does not report whether the agent was closed, so a live-but-closed agent passes the null check.

Async GStreamer callbacks — Incoming-data handling is scheduled on the GStreamer main context / run loop, so callbacks can execute after the peer connection (and ICE agent) has been closed, creating a use-after-close window.

findStreamAndApply / handleIncomingData — The callback locates the ICE stream by id and invokes webkitGstWebRTCIceStreamHandleIncomingData on it; run against a closed agent this operates on torn-down stream state.

Vulnerability window

  1. Configure — webkitGstWebRTCIceAgentConfigure() schedules an async incoming-data callback capturing a weak reference to the agent.
  2. Close race — The peer connection / ICE agent is closed (state -> AgentState::Closed) after the callback is scheduled but before it runs.
  3. Use-after-close — The callback’s weakThis check passes (object still alive), so pre-fix it proceeds into findStreamAndApply on a closed agent’s torn-down state, crashing flakily in webrtc/datachannel/multiple-connections.html (webkit.org/b/315571).
  4. Fix — webkitGstWebRTCIceAgentIsClosed() is added and checked right after the liveness check, so a post-close callback returns early (canonical 314018@main).

Exploitation

  1. Establish + configure — Create GStreamer-backed RTCPeerConnections with data channels; configuration schedules async incoming-data callbacks holding weak references to the ICE agents.
  2. Close mid-flight — Close a connection so its ICE agent reaches AgentState::Closed while a scheduled incoming-data callback is still pending on the run loop.
  3. Use-after-close — The callback runs, passes the liveness-only weak check, and operates on the closed agent’s stream state – a race crash. Escalation beyond a flaky crash is not demonstrated and would require controlling the torn-down state. INFERRED.

Detection & hunting

For defenders and SOC / detection engineers:

  • Flaky crashes in datachannel multiple-connections
  • Callbacks after AgentState::Closed

Audit directions

  • Other async ICE-agent callbacks
  • Liveness-only weak checks
  • Close ordering vs pending run-loop work

Before / after

Loading diff…