← All issues

[3] WebSpeechSynthesisWrapper missing observer deregistration

Severity: Low | Component: WebCore platform speech synthesis (Cocoa) | 1b953e1

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

#if HAVE(AVSPEECHSYNTHESIS_VOICES_CHANGE_NOTIFICATION)
+
+- (void)dealloc
+{
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:RetainPtr { AVSpeechSynthesisAvailableVoicesDidChangeNotification }.get() object:nil];
+ [super dealloc];
+}
+
- (void)availableVoicesDidChange
{
// AVFoundation may post AVSpeechSynthesisAvailableVoicesDidChangeNotification from a
...
// -initWithSpeechSynthesizer: (unchanged, shown for symmetry)
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(availableVoicesDidChange) name:RetainPtr { AVSpeechSynthesisAvailableVoicesDidChangeNotification }.get() object:nil];

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.

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.

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.