Skip to content

Close the open list item for a list item start tag, through formatting - #500

Merged
jmanico merged 7 commits into
mainfrom
list-item-after-resumed-formatting-492-8
Sep 21, 2026
Merged

jmanico merged 7 commits into
mainfrom
list-item-after-resumed-formatting-492-8

Conversation

@jmanico

@jmanico jmanico commented Sep 19, 2026

Copy link
Copy Markdown
Member

Item 8 of #492: a list-item start after reconstructed formatting could nest
inside the preceding item instead of closing it and becoming its sibling.

What broke

Sanitizers.BLOCKS.and(Sanitizers.FORMATTING) on
<math><b>x</math><li>y<li>z produced nested list items. The same happened
for ordinary lists such as <ul><li><b>x<li>y: containment metadata saw the
open formatting element and supplied a fresh list inside it, while a browser's
list-item start algorithm first closes the open li through that formatting
and reconstructs the formatting in the sibling item.

What changed

  • TagBalancingHtmlStreamEventReceiver applies the list-item start walk to
    the emitted HTML context, then uses the existing close-and-reconstruct path
    so formatting resumes inside the sibling item.
  • The walk preserves logical select/option/optgroup boundaries when the
    policy drops their output, and does not cross a forwarded foreign root in a
    way that would reinterpret foreign PCDATA as HTML raw text.
  • Formatting queued by this walk waits until after a raw-text start where a
    browser would insert that element beside the formatting. The queue retains
    the browser's three-of-a-name Noah's Ark bound.
  • Physical output-list state is exposed through the existing output-policy
    seam so a dropped or renamed wrapper cannot make first and second passes
    take different paths.
  • Nesting-limit handling retains logical reconstruction entries that cannot
    be emitted and accounts for the physical output budget of a sibling item,
    including later dynamic limit changes.
  • Forwarded raw-text context is maintained incrementally, keeping a long run
    of unknown elements followed by list items linear.
  • Hostile foreign/raw-text, dropped-select, li/option, fixed-limit, and
    dynamic-limit regressions are covered. change_log.md documents the
    behavior change.

The branch was updated by merging current main at aca9a0e in 11bda2d;
the stabilization commit is 7ae2b5c.

Verification

  • Reduced item-8 cases pass and are fixed points, including resumed formatting,
    dropped select/option/optgroup, li-only/li-option/li-select,
    forwarded SVG raw-text payloads, and the two nesting-limit residuals.
  • The earlier 22 sanitizer gains and 17 limit/event gains are eliminated.
  • Full differential probe against main at aca9a0e, using the identical
    probe source, compiled probe, corpus, policies, and seeds:
    • Probe source SHA-256:
      f56c346716f848914be899f37b1b422c1a84b113a97526b71691ca76e33ba972
    • Base JAR SHA-256:
      5ae0cbd95686e8d5130f3b2dce2c8e57a6f9ebfffde5bb2a7936c83b693792b1
    • Candidate JAR SHA-256:
      8b652c439d4c5584dbded092b21a4c01866f2fc66d631d30413fd634788ee083
    • Sanitizer mode: 678,920 rows; 3,936 changed outputs; 0 gained
      flags
      ; 903 flags removed (727 non-idempotent, 173 tree-mutates,
      3 fidelity); 0 metadata, row, or total mismatches.
    • Limit/event mode: 36,063 rows; 26 changed outputs; 0 gained flags;
      0 lost flags; 0 metadata, row, or total mismatches.
  • ./mvnw -o -ntp -B clean verify: success; 697 library tests and 7 example
    tests, 0 failures or errors, plus the JPMS consumer verification.

Part of #492 (item 8)

jmanico and others added 4 commits September 19, 2026 08:03
A browser's list item start tag closes the list item that is open before
it inserts the new one, walking down its stack past the formatting
elements open inside that item, so the items are siblings and the
formatting is reconstructed inside the second.  The balancer consulted
the containment metadata instead, which answered the open formatting
element with a fresh list inside it, so the second item and every one
after it nested a list deeper.  The walk stops where the parsing
algorithm's does, at an element in its special category other than
address, div and p, so an item inside a heading, a form, a table, a
template or a foreign root still nests as a browser nests it, and the
item a select keeps for content it cannot hold is none of the input's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…w text

Closing the item for a list item start tag queues the formatting open
inside it, and the probe found that formatting reopened inside a textarea
or a style that followed, where its tags come out as that element's text.
A browser reconstructs formatting around such an element, and again for
the text after it, which is what the balancer now does; an element
forwarded under a name this receiver does not recognize, an SVG-cased
textArea, is asked for separately, since it has no entry on the stack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tput

The review measured a 252x output amplification on a shipped policy: one
list item after another closing over the same open formatting element
queued one more copy each time.  The queue now keeps at most three of a
name, as a browser's list of active formatting elements does through its
Noah's Ark clause, which is the bound a browser has; the same input is
now five times its length, not two hundred and fifty.

An element with no output bounds nothing a browser reading the output can
see, so it no longer stops the walk for the item to close, which the
review found made a first pass keep an item the second pass closed.  A
name the specification calls special but Chrome does not, dialog and
search, is no barrier either, as this receiver's input tracker and
testDialogElementCategoryIsNotReliedOn already say.  Preparation no
longer re-runs the walk when it re-enters itself, since the barriers it
honoured may have closed since, which made a select decide the item's
fate through the close loop rather than the walk.

Formatting is reconstructed only where it needs no wrapper implied under
the container, so a style after the items is the list's own child, as a
browser has it, and the output is a fixed point.  It is not reconstructed
around an element whose content a browser reads as text either, only for
the text that follows it, and an element forwarded inside such a one at
any depth counts, not only the innermost.  The check is asked once per
preparation rather than once per resumption.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ption's item

A table this receiver closed in the output to put content in front of it,
and an entry whose output is a foreign element, are judged as the output
shows them, not as the logical stack holds them, so the first pass no
longer keeps an item that the second pass closes.  An option stays a
barrier although a browser walks past one: a list this receiver opens
inside an option holds the item it writes for the list's content, and
closing that item for the next start tag would unwrite on the next pass
what this pass wrote.

The probe is down from 52 flagged rows on 43 inputs to 22 on 16; the
remaining ones are listed in the pull request and this change is not
ready to merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jmanico
jmanico marked this pull request as draft September 19, 2026 16:06
jmanico and others added 3 commits September 19, 2026 09:12
…hat changed

An element the renderer writes as a pre, an xmp, listing or plaintext, has
no raw text in the output, so formatting is reconstructed for it as for
any other element; xmp is also the one raw-text start tag whose own rule
reconstructs it.  The barrier for the walk is the emitted element, not the
name the input wrote, so a policy that renames a barrier to something else
leaves no barrier for the next pass to disagree about.  The resumption
queue drops the earliest of four alike, as the Noah's Ark clause says, not
the innermost.  The change log describes what this now does, including the
queue's bound and where formatting is reconstructed, and the stranded doc
comment is back on the method it describes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	change_log.md
#	owasp-java-html-sanitizer/src/test/java/org/owasp/html/HtmlSanitizerTest.java
@jmanico
jmanico marked this pull request as ready for review September 21, 2026 02:25
@jmanico
jmanico merged commit be813dd into main Sep 21, 2026
7 checks passed
@jmanico
jmanico deleted the list-item-after-resumed-formatting-492-8 branch September 21, 2026 02:30
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.

1 participant