Skip to content

fix: Track HTML Stack Mutations in Foreign Content Context - #462

Merged
jmanico merged 5 commits into
mainfrom
461-foreign-context-scope
Sep 10, 2026
Merged

fix: Track HTML Stack Mutations in Foreign Content Context#462
jmanico merged 5 commits into
mainfrom
461-foreign-context-scope

Conversation

@jmanico

@jmanico jmanico commented Sep 10, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #461.

The bounded foreign-content context tracker now models the HTML tree-builder transitions that can change whether a self-closing flag is honored inside SVG or MathML. It follows the current in-body rules for p, headings, list items, buttons, forms, formatting elements, select/option/optgroup, image, and current tree-builder void elements. It also tracks the form pointer and the inherited in-body, in-table, or in-cell mode across a foreign root.

When the correct result still depends on state outside the bounded tracker—table/template state, a complex table transition, or reconstructing the active formatting list—the tracker enters its sticky fail-closed state. In that state only self-closing <svg/> and <math/> roots close themselves.

The element sets now match the current WHATWG rules: select is both a default-scope boundary and a specifically handled end tag, but its start tag no longer changes the insertion mode. The tracker also uses a tree-builder-specific immediate-pop set, since Chrome treats command and isindex as ordinary HTML elements while still immediately popping bgsound.

Known, well-formed transitions recover precisely: a foreign root in a table cell, a completed select island, and an empty table island do not poison later SVG self-closing tags.

No shipped release contains the affected context-tracker code.

Chrome oracle cases

Each hostile transition is followed by <object/>hidden. An uppercase HTML OBJECT owning hidden means Chrome is in HTML content and the sanitizer must suppress that fallback text.

Representative cases fixed here:

<body><template><form><svg></form><object/>hidden
<!-- template contents: FORM > svg, then OBJECT > #text "hidden" -->

<svg><foreignObject><image><math></svg><object/>hidden
<!-- BODY: svg > foreignObject > (IMG, math), then OBJECT > #text "hidden" -->

<svg><foreignObject><div><b></div><math></math></foreignObject><object/>hidden
<!-- foreignObject: DIV > B, then reconstructed B > (math, OBJECT > "hidden") -->

<svg><foreignObject><b><i></b><math></math></foreignObject><object/>hidden
<!-- foreignObject: B > I, then reconstructed I > (math, OBJECT > "hidden") -->

<svg><foreignObject><command><math></math></foreignObject><object/>hidden
<!-- foreignObject: COMMAND > (math, OBJECT > "hidden") -->

The current-select behavior is intentionally different:

<svg><foreignObject><select><svg><object/>hidden
<!-- SELECT > svg > object, with "hidden" in foreign content -->

Test plan

  • Added hostile regressions for table-scope and close-cell transitions, templates, form-pointer behavior, adoption-agency and active-formatting reconstruction, p/list/heading/button stack changes, image, select/option rules, and current parser element categories.
  • Added positive fidelity regressions for completed select/empty-table islands and SVG in a table cell.
  • Cross-checked the directed cases against current Chrome and the current WHATWG parsing algorithm.
  • Ran 200,000 deterministic differential cases against the prior PR head: 8,183 newly suppressed mismatches and zero cases where the branch exposed text that the parser kept inside an HTML object.
  • ./mvnw clean verify passes on JDK 11, 17, 21, and 25 (490 reactor tests per run, including fuzzers and the JPMS consumer check).

Review follow-up (f9819fa)

An independent review against the current WHATWG tree-construction rules and Chrome 152 found five cases where the tracker stayed in foreign content while Chrome was in HTML content, plus one avoidable fidelity loss. Each was reproduced with <object/>hidden as the oracle and is fixed with a regression test:

  • <xmp> now closes an open p, like pre and listing: <svg><foreignObject><p><xmp></xmp><math></svg><object/>hidden
  • <table> after an open p fails closed, since only a no-quirks document closes the p and the embedding document's mode is unknown: <svg><foreignObject><p><table></table></foreignObject><object/>hidden
  • Only the first of duplicate type attributes decides whether an input is hidden, as the tokenizer drops the later ones: <table><svg><foreignObject><select><input type=text type=hidden></foreignObject></svg></select></foreignObject><object/>hidden
  • Untracked cell, caption and section end tags must name the open part; </td> inside a th, </tbody> under thead, or </caption> inside a cell is ignored as browsers ignore it: <table><tr><th></td><svg><foreignObject><form></foreignObject><object/>hidden
  • search is in the specification's special category but not in Chrome's, so an end-tag or list-item walk that reaches one fails closed: <svg><foreignObject><cite><search><span></cite></foreignObject></svg></span></search></cite></foreignObject><object/>hidden
  • Nested tables and captions are followed with a bounded stack instead of making the context permanently unknown, so <table><tr><td><table><tr><td>x</td></tr></table></td></tr></table><svg><path/><rect/></svg> keeps its self-closing tags.

A positive test also checks that each modeled transition still honors a later <path/>, so the transitions are tracked rather than failed closed. Cross-checked with 438 element-category probes and about 78,000 random differential cases in Chrome 152, in quirks and no-quirks mode: zero cases where the branch exposes text that Chrome keeps inside an HTML object. ./mvnw clean verify passes on JDK 21 (495 reactor tests).

🤖 Generated with Claude Code

https://claude.ai/code/session_01RajvH16nNkgw9TJS6nEpTT

jmanico and others added 2 commits September 10, 2026 07:51
The context tracker assumed that an integration point between the
current node and the tracked bottom blocks every HTML end tag.  That
holds for end tags searched in the default, list-item and button scopes
and for the "any other end tag" walk, but table scope is bounded only by
html, table and template, so </table>, </td> and the rest of the table
structure reach past foreignObject, desc, mi and annotation-xml to a
table around the foreign content and close all of it.  The tracker kept
its foreign state, honored the self-closing flag on the next tag, and
a dropped element such as <object/> no longer held the text after it.

The HTML end-tag scan also popped through elements in the special
category, where browsers ignore the tag, matched headings only by exact
name although </h1> closes any open heading, popped the elements above a
form on </form>, which only removes the form, and cleared its stack on a
stray end tag although the browser may still be in foreign content, so
the next <svg> inside MathML was taken for an SVG root.

The tracker now enters its existing unknown state, where only <svg/> and
<math/> close themselves, whenever an end tag reaches the HTML rules with
an effect that depends on untracked ancestors, on the insertion mode, or
on the list of active formatting elements: table-scope and template end
tags, formatting end tags across a special element, stray end tags with
nothing tracked in the way, and table, template, select and frameset
start tags at an integration point.  Within the tracked region the walk
follows the spec: the special category for any other end tag, the
scopes for the tags with their own rules, heading cross-matching, and
form removal.  Integration points and annotation-xml bound every scope.

Verified against Chrome on the hand-built cases and on 2000 generated
inputs, where the fix removes every divergence toward foreign state.

Closes #461.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VYF1WjZmwbGNrph7RDw2BS
@jmanico jmanico changed the title fix: Fail Closed When an HTML End Tag Reaches Foreign Content With an Unknown Effect fix: Track HTML Stack Mutations in Foreign Content Context Sep 10, 2026
jmanico and others added 3 commits September 10, 2026 10:41
Each case was verified against Chrome 152 with <object/>hidden as the
oracle: an HTML object owning the text means the browser is in HTML
content and the sanitizer must not honor the self-closing flag.

- An xmp start tag closes an open p, like pre and listing.
- A table start tag after an open p fails closed, since only a
  no-quirks document closes the p and the embedding document's mode
  is unknown.
- Only the first of duplicate type attributes decides whether an input
  is hidden, as the tokenizer drops the later ones.
- Untracked cells, captions, sections and nested tables are followed
  with a bounded stack, so a cell or section end tag that names
  something else is ignored as browsers ignore it, and well-formed
  nested tables and captions no longer poison later SVG.
- A search element makes the context unknown when an end-tag or
  list-item walk reaches it, since the specification puts it in the
  special category and current Chrome does not.
- A self-closing svg or math root still closes itself inside a tracked
  empty table when the tracker gives up.

Issue #461.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RajvH16nNkgw9TJS6nEpTT
dialog is in the WHATWG parsing algorithm's special category but absent
from Chrome's special-node set, the same split the review follow-up
already handled for search. A stack walk that reached an open dialog
followed Chrome alone, so a later self-closing <object/> (or a raw-text
element such as <title/>) was honored, exposing text that a
spec-compliant or Firefox parser keeps inside the HTML element.

Add dialog to the special and ambiguously-special sets so the three
affected walks fail closed: the "any other end tag" scan, the list-item
start scan, and the adoption agency's furthest-block search. Add a
regression test covering each walk; every case leaks the text on the
prior branch head and is suppressed now. A modeled dialog block
transition still honors a later self-closing tag.

Issue #461.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015c2SFXiJJvU9JfWBCtGdNo
Bring in #333, #447, #243, #155 and #453 from main; combine the change_log
Next release bullets with the foreign-content tracker bullets on this branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015c2SFXiJJvU9JfWBCtGdNo
@jmanico
jmanico merged commit c13d376 into main Sep 10, 2026
6 checks passed
@jmanico
jmanico deleted the 461-foreign-context-scope branch September 11, 2026 01:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Foreign content context tracker keeps foreign state after HTML end tags that browsers process with table scope or the special category

1 participant