[3] WebSpeechSynthesisWrapper missing observer deregistration
Low, but only because modern Foundation stores the observer weakly and nils it on dealloc. On pre-zeroing-weak Cocoa this exact asymmetry would be a textbook dangling-observer UAF; the notification is also system-driven rather than web-summonable, so what remains is a robustness gap and a pattern-audit lesson.
Observer-lifecycle bugs happen when an object subscribes to a notification hub at construction but never unsubscribes before it is freed, leaving a registration that can outlive the subscriber. Here the subscriber is WebSpeechSynthesisWrapper, an Objective-C bridge between WebCore's speech API and AVFoundation's speech synthesizer, which registers with NSNotificationCenter — Cocoa's global publish/subscribe hub — to hear about voice-inventory changes. The lifecycle contract is symmetric: an addObserver: in the initializer must be matched by a removeObserver: at teardown so no notification can ever be dispatched to a deallocated observer.
The angle: for defensive value only — modern Foundation already stores this observer weakly and auto-nils it, so no use-after-free is reachable; the patch closes a robustness gap during voice-notification teardown, and the missing -dealloc unregistration is the pattern worth auditing elsewhere.
This is a merge-back of the safari-7624-branch fix. The -availableVoicesDidChange main-thread hop and null-check that the original fix added already landed independently on main in 312522@main via ensureOnMainThread; this merge-back applies only the remaining -dealloc change. WebSpeechSynthesisWrapper registers as an observer for AVSpeechSynthesisAvailableVoicesDidChangeNotification but never removed itself. The fix adds -dealloc to call -removeObserver:name:object:. While Foundation stores such observers as zeroing weak references that auto-invalidate when the observer is deallocated, removing the registration promptly is best practice.
Source/WebCore/platform/cocoa/PlatformSpeechSynthesizerCocoa.mm
Patch Details
The patch adds a -dealloc method to WebSpeechSynthesisWrapper. The new method calls [[NSNotificationCenter defaultCenter] removeObserver:self name:AVSpeechSynthesisAvailableVoicesDidChangeNotification object:nil] before chaining to [super dealloc]. This is the counterpart to the addObserver:selector:@selector(availableVoicesDidChange) name:... registration in -initWithSpeechSynthesizer:, which previously had no matching deregistration. The method is guarded by #if HAVE(AVSPEECHSYNTHESIS_VOICES_CHANGE_NOTIFICATION), matching the guard on the registration. The existing -availableVoicesDidChange main-thread hop and null-check (landed separately in 312522@main) are preserved.
Asymmetric subscribe/unsubscribe pair — observer registered at init with no matching deregistration at deallocation.
Background
NSNotificationCenter.
NSNotificationCenter is Foundation's publish/subscribe hub — a global message bus for loosely-coupled observers. Objects register via addObserver:selector:name:object: to receive named notifications and are expected to deregister via removeObserver: before they are freed. Its purpose is decoupling notification producers from consumers so they need no direct pointer references.
Zeroing weak references.
For observers registered via the selector-based API, modern Foundation stores the observer as a zeroing weak reference: unlike a strong reference (which would keep the object alive) or an unsafe_unretained raw pointer (which would dangle), a zeroing weak reference is automatically nil-ed when the observer deallocates. "Zeroing" is the property that matters here — it means a later post does not message a freed pointer.
-dealloc and the wrapper.
-dealloc is the Objective-C teardown point where observer deregistration conventionally occurs, chaining to [super dealloc]. WebSpeechSynthesisWrapper is an NSObject conforming to AVSpeechSynthesizerDelegate that bridges WebCore's PlatformSpeechSynthesizer to AVFoundation and observes voice-change notifications so it can call PlatformSpeechSynthesizer::voicesDidChange() on the main thread. AVSpeechSynthesisAvailableVoicesDidChangeNotification is an AVFoundation notification posted (possibly from a background thread) when the set of installed system voices changes.
Analysis
The root cause is an observer-lifecycle asymmetry: WebSpeechSynthesisWrapper registered itself in its initializer but provided no -dealloc to deregister. The registration in -initWithSpeechSynthesizer: had no matching teardown, so the notification-center record outlived the intent of the subscription.
Pre-mitigation Cocoa Modern Foundation (this platform)
(non-zeroing storage) (zeroing-weak storage)
────────────────────── ─────────────────────────────────
init: addObserver (raw ptr) init: addObserver (weak ptr)
... ...
dealloc (no removeObserver) dealloc (no removeObserver)
observer slot -> freed ptr observer slot -> auto-nil'd
notification posts notification posts
dispatch to freed obj UAF dispatch skipped (no-op)
The classic exploitation direction for a missing observer removal is to allocate the wrapper, let it register, deallocate it, then trigger the notification so the center dispatches availableVoicesDidChange to the freed observer; if the freed slot had been reclaimed with attacker-controlled data, the selector dispatch could reach a controlled object. Two facts collapse that direction here: the selector-based registration is held as a zeroing weak reference, so the pointer is auto-invalidated at deallocation and no freed-object dispatch occurs; and the notification is AVFoundation-driven by a system voice-inventory change, not summonable from web content on demand. No read/write or control-flow primitive is reachable, and even the hypothetical unmitigated case would still require a separate sandbox escape from the speech-hosting process.
This vulnerability weakens the memory-safety robustness of the speech-synthesis observer lifecycle, though the practical trust-boundary impact is minimal: the invariant that an observer deregisters before deallocation was upheld only by Foundation's zeroing-weak behavior, not by WebKit's own code. The fix restores registration/deregistration symmetry so correctness no longer rests on a framework detail.
The reusable lesson is the role of framework mitigation as an implicit safety net — the same source-level bug that would be a UAF against non-zeroing observer storage is merely a cleanliness issue against modern NSNotificationCenter. Crucially, that net is API-specific: registrations made via addObserverForName:object:queue:usingBlock: are NOT zeroing-weak and DO dangle, so the identical asymmetry in a block-based registration is a genuine bug.
Audit directions
- Asymmetric subscribe/unsubscribe pairs where safety depends on a framework mitigation rather than explicit symmetry. Grep
Source/WebCore/platform/cocoaandSource/WebCore/platform/audio/cocoaforaddObserver:/addObserverForName:without a matchingremoveObserver:in-dealloc, starting with AVFoundation/media delegate wrappers like the speech and audio-session bridges. The review tell is a class whose-initcallsaddObserver...but whose@implementationhas no-dealloc(or a-deallocthat skips deregistration). - Registrations that do not get zeroing-weak protection. Verify whether each notification registration uses the zeroing-weak selector API or the non-zeroing block API. The same lifecycle asymmetry through KVO (
addObserver:forKeyPath:),CFNotificationCenter,DispatchSource, or block-basedaddObserverForName:object:queue:usingBlock:does NOT get Foundation's zeroing-weak protection, so the identical source shape there is a real dangling-observer/UAF. Search WebCore/PAL for block-based observer registrations and confirm each captures the object weakly and is torn down. The tell is a block-based registration whose capturedself/token is never passed toremoveObserver:. - Subscription outlives subscriber — a lifecycle contract violation independent of the memory-safety outcome. This appears wherever a subscribe/unsubscribe pair is split across init/teardown: JavaScript DOM
addEventListener/removeEventListener, Node.jsEventEmitter, Rusttokio::broadcast, DI containers with lifecycle scopes. The reusable invariant: if an initializer registers with a framework-owned collection, the destructor must deregister — even when the framework claims to auto-handle it, because that guarantee varies by registration API and edge-case teardown races usually leak through. The tell is any type whose constructor subscribes but whose destructor does not unsubscribe.