[2] UIProcess keyEventQueue dangling reference during key-event interpretation
Medium. A freed or relocated event inside the UIProcess is memory-safety territory. The regression test reaches the re-entry only by swizzling a private UIKit-side method, and no web-content trigger has been established. Going beyond a crash would also need control of the freed Deque buffer.
Event queues that hand out references to their elements are only safe if nothing mutates the queue while the reference is live. On iOS, WebPageProxy (the UIProcess object representing one web page) queues hardware key presses in keyEventQueue, a Deque of NativeWebKeyboardEvent (a wrapper around the platform key event). When the WebContent process needs the UI side to interpret a key, it drives UIKit text-input handling synchronously on the front event. The expectation is that the front event stays valid and stationary until that call returns.
The angle: code that re-enters the web view during keyboard handling can free or relocate the key event the UI process is still interpreting, which crashes the host app and would need buffer-reuse control to become anything more.
Copy the front key event before interpreting it in
WebPageProxy::interpretKeyEvent. The function passed a reference to the front element ofkeyEventQueue, aDequestoringNativeWebKeyboardEvents by value, into a nested synchronous interpretation call. That call runs UIKit text-input handling, which can re-enterWebPageProxy:keyEventHandlingCompleted()removes the front element viatakeFirst(), andhandleKeyboardEvent()appends, which can reallocate the backing store. UPDATE: onmain, 320030@main (Bug 322699) already fixed this differently.keyEventQueuenow holdsRef<NativeWebKeyboardEvent>,protect(firstQueuedKeyEvent())keeps the event alive across the nested call, andNativeWebKeyboardEventcan no longer be copied. Only the regression test is merged back.
Tools/TestWebKitAPI/Tests/WebKit/WKWebView/ios/KeyboardInputTestsIOS.mm
Patch Details
This commit is a backport that lands only the regression test; the production fix is already on main. The diff touches a single file. It forward-declares the private WKKeyboardScrollViewAnimator class and its -beginWithEvent:scrollView: method, then adds ReentrantKeyEventDuringInterpretKeyEventDoesNotCrash. The test focuses an autofocused input and swizzles -[WKKeyboardScrollViewAnimator beginWithEvent:scrollView:], which runs inside the nested interpretation. On its first invocation the swizzled block calls -[WKWebView handleKeyEvent:completion:] 32 times, each appending an event to keyEventQueue while the original front event is still being interpreted. The test asserts didReenter to prove the nested path was reached and relies on ASan to flag any use-after-free.
The two fixes differ. According to the commit message, the original safari-7625 fix (316606.241) copied the front NativeWebKeyboardEvent onto the stack before the call. The main fix converts the queue to Ref<NativeWebKeyboardEvent> and protects the front element.
Reference into a growable by-value container held across a synchronous re-entrant callback that can mutate the container.
Background
Where this lives. This is the UIProcess half of WebKit's iOS keyboard pipeline, which runs in the host application itself. It is not in the sandboxed WebContent process.
Key event flow.
Hardware key presses arrive at -[WKWebView handleKeyEvent:completion:], which calls WebPageProxy::handleKeyboardEvent(). That appends the event to keyEventQueue and sends it to the WebContent process. When the WebContent process needs the UI side to interpret the key (text input, editing commands, keyboard scrolling), WebPageProxy::interpretKeyEvent() drives a synchronous call into WKContentView/UIKit text-input handling. Keyboard scrolling goes through WKKeyboardScrollViewAnimator. When the WebContent process reports completion, keyEventHandlingCompleted() removes the front event (via takeFirst(), per the commit message) and invokes its completion.
Re-entrancy.
UIKit text-input handling is free to call back into WKWebView and WebPageProxy synchronously, for example by dispatching further key events or completing pending ones, before the outer call returns.
WTF::Deque<T> and NativeWebKeyboardEvent.
Deque is a ring-buffer container that stores elements inline in one heap backing store. Appending past capacity allocates a larger buffer, moves the elements and frees the old one, and takeFirst() moves the front element out and destroys its slot. NativeWebKeyboardEvent wraps a platform keyboard event; on iOS it holds a RetainPtr<::WebEvent> (m_nativeEvent) beside the cross-platform WebKeyboardEvent fields.
Ref and protect().
Ref<T> is WebKit's non-null, reference-counted smart pointer. protect(x) creates a temporary strong reference that keeps a refcounted object alive for a scope.
Analysis
This is a use-after-free through a dangling container-element reference. Per the commit message, interpretKeyEvent() passed a reference to the front element of keyEventQueue into the nested interpretation call, and on the affected branch the queue stores NativeWebKeyboardEvent by value. The pre-fix body of interpretKeyEvent and the keyEventQueue declaration sit in the truncated part of the supplied source, so the by-value storage is relayed from the commit message. The NativeWebKeyboardEvent.h supplied reflects the main design: every platform constructor is a static Ref<NativeWebKeyboardEvent> create(...) and the iOS variant holds const RetainPtr<::WebEvent> m_nativeEvent.
interpretKeyEvent() keyEventQueue (Deque, by value)
─────────────────── ───────────────────────────────
ref = &queue.first() ───────────────────► [E0][ ][ ]... buffer B1
call UIKit interpretation
└─► beginWithEvent:scrollView: (re-entry)
handleKeyboardEvent() x N ───────► append past capacity: move to B2, free B1
keyEventHandlingCompleted() ─────► takeFirst(): E0 slot destroyed
interpretation still dereferences ref ───► ref -> freed B1 / dead slot <-- failure window
Either re-entrant path leaves the in-flight reference pointing at a destroyed object or a freed buffer while the interpretation frame is still on the stack.
The regression test exercises the append path:
- A key-down for
ais sent throughhandleKeyEvent:completion:to an autofocused input, andWebPageProxyenqueues it and drives interpretation. - Interpretation reaches the swizzled
-beginWithEvent:scrollView:. The test comment says this proves_interpretKeyEventwas reached synchronously. - On first entry the block sends 32 more key-downs, each appending to
keyEventQueuewhile the original event is still the one being interpreted. - 32 appends exceed the queue's capacity if it starts near
WTF::Deque's minimum capacity (about 16), forcing a backing-store reallocation. Whether they do depends on the queue's state at re-entry. - Under ASan, any later access to the stale event aborts the test, and
didReenterguards against a vacuous pass.
No web-content-reachable path into this re-entry has been established. The test drives it through a swizzled WKKeyboardScrollViewAnimator method. The immediate effect is a UIProcess crash, an app-level denial of service. Anything beyond that would require controlling the re-entrant key sequence and reclaiming the freed Deque buffer with attacker-influenced data.
This vulnerability weakens memory safety in the UIProcess, the most privileged WebKit process and the host application itself. The violated invariant is that an event object under interpretation stays alive and stationary for the whole interpretation window.
Insight: WebKit fixed this twice, and the difference is instructive. According to the commit message, the branch fix snapshots the event on the stack, following keyEventHandlingCompleted(), which copies keyEventQueue.first() before sendKeyEvent(). The main fix makes queue elements heap-refcounted, so reallocation can no longer move them and protect() survives a takeFirst(). The commit message also says NativeWebKeyboardEvent is no longer copyable, which would stop by-value queues from creeping back. The touch path already followed this model (QueuedTouchEvents holds Ref<NativeWebTouchEvent> in WebPageProxyInternals.h), so the keyboard queue was the laggard.
Audit directions
- References into reallocating containers across re-entrant calls. A reference to an element of a by-value container survives a call that can re-enter and mutate that container. The invariant is that no
T&into aDeque/Vector/HashMapvalue outlives a call that can reach code mutating the container. Narrow: grepSource/WebKit/UIProcessfor.first(),.last()andoperator[]results bound toauto&orconst T&on WebPageProxy queues (mouse, wheel, gesture), followed by a call into PageClient, UIKit/AppKit, a delegate or a synchronous IPC. A hit is such a reference used after the call without being re-fetched. Wider: range-for loops over a container whose body dispatches callbacks, and iterators held across observer notifications in WebCore (event listener vectors, observer lists). Widest: this is classic iterator/reference invalidation under re-entrancy, which Chromium'sbase::ObserverListexists to tolerate and which Rust's borrow checker forbids statically whileRefCellturns it into a runtime panic. - Platform callouts made while C++ holds unprotected state. Audit every Objective-C or Swift delegate, UIKit and AppKit callout from UIProcess C++, especially in
WKContentViewInteraction.mmtext-input and keyboard paths such as_interpretKeyEventandWKKeyboardScrollViewAnimator. Platform frameworks and host apps can call back intoWKWebViewpublic API synchronously. Narrow: list theWKContentViewmethods invoked fromWebPageProxythat themselves call publicWKWebViewentry points or input-delegate methods, then check whether theWebPageProxycaller holds raw references across them. A hit is a C++ frame that capturesthis-owned state before the callout and dereferences it afterward withoutprotect()or a re-check. Wider:WKUIDelegateandWKNavigationDelegatecallbacks invoked mid-operation, where the client can close, navigate or re-enter the page. Widest: any embedding framework calling host-supplied callbacks while holding internal state (ChromiumWebContentsDelegate, Electron, CEF). - Value-stored event and command objects. Check which containers in WebKit IPC/UIProcess code still store event or command objects by value after the move to
Ref<NativeWebKeyboardEvent>. Main's pattern is to make queued objects refcounted and non-copyable soprotect()is possible and accidental copies fail to compile. Narrow: inWebPageProxyInternals.handWebPageProxy.cpp, compare each event-queue element type against theRef<>-basedQueuedTouchEvents; anyDeque<SomeEvent>with value elements handed by reference into a synchronous path is a candidate. Wider: pending-callback maps (HashMap<ID, CompletionHandler>) whose handlers are invoked by reference while the handler can modify the map. Widest: an element handed out across an unbounded call should have identity-stable storage (heap node plus refcount, or an index re-resolved after the call), as in game-engine entity arrays and GUI event queues such as QtQEventLoopre-entrancy. The match tell is a stored value type whose address escapes into a call that can trigger reallocation.