← All issues

[7] GameControllerGamepad UAF from raw this capture in framework-retained blocks

Severity: Medium | Component: WebCore Gamepad | 101c855

Rated Medium because the observable effect is a confirmed use-after-free crash on controller disconnect (the test case reproduces it), but exploitation requires physical or virtual HID device access — not triggerable from web content alone — and the race window is narrow, limiting reliable exploitation despite the UI-process context.

Replaces raw this captures in Objective-C callback blocks with WeakPtr plus null checks. Adds a destructor that calls teardownElements() to unregister all GameController framework handlers when the object is destroyed.

Source/WebCore/platform/gamepad/cocoa/GameControllerGamepad.mm

void GameControllerGamepad::setupElements()
{
RetainPtr<GCPhysicalInputProfile> profile = m_gcController.get().physicalInputProfile;
+ WeakPtr weakThis { *this };
 
if ([profile respondsToSelector:@selector(setThumbstickUserIntentHandler:)]) {
[profile setThumbstickUserIntentHandler:^(__kindof GCPhysicalInputProfile*, GCControllerElement*) {
- m_lastUpdateTime = MonotonicTime::now();
- GameControllerGamepadProvider::singleton().gamepadHadInput(*this, true);
+ if (!weakThis)
+ return;
+ weakThis->m_lastUpdateTime = MonotonicTime::now();
+ GameControllerGamepadProvider::singleton().gamepadHadInput(*weakThis, true);
}];
}
+GameControllerGamepad::~GameControllerGamepad()
+{
+ teardownElements();
+}
+void GameControllerGamepad::teardownElements()
+{
+ auto profile = RetainPtr { m_gcController.get().physicalInputProfile };
+ if (!profile)
+ return;
+
+ if ([profile respondsToSelector:@selector(setThumbstickUserIntentHandler:)])
+ [profile setThumbstickUserIntentHandler:nil];
+
+ for (GCControllerButtonInput *button in [profile allButtons])
+ button.valueChangedHandler = nil;
+
+ profile.get().dpads[GCInputLeftThumbstick].xAxis.valueChangedHandler = nil;
+ profile.get().dpads[GCInputLeftThumbstick].yAxis.valueChangedHandler = nil;
+ profile.get().dpads[GCInputRightThumbstick].xAxis.valueChangedHandler = nil;
+ profile.get().dpads[GCInputRightThumbstick].yAxis.valueChangedHandler = nil;
+}

Tools/TestWebKitAPI/Tests/mac/HIDGamepads.mm

+TEST(Gamepad, DisconnectDuringInput)
+{
+ // Tests that handler blocks don't crash when invoked after GameControllerGamepad destruction.
+ ...
+ for (int i = 0; i < 100; ++i) {
+ gamepad->setAxisValue(0, (float)i / 100.0);
+ gamepad->setAxisValue(1, (float)(100 - i) / 100.0);
+ gamepad->publishReport();
+ }
+
+ // This destroys VirtualGamepad, which triggers controllerDidDisconnect,
+ // which destroys GameControllerGamepad.
+ gamepad = nullptr;
+
+ // Wait for disconnect message.
+ Util::run(&didReceiveMessage);
+}

The patch makes three changes: (1) setupElements() captures a WeakPtr weakThis { *this } and all callback blocks now check if (!weakThis) return; before accessing member variables. (2) A new ~GameControllerGamepad() destructor calls teardownElements(). (3) A new teardownElements() method sets all registered handler blocks to nil on the GCPhysicalInputProfile, preventing the framework from firing callbacks after destruction. The fix applies a belt-and-suspenders approach — both WeakPtr null checks (defense in depth) and handler deregistration (primary fix). Notably, the axis handler blocks still pass *this instead of *weakThis to gamepadHadInput() — an apparent oversight, though teardownElements() mitigates this by clearing handlers on destruction.

The Gamepad API exposes physical game controllers to web content. On Apple platforms, WebKit uses the GameController framework (GCController, GCPhysicalInputProfile) to receive input events. Input handlers are registered as Objective-C blocks on controller elements (buttons, axes, thumbsticks). These blocks are retained by the framework and invoked asynchronously when the hardware reports new input — their lifetime is controlled by the Objective-C runtime, not by the C++ object that registered them.

WeakPtr is a weak reference that automatically nullifies when the pointee is destroyed, allowing safe liveness checks in retained callbacks. Without explicit cleanup, Objective-C blocks captured by framework objects will persist indefinitely, holding references to whatever they closed over.

Use-after-free from raw this capture in framework-retained callback blocks that outlive the C++ object.

Before the fix, GameControllerGamepad::setupElements() registered Objective-C block callbacks with the GameController framework that captured the raw this pointer. These blocks were retained by the GCController's input profile and could fire asynchronously at any time when the physical controller produced input. When a GameControllerGamepad object was destroyed (e.g., on controller disconnect), no destructor existed to unregister these handlers. The GameController framework continued to hold the blocks, and when a queued or new input event fired the callback, the block dereferenced the now-freed this pointer, writing to m_axisValues, m_buttonValues, and m_lastUpdateTime, and passing a dangling reference to gamepadHadInput().

🔒

Explores the heap exploitation potential of the dangling-pointer writes and the process-level implications of where this code executes

Subscribe to read more

🔒

Multiple audit patterns identified for framework callback lifetime issues across WebKit's platform integration layers, with concrete search targets

Subscribe to read more