4d4fbca02b Crash can occur when a video is playing in fullscreen and a second video gets fullscreen'ed in visionOS
Triage note: Converts unsafe raw self and inWindow captures to retained pointers, fixing a lifetime crash when a second video enters fullscreen.
Contents
The bug at a glance
This is a use-after-free in the UIProcess-side visionOS/iOS fullscreen controller reached through an ordinary, un-privileged UI flow (a second video entering fullscreen while a first is still animating), so it is memory corruption in a privileged process rather than a sandboxed renderer bug. However, the trigger is a timing race in the animation lifecycle rather than a directly attacker-shaped heap grooming primitive, and the observed outcome is a crash in objc_msgSend, so medium severity is appropriate absent evidence of controlled reuse.
The C++ lambdas driving the spatial fullscreen transition outlived the controller whose ivars they messaged, because they captured self, inWindow, outWindow and originalState as raw Objective-C pointers. The fix is a capture-mode change: retain the objects for the duration of the in-flight animation chain, exactly as the resizeCompletionBlock immediately above already did.
Root cause
In -[WKFullScreenWindowController _performSpatialFullScreenTransition:completionHandler:] the transition is expressed as a chain of makeBlockPtr-wrapped lambdas: animationBlock schedules a sequence of [UIView animateWithDuration:…] steps, whose final completion invokes animationCompletionBlock, which in turn invokes resizeCompletionBlock. These blocks are handed to UIKit and executed asynchronously across multiple animation frames. Before the patch, animationBlock captured strongSelf’s predecessor as a bare self plus a redundant weakSelf, and captured inWindow, outWindow, and originalState as raw Objective-C pointers; animationCompletionBlock captured inWindow raw as well.
Raw captures place no ownership on the referenced objects. The controller’s owning ivars (the windows, the saved originalState, and the controller itself via its parent references) can be released while the animation is still in flight, for example when a second video is fullscreened and tears down or replaces the first controller’s state. When a later animation step then dereferences one of those dangling captures, e.g. [self _updateFullscreenWindowOrigin], [outWindow setAlpha:0], or iterating [originalState ornamentProperties], the message send lands on a freed Objective-C object and UIKit crashes inside objc_msgSend.
The pre-existing weakSelf capture is telling: it shows the author was aware of the lifetime hazard for self but still dereferenced the raw self inside the block rather than the weak reference, so the guard was inert. The patch removes weakSelf entirely and switches every dangling capture to a retained one: strongSelf = retainPtr(self), and inWindow/outWindow/originalState = retainPtr(…). Retaining keeps each object alive until the block chain (and therefore the last strongSelf/window reference) is destroyed, closing the window between free and use.
The secondary edits in the hunk, rewriting dot-syntax property access such as outWindow.alpha and originalState.ornamentProperties into explicit [outWindow setAlpha:0] / [originalState ornamentProperties] message sends, are a mechanical consequence of the captures now being RetainPtr<> smart pointers rather than raw pointers: dot-property syntax does not apply to the RetainPtr wrapper, so .get()-equivalent message sends are used instead. They are not themselves security-relevant.
Key code
animationBlock now retains self and the windows for the animation’s asynchronous lifetime
auto animationBlock = makeBlockPtr([inWindow = retainPtr(inWindow), outWindow = retainPtr(outWindow), originalState = retainPtr(originalState), enter, allowSceneGeometryUpdates, shouldAnimateResizeScene, strongSelf = retainPtr(self), animationCompletionBlock = WTF::move(animationCompletionBlock)] mutable {
if (shouldAnimateResizeScene && !enter)
[strongSelf _updateFullscreenWindowOrigin];
Patch walkthrough
Source/WebKit/UIProcess/ios/fullscreen/WKFullScreenWindowControllerIOS.mm— Removes the inertWeakObjCPtr<WKFullScreenWindowController> weakSelf { self }guard. In animationCompletionBlock, changes the rawinWindowcapture toinWindow = retainPtr(inWindow). In animationBlock, changesinWindow,outWindow,originalStateraw captures toretainPtr(...)and replaces the rawself(plus moved weakSelf) withstrongSelf = retainPtr(self), updating every use inside the block to messagestrongSelf/the retained windows. This makes the block chain hold strong references for its entire asynchronous lifetime, matching the resizeCompletionBlock pattern just above.
Background
WKFullScreenWindowController (iOS/visionOS) — The UIProcess object that owns the element-fullscreen presentation on iOS-family platforms. On visionOS it performs a ‘spatial’ transition that animates windows in 3D (depth displacement of ornaments, transform3D translation). It holds the incoming and outgoing UIWindows and a saved originalState describing pre-transition geometry, all of which the transition blocks reference.
Objective-C block captures and lifetime — An Objective-C block copied to the heap normally retains the Objective-C objects it captures automatically. C++ lambdas wrapped by WebKit’s makeBlockPtr do NOT get that automatic ARC retain for raw pointer captures; a raw ObjC pointer captured in a C++ lambda is a non-owning reference. RetainPtr<> is WTF’s smart pointer that issues an explicit retain/release, which is why converting the captures fixes the lifetime.
makeBlockPtr / RetainPtr<> — makeBlockPtr adapts a C++ lambda into a reference-counted Objective-C block (BlockPtr). RetainPtr<T> is WTF’s RAII wrapper over CoreFoundation/Objective-C retain counts. Capturing an object as retainPtr(obj) inside the lambda ensures the object survives as long as any copy of the block survives.
Asynchronous UIView animation chaining — [UIView animateWithDuration:…completion:] runs the completion after the animation finishes, potentially many frames later. Chaining animationBlock -> animationCompletionBlock -> resizeCompletionBlock stretches the effective lifetime requirement of every captured object across the whole multi-second transition, during which unrelated events (a second fullscreen request) can free the controller’s state.
Vulnerability window
- Trigger — A video is playing in element-fullscreen on visionOS and the spatial transition animation chain is scheduled with raw captures of self and the windows.
- Interruption — A second video is fullscreened while the first transition is still in flight, causing the first controller’s owning ivars (windows/originalState, and controller references) to be released.
- Dangling use — A later scheduled animation step dereferences a now-freed capture, e.g. [self _updateFullscreenWindowOrigin] or [outWindow setAlpha:0] or [originalState ornamentProperties].
- Crash — UIKit sends a message to the freed object and faults in objc_msgSend; observed as a UIProcess crash (bug 317218 / rdar://171215079).
- Fix — Captures switched to RetainPtr<>; the block chain holds strong references until it completes, eliminating the free-before-use window.
Triggering
No test was added in the patch. Per the commit message the crash is reproduced on visionOS by playing a video in element fullscreen and then fullscreening a second video before the first spatial transition animation completes, causing the in-flight animation blocks to message the released controller/windows. This is a lifetime race, not a deterministic scripted primitive.
Exploitation
- Trigger reliability — Requires winning a timing race between the animation completion chain and the release of the first controller’s state; the crossing of two fullscreen sessions is script-reachable (two <video> elements calling requestFullscreen), but landing the free precisely inside an in-flight animation is non-deterministic.
- Reuse control — Crash-only as observed. Turning the UAF into a controlled primitive would require grooming the freed UIWindow/controller allocation to be replaced by attacker-shaped data before the dangling message send, which the patch and bug give no evidence of. objc_msgSend on a controlled isa could in principle be steered, but this is inferred, not demonstrated.
- Process context — The bug is in the UIProcess, not the sandboxed WebContent process, so a successful exploit would corrupt a higher-privilege process; however reaching it still depends on the platform-specific spatial transition path (visionOS).
Detection & hunting
For defenders and SOC / detection engineers:
- objc_msgSend crashes in fullscreen teardown — Crash reports faulting in objc_msgSend with WKFullScreenWindowController animation blocks (animateWithDuration completion frames) on the stack, especially on visionOS, are the signature of this dangling-capture bug.
- Overlapping fullscreen sessions — Telemetry showing a second element-fullscreen request arriving before a prior spatial transition completed correlates with the trigger condition.
Audit directions
- Other blocks in WKFullScreenWindowControllerIOS — Audit every makeBlockPtr/animateWithDuration completion in this file for raw
self/window/state captures; the resizeCompletionBlock was already correct but other transition helpers may share the original defect. - Inert weak-then-strong-deref pattern — Grep the codebase for lambdas that capture both
weakSelfand rawself(or capture weakSelf but dereferenceself), which is the anti-pattern that made the guard here useless. - C++ lambda ObjC captures via makeBlockPtr — Systematically review makeBlockPtr call sites for raw Objective-C pointer captures that are dereferenced asynchronously; unlike ARC blocks these get no automatic retain and are a recurring UAF source.
- Fullscreen controller lifetime on visionOS — Examine how a second requestFullscreen tears down or replaces an in-flight controller’s ivars, to confirm no other consumers assume those objects remain live through the animation.