7b904b1187 Crash in -[WKDateTimePicker removeDatePickerPresentation] via SetForScope destructor writing to self
Triage note: Wraps the picker/control in protect() so a reentrant dismissal freeing self does not leave the SetForScope destructor writing to freed memory.
Contents
The bug at a glance
A UI-process use-after-free on iOS: a DidCommitLoadForFrame IPC during navigation dismisses the date/time picker, and dismissViewControllerAnimated: can synchronously free the WKDateTimePicker; the SetForScope<bool> destructor then writes _isDismissingDatePicker = NO into the freed object. It is a genuine memory-safety fix in the trusted UIProcess, but reachability requires a specific reentrant deallocation the authors could not reproduce deterministically (the commit says no test, verified only via method swizzling), and the write is a single fixed byte to a freed slot, so medium rather than high.
SetForScope saves a reference to a member and restores it in its destructor at scope exit. If the object owning that member is deallocated inside the scope – during the dismiss/edit callback – the destructor’s restore store lands in freed memory. Wrapping the receiver in protect() (a scoped RetainPtr) keeps self alive until the callback returns, so the SetForScope restore writes into a still-live object.
Root cause
-[WKDateTimePicker removeDatePickerPresentation] guards re-entrant dismissal with a stack SetForScope<bool>: SetForScope isDismissingDatePicker { _isDismissingDatePicker, YES };. SetForScope captures a reference to the _isDismissingDatePicker member (i.e. a pointer into the WKDateTimePicker instance), sets it to YES for the duration of the scope, and its destructor restores the previous value (NO) when the block exits.
Inside that scope the code calls [_datePickerController dismissViewControllerAnimated:NO completion:nil]. During navigation-driven dismissal (a DidCommitLoadForFrame IPC arrives, tearing down the form peripheral), this dismissal can, by a mechanism the authors could not pin down, cause the WKDateTimePicker itself to be released and deallocated before the method returns. The receiver self is gone, but the SetForScope object still lives on the stack and still holds a reference to the now-freed _isDismissingDatePicker byte.
When removeDatePickerPresentation returns, the SetForScope<bool> destructor runs and writes the restored value NO through that reference – a store into freed heap memory (use-after-free write). Because the target is a fixed offset inside a freed WKDateTimePicker, it corrupts whatever now occupies that slot.
The fix is to retain the receiver across the dangerous call: [protect(_datePickerController) dismissViewControllerAnimated:NO completion:nil] – but more importantly the pattern applied throughout keeps the objects that can be freed reentrantly alive for the duration of the outbound call. protect() returns a scoped RetainPtr that holds a +1 reference until end of the full-expression/scope, so the WKDateTimePicker (and, in WKFormPeripheralBase, the _control) cannot be deallocated while the callback runs, guaranteeing the SetForScope restore store hits live memory. The same protect() hardening is applied to -[WKFormPeripheralBase beginEditing]/endEditing around controlBeginEditing/controlEndEditing (endEditing is called the actual crash fix; the others are drive-by), and per the message to the __weak _delegate in WKDatePickerPopoverController, though those popover changes are not present in the provided diff.
Key code
Retain the receiver across the reentrant dismiss so the SetForScope restore does not write to freed self (WKDateTimeInputControl.mm)
if (_datePickerController) {
if (!_isDismissingDatePicker) {
SetForScope isDismissingDatePicker { _isDismissingDatePicker, YES };
[protect(_datePickerController) dismissViewControllerAnimated:NO completion:nil];
}
_datePickerController = nil;
Patch walkthrough
Source/WebKit/UIProcess/ios/forms/WKDateTimeInputControl.mm— In -[WKDateTimePicker removeDatePickerPresentation], the dismiss call is changed from[_datePickerController dismissViewControllerAnimated:NO completion:nil]to[protect(_datePickerController) ...]. protect() holds a scoped reference to the date-picker controller across the dismissal; combined with the surrounding SetForScope<bool> isDismissingDatePicker, this keeps the relevant objects alive so the SetForScope destructor’s restore store does not hit freed memory.Source/WebKit/UIProcess/ios/forms/WKFormPeripheralBase.mm— -[WKFormPeripheralBase beginEditing] now calls[protect(_control) controlBeginEditing]and -endEditing now calls[protect(_control) controlEndEditing]. The endEditing change is the crash fix (protects _control across a callback that can free it); beginEditing is a matching drive-by. Both keep _control alive across the reentrant control callback.
Background
SetForScope<bool> — A WTF RAII helper that stores a reference to a variable, assigns it a new value for the scope, and restores the original value in its destructor at scope exit. If the referenced storage is freed before the destructor runs, the restore becomes a write to freed memory.
protect() / RetainPtr — protect() wraps an Objective-C object in a scoped RetainPtr that adds a +1 reference for the duration of the expression/scope. Sending a message through protect(obj) guarantees obj outlives the call even if the original owning reference is dropped reentrantly.
WKDateTimePicker / _datePickerController — The iOS UIProcess form peripheral that presents the date/time UIDatePicker via a _datePickerController (UIViewController). Dismissing it can run completion/teardown work synchronously that touches the peripheral’s owner.
DidCommitLoadForFrame-driven dismissal — When a navigation commits, the focused form peripheral is torn down; this is the path that invokes removeDatePickerPresentation and can, reentrantly, release the WKDateTimePicker mid-dismiss.
WKFormPeripheralBase controlBeginEditing/controlEndEditing — Base-class hooks that forward editing lifecycle to the concrete _control (an id<WKFormControl>). controlEndEditing can trigger dismissal/deallocation, so calling it through an unretained _control is unsafe.
Vulnerability window
- Focus — User focuses a date/time input on iOS; WKDateTimePicker presents _datePickerController and the form peripheral is active.
- Navigation — A DidCommitLoadForFrame IPC arrives (navigation commits), triggering teardown of the focused peripheral and a call to removeDatePickerPresentation.
- Scope entered — SetForScope isDismissingDatePicker captures &_isDismissingDatePicker and sets it YES, then dismissViewControllerAnimated:NO is invoked.
- Reentrant free — During the dismiss, WKDateTimePicker is released and deallocated by an unknown mechanism; self is freed while the SetForScope object still references the freed member.
- UAF write — removeDatePickerPresentation returns; the SetForScope<bool> destructor restores NO through the dangling reference, writing into freed memory.
- Fix — protect(_datePickerController) (and protect(_control) in WKFormPeripheralBase) retains the objects across the call so nothing is freed before the SetForScope destructor runs.
Triggering
No test was added; the commit states the exact conditions to reproduce are unknown and it was verified locally by swizzling an arbitrary method to force reentrant deallocation. Conceptual trigger: focus a date/time input on iOS to present WKDateTimePicker, then cause a navigation that commits (DidCommitLoadForFrame) while the picker is dismissing, such that the WKDateTimePicker is released synchronously inside dismissViewControllerAnimated:; the SetForScope<bool> destructor then writes _isDismissingDatePicker = NO into the freed object.
Exploitation
- Trigger — Drive a navigation that commits while a date/time picker is being dismissed, so removeDatePickerPresentation runs its SetForScope scope during teardown.
- Reentrant free — Arrange for the dismiss to drop the last reference to WKDateTimePicker (the mechanism is UIKit-internal/unknown), freeing self mid-scope.
- Reclaim — Reoccupy the freed WKDateTimePicker allocation so the fixed-offset _isDismissingDatePicker byte overlaps an attacker-meaningful field before the destructor runs.
- Write — The SetForScope destructor writes a single byte (NO/0) at that offset into freed memory; this is a constrained one-byte poke rather than a general write, so exploitation would hinge on that byte’s placement in a reclaimed object.
Detection & hunting
For defenders and SOC / detection engineers:
- UAF write from a SetForScope destructor in WKDateTimePicker teardown — Crash reports on iOS UIProcess showing removeDatePickerPresentation on the stack with a store into a freed WKDateTimePicker after dismissViewControllerAnimated: are the signature.
- Navigation-during-picker-dismissal — Correlate crashes with DidCommitLoadForFrame occurring while a form peripheral (date/time picker) is focused/dismissing.
- Reentrant dealloc of form peripherals — Instrument WKFormPeripheralBase/_control and WKDateTimePicker/_datePickerController lifetimes; a deallocation observed inside controlEndEditing/dismiss callbacks indicates the hazardous reentrancy.
Audit directions
- SetForScope over member storage across callbacks — Grep for SetForScope (and similar RAII restore helpers) whose target is an instance member and whose scope contains a call that can synchronously deallocate the instance; each is a candidate write-to-freed-self bug that protect(self)/protectedThis fixes.
- iOS UIProcess form peripherals — Audit WKFormInputControl, WKFormColorControl, WKFormSelectControl and other WKFormPeripheralBase subclasses for unprotected message sends to _control/_delegate/controllers across begin/end editing and dismissal paths.
- __weak delegate dereferences — Per the commit, WKDatePickerPopoverController’s __weak _delegate calls were also protected (not shown in this diff); review other __weak delegate call sites that invoke non-trivial methods without first taking a strong local.
- UIViewController dismiss reentrancy — Review all UIProcess call sites of dismissViewControllerAnimated:completion: that run inside a scope holding references to self’s members, since UIKit dismissal can synchronously run teardown that frees the caller.