Skip to content

Reject all comment-closing sequences in Comment::set_text (cherry-pick of upstream ba25359) - #6

Closed
robobun wants to merge 4 commits into
bunfrom
robobun/comment-closing-sequences
Closed

robobun wants to merge 4 commits into
bunfrom
robobun/comment-closing-sequences

Conversation

@robobun

@robobun robobun commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Comment::set_text rejects --> only (src/rewritable_units/tokens/comment.rs:70). The HTML tokenizer also ends a comment at --!> (comment end bang state), and ends an empty comment at <!--> and <!---> (abrupt closing of empty comment). So text with --!> anywhere, or with a leading > or ->, ends the comment early and the rest of the text becomes markup: set_text("x--!><img src=x onerror=alert(1)>") serializes to <!--x--!><img src=x onerror=alert(1)>-->.
  • Upstream fixed this in cloudflare/lol-html@ba25359 (released in v3.0.0, listed as a security fix). This branch is on the v2.7.2 base and lacks it.

Fix

  • Cherry-pick of upstream commit ba25359 onto bun, with -x. set_text returns CommentTextError::CommentClosingSequence for all four forms. The error text is upstream's new one: Comment text shouldn't contain a comment-closing sequence.
  • Two conflicts, both mechanical. set_text keeps the v2.7.2 BytesCow::from_str_without_replacements(..) plus into_owned() (upstream had moved to owned_from_str_without_replacements). The CHANGELOG.md hunk is dropped because this branch has no v3.0.0 section.
  • Verified: cargo test --features=integration_test (172 + 3 + 32 + 2 pass). In Bun, comment.text = "x--!>...", ">..." and "->..." now throw, and near misses such as --!, x--, a->b still round-trip.

Please merge without squash, or tell me the new sha. Bun pins this branch by commit in scripts/build/deps/lolhtml.ts.

Bun side: oven-sh/bun#42646

robobun and others added 4 commits August 2, 2026 02:58
A handler can return Err(SuspensionRequest) to park the current rewritable
unit on the heap and exit write()/end() with the non-poisoning
RewritingError::Suspended; HtmlRewriter::resume() continues from a
StateMachineBookmark.

- take_owned() on each rewritable unit deep-copies borrowed byte slices so
  the unit outlives the write() stack frame
- Arena::shift advances a start offset instead of memmoving the tail (a
  suspended rewrite re-feeds its unconsumed tail on every resume)
- the pending captured-text flush is hoisted from Dispatcher::handle_tag
  into the lexer actions so every suspension point has a uniform shape
- end_mut() is the non-consuming end() for a rewriter that may suspend in a
  document-end handler

Bun uses this so an async JS content handler can suspend the rewrite and
resume it from a promise reaction instead of spinning a nested event loop
inside write(). See oven-sh/bun#36733.
The upstream ci.yml triggers on push: branches: [master] only, so an
additive bun.yml runs cargo test --features=integration_test on pushes/PRs
to bun without touching ci.yml (keeps rebases onto upstream clean).
…epth

The selector-VM open-element stack had two per-token scans that go
quadratic (or worse) on pathological nesting:

  * Stack::pop_up_to did a full rposition() over the open stack for
    every end tag. A stray end tag that matches nothing still walked
    the whole stack, so N unclosed opens plus N stray closes was O(N^2):
    "<a></b>".repeat(100_000) (~700 KB) burned ~25s of CPU.

  * try_exec_hereditary_jumps_without_attrs walked every ancestor and
    re-executed its hereditary_jumps for every start tag. For a selector
    like "div div" over N nested <div>s every item carries the same
    address range, so the walk is O(depth) per tag and O(N^2) overall;
    k-arm chains ("div div div") compound to O(N^k).

Both paths are reachable from HtmlRewriter with any element-handler
registration, so untrusted HTML can pin a CPU core via sub-MB input.
Memory stays linear so max_allowed_memory_usage does not bound it.

Changes:

  * Stack now keeps open_name_counts: HashMap<LocalName, usize> so
    pop_up_to rejects a name not on the stack in O(1). When the name is
    present the rposition scan length equals the subsequent drain
    length, so total scan work over a document is bounded by total
    pushes.

  * Stack now keeps active_hereditary_jumps: Vec<(AddressRange, usize)>,
    the distinct hereditary-jump ranges currently contributed by any
    open item, each paired with the shallowest depth that introduced it.
    Descendant matching iterates this flat list (bounded by program
    size, not depth) and pop_up_to prunes it by depth. The per-item
    has_ancestor_with_hereditary_jumps flag and the
    HereditaryJumpPtr.stack_offset resume coordinate are removed.

  * LocalName gets a manual Hash impl that ASCII-lowercases the Bytes
    arm so it agrees with the existing case-insensitive PartialEq.
    Without this, keying a HashMap on LocalName is unsound for any name
    that falls off LocalNameHash (custom elements, >12 chars, chars
    outside a-zA-Z1-6). This also fixes the same latent contract
    violation in TypedChildCounterMap.

Re-executing the same address range against the same start tag was
already a no-op after the matched-id set union in add_execution_branch,
so dedup-once execution is observationally equivalent.
…et_text.

Previously `Comment::set_text` only rejected the canonical `-->` terminator, but a
WHATWG-conformant browser also closes a comment on three more byte sequences:

1. `--!>` anywhere in the body (comment-end-bang state).
2. A leading `>` (abrupt-closing-of-empty-comment from comment-start).
3. A leading `->` (abrupt-closing-of-empty-comment from comment-start-dash).

(cherry picked from commit ba25359)

Port to the v2.7.2 base: `set_text` keeps `BytesCow::from_str_without_replacements`
plus `into_owned()`, and the CHANGELOG.md hunk is dropped because this branch has
no v3.0.0 section.
@robobun

robobun commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: the bun branch was rebased onto v3.0.1 on 2026-09-17 (e1a306d) and now has upstream's own ba25359, so this cherry-pick has nothing left to add there.

The port for the v2.7.2 lineage is kept for Bun's pin while Bun's main is on that lineage: robobun/comment-closing-sequences-v2.7.2 (a4a3d0c) is 725ce49 plus this one commit, and oven-sh/bun#42646 pins it. This PR's branch (ef9f6fd, the same commit on top of ea6125b) stays in place so that earlier commits of that Bun PR still build.

@robobun robobun closed this Sep 22, 2026
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.

2 participants