77e479c3f25aab6c5260bb1d7edff95e2b15a01c [autofill] Remove SelfKeepAlive from WebFormElementObserverImpl
The bug at a glance
The commit message itself names the removed pattern as a source of Use-After-Free hazards with untraced raw_ptrs and memory-leak risk, but the change is defensive hardening of a latent lifetime anti-pattern crossing the Blink public API boundary; no concrete attacker-reachable trigger is shown in the diff.
The old design kept a GC object alive via SelfKeepAlive and exposed it to components/autofill as a raw_ptr that was manually freed through Disconnect(); if any path bypassed Disconnect() the object could leak or, worse, the untraced cross-boundary raw_ptr could outlive/precede its referent and become a dangling access during form tracking.
Root cause
WebFormElementObserverImpl was a GarbageCollected object that artificially rooted itself with SelfKeepAlive and was manually torn down when FormSubmissionTracker called Disconnect() on a raw_ptr. Oilpan persistent handles were being abused as a malloc/free mechanism across the public Blink boundary, so lifetime depended on every caller remembering to call Disconnect(); a missed Disconnect() leaked the object, and the untraced raw_ptr held by the C++ side had no Oilpan tracing to keep the pointer and referent consistent. The CL converts the implementation to a plain C++ object owned by std::unique_ptr, holding only a Persistent to the inner GC mutation observer, so destruction is governed by RAII and the destructor disconnects the inner callback.
Key code
unique_ptr ownership with RAII destructor replaces SelfKeepAlive + manual Disconnect()
~WebFormElementObserverImpl() {
if (mutation_callback_) {
mutation_callback_->Disconnect();
mutation_callback_ = nullptr;
}
}
// header: std::unique_ptr<blink::WebFormElementObserver> form_element_observer_;
Patch walkthrough
components/autofill/content/renderer/form_submission_tracker.h— Changes the member from raw_ptrblink::WebFormElementObserver to std::unique_ptrblink::WebFormElementObserver, giving the tracker clear ownership instead of an untraced raw pointer to a self-rooted GC object.components/autofill/content/renderer/form_submission_tracker.cc— ResetLastInteractedElements() now calls form_element_observer_.reset() (RAII teardown) instead of the manual Disconnect()+nullptr dance, removing the possibility of a bypassed Disconnect() leaking or dangling.third_party/blink/public/web/modules/autofill/web_form_element_observer.h— Public factory now returns std::unique_ptr and the interface exposes a virtual destructor instead of a manual Disconnect(); callers can no longer leave the object un-freed.third_party/blink/renderer/core/exported/web_form_element_observer_impl.cc— Create() uses std::make_unique instead of MakeGarbageCollected; the destructor disconnects the inner mutation callback, replacing the explicit Disconnect()/self_keep_alive_.Clear() logic.third_party/blink/renderer/core/exported/web_form_element_observer_impl.h— Drops GarbageCollected base, SelfKeepAlive member and Trace(); keeps only a Persistent, eliminating the self-rooting anti-pattern that made lifetime depend on an external Disconnect() call.
Background
Oilpan / SelfKeepAlive — SelfKeepAlive is an Oilpan persistent handle that lets a GarbageCollected object keep itself alive until explicitly cleared, which here was misused as a manual malloc/free across the public Blink API.
WebFormElementObserver — A Blink autofill helper that watches a form/control element for DOM mutations (e.g. removal) and invokes a callback, used by the renderer-side FormSubmissionTracker.
raw_ptr (MiraclePtr) — A checked raw pointer type; when it points at an Oilpan-managed object not covered by tracing, lifetime consistency is not guaranteed by the GC.
Vulnerability window
- Reachability — Reached by ordinary web content in a stock build via autofill form tracking (interacting with and mutating forms) in the renderer process.
- Trigger — A control path that fails to call Disconnect() before dropping the observer reference (inferred) would leak the self-rooted object, or an untraced raw_ptr access could occur against an inconsistently-lived object.
- Primitive — At most a dangling/stale access or memory leak on the Oilpan heap within the renderer (inferred); no concrete corruption primitive is demonstrated by the diff.
- Escalation — No escalation shown; this is a hardening refactor that closes a latent lifetime hazard (inferred).
Proof of concept
Conceptually, drive autofill form tracking (interact with a form so WebFormElementObserver is created) and reach a teardown path that previously relied on an external Disconnect() call; the pre-patch anti-pattern leaves lifetime dependent on that call rather than RAII. No reliable attacker-controlled corruption is demonstrated by the diff.
Exploitation
- Setup — Load a page with autofillable forms to cause FormSubmissionTracker to create a WebFormElementObserver (inferred).
- Lifetime confusion — Reach a teardown where Disconnect() is bypassed, leaking the self-rooted object or leaving an untraced cross-boundary pointer (inferred).
- Outcome — Latent UAF/leak hazard only; no demonstrated exploit primitive (inferred).
Detection & hunting
For defenders and detection engineers:
- *grep for SelfKeepAlive used together with a public ::Disconnect() teardown and a raw_ptr/raw pointer crossing the Blink public API — Flag GC objects that self-root and are freed by an external manual call rather than RAII/unique_ptr.
- weggli: raw_ptr<…Observer> members freed via ->Disconnect() then =nullptr — Hunt manual free patterns over GC/Oilpan objects exposed across module boundaries.
Audit directions
- Other Blink public/web modules exposing MakeGarbageCollected objects with Disconnect()-style manual teardown — Look for the same self-keep-alive-as-malloc pattern and convert to owned unique_ptr.
- components/autofill renderer code holding raw_ptr to Blink-created observers — Verify every observer has clear RAII ownership rather than external Disconnect() dependence.