← All reports

[5] Frame-tree ancestor-walk consolidation (`FrameAncestorIterator`)

LowWebCore frame treeOther

0ae01ce

Low — no exploitable defect is identified here. This is a semantics-preserving consolidation of 24 hand-rolled ancestor walks; the value is forward-looking, and the one thing worth carrying away is where type-filtered walks fail open on RemoteFrame ancestors.

WebCore's frame tree is the per-process view of the browsing-context hierarchy: in-process frames are LocalFrame, out-of-process frames appear as RemoteFrame placeholders. Many security decisions walk a frame's ancestors — CSP frame-ancestors, X-Frame-Options, Document::canAccessAncestor, secure-context inheritance, WebAuthn relying-party scoping. Before this change each walk was re-implemented by hand, independently deciding its start node, null handling, and how to treat a non-LocalFrame ancestor.

The angle: for audit value — a RemoteFrame ancestor skipped by a type-filtered policy walk contributes no origin and therefore cannot cause a deny, so a walk that skips rather than fails on remote ancestors is the fail-open direction under site isolation.

The diff introduces FrameAncestorIterator/FrameAncestorRange and converts 24 ancestor walks to them; the commit does not carry a message detailing a specific motivating defect.

Source/WebCore/page/Frame.h

+ template<typename FrameType> class FrameAncestorIterator { ... };
+ template<typename FrameType = Frame> FrameAncestorRange<FrameType> ancestorFrames(Frame&);

Source/WebCore/page/csp/ContentSecurityPolicyDirectiveList.cpp

- for (Frame* ancestor = frame.tree().parent(); ancestor; ancestor = ancestor->tree().parent()) { ... }
+ for (auto& ancestor : ancestorFrames<LocalFrame>(frame)) { ... }

The diff adds FrameAncestorIterator/FrameAncestorRange and converts 24 hand-rolled ancestor walks — including the cross-origin policy sites above, the loader, focus/fullscreen/user-gesture plumbing, accessibility, and two WebProcess callers. FrameAncestorRange holds an owning RefPtr<Frame> m_first so a temporary range passed into std::ranges::any_of keeps its start frame alive, and the iterator's RefPtr<FrameType> m_current replaces the raw Frame* loop variables in FrameLoader::allAncestorsAreComplete/started, the same two functions that lose their NODELETE annotation in FrameLoader.h.

Fail-open-by-omission: a type-filtered ancestor walk silently skips out-of-process ancestors in security-sensitive traversals.

Local vs. remote frames. Under site isolation, a cross-origin subframe lives in another process and appears locally as a RemoteFrame placeholder carrying no in-process origin; same-site frames are LocalFrame.

Why the walks matter. Several ancestor walks are the cross-origin policy decision: CSP frame-ancestors, XFO, canAccessAncestor, secure-context inheritance, WebAuthn RP scoping.

firstOfType() semantics. The new primitive's firstOfType() encodes skip as the default for ancestors of the wrong type. For rendering/accessibility walks, skipping is correct.

No memory-safety or logic defect is identified here. Comparing each hunk against its predecessor, the start node and the skip-vs-fail treatment of non-LocalFrame ancestors are identical before and after at all 24 sites, so this reads as a semantics-preserving consolidation of security-sensitive traversal code; no commit message accompanies the change to indicate otherwise.

The mechanism worth naming anyway: for a policy walk under site isolation, skipping a RemoteFrame ancestor is fail-open — it lives in another process, contributes no origin, and cannot cause a deny. The diff shows both shapes side by side. checkFrameAncestors uses ancestorFrames<LocalFrame>(frame) and skips remote ancestors. A second checkFrameAncestors(..., const Vector<Ref<SecurityOrigin>>& ancestorOrigins) overload evaluates the same directive against an explicitly supplied ancestor-origin list — whether that path covers the remote ancestors the type-filtered walk skips is the open audit question, and determining which callers use which form is the first step. shouldInterruptLoadForXFrameOptions instead uses the untyped ancestorFrames(...) plus an explicit !localFrame -> return true, the fail-closed shape.