Repository navigation
Conversation
`Element::set_tag_name` on an HTML void element (`<img>`, `<br>`, `<input>`, `<hr>`, ...) rewrote only the start tag. A void element has no end tag in the input, so nothing closed the new name: every sibling after `<img>` renamed to `picture` re-parsed as a child of `<picture>`, and after a rename to `textarea` or `style` the rest of the document became its text. The start tag now carries an `emit_end_tag` flag. `set_tag_name` sets it when the element cannot have content, is in the HTML namespace, and the new name is not itself a void element name. The serializer then writes `</name>` straight after the start tag, before any `after()` content, and drops a decorative `/>`. A rename to another void name, a rename of a self-closing element in foreign content (closed by its `/>` whatever the name), and `remove()`/`replace()` are unchanged. The element still reports `can_have_content() == false`, so `prepend`, `append` and `set_inner_content` stay no-ops and no end tag handlers run for the written tag. The void-name list moves from `selectors_vm::stack` to `Tag::is_void_html_element` so both callers share it. Nine existing tests used an `<img>` rename as a match marker and now expect the end tag too.
robobun
requested review from
a team,
Noah-Kennedy,
jasnell and
orium
as code owners
September 8, 2026 11:16
robobun
added a commit
to oven-sh/bun
that referenced
this pull request
Sep 15, 2026
… name Pin oven-sh/lol-html at 5816ca4 (oven-sh/lol-html#4, the bun-branch port of cloudflare/lol-html#337). `el.tagName = "picture"` on `<img>` rewrote only the start tag, so the following siblings re-parsed as children of the unclosed `<picture>`. lol-html now writes `</picture>` right after the start tag unless the new name is void too. The pin also moves past ea6125b (descendant-selector matching linear in depth), which #41479 pins on its own.
Contributor
Author
|
@orium would you have time to look at this one? It comes from the same fuzzing run as #329, #331 and #332 (Bun's |
robobun
added a commit
to oven-sh/lol-html
that referenced
this pull request
Sep 22, 2026
`Element::set_tag_name` on an HTML void element (`<img>`, `<br>`, `<input>`, `<hr>`, ...) rewrote only the start tag. A void element has no end tag in the input, so nothing closed the new name: every sibling after `<img>` renamed to `picture` re-parsed as a child of `<picture>`, and after a rename to `textarea` or `style` the rest of the document became its text. The start tag now carries an `emit_end_tag` flag. `set_tag_name` sets it when the element cannot have content, is in the HTML namespace, and the new name is not itself a void element name. The serializer then writes `</name>` straight after the start tag, before any `after()` content, and drops a decorative `/>`. A rename to another void name, a rename of a self-closing element in foreign content (closed by its `/>` whatever the name), and `remove()`/`replace()` are unchanged. The element still reports `can_have_content() == false`, so `prepend`, `append` and `set_inner_content` stay no-ops and no end tag handlers run for the written tag. The void-name list moves from `selectors_vm::stack` to `Tag::is_void_html_element` so both callers share it. Nine existing tests used an `<img>` rename as a match marker and now expect the end tag too. Port of cloudflare#337 (8dba2f7) to the `bun` branch. The differences: the start tag is reached through `tag()`/`tag_mut()`, and `StartTag::take_owned` copies the flag so that it survives a handler suspension (new test `void_element_renamed_across_suspension`).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Element::set_tag_nameon an HTML void element (<img>,<br>,<input>,<hr>, ...) rewrites only the start tag. A void element has no end tag in the input, so nothing closes the new name. When a browser re-parses the output, every following sibling becomes a child of the renamed element, and after a rename totextarea,title,styleorscriptthe rest of the document becomes its text:The DOM equivalent (
createElement+ copy attributes +replaceWith) gives an empty, closed element with its siblings intact. The doc comment asks for "the same content model", but the library already knows the element is void (can_have_content()isfalse), and the caller has no good way to close it:after("</picture>", ContentType::Html)ispush_front, so it only works if it is the lastafter()call across all handlers, it leaves a stray end tag underremove()/replace(), and it is wrong for/>elements in foreign content.This was found by differential fuzzing of Bun's
HTMLRewriteragainst Chromium: 8 of 12 void-to-non-void renames diverged from the DOM (the 4 that agreed had no following siblings), while 157/157 other renames agreed.Change
StartTaggets anemit_end_tagflag.Element::set_tag_namesets it when the element cannot have content, is in the HTML namespace, and the new name is not itself a void element name.serialize_selfthen writes</name>right after the start tag, which puts it before anyafter()content, and drops a decorative/>. Unchanged:hr->br): no end tag,</br>would even parse as a second<br>;<svg><circle/>):/>closes it whatever it is called;remove(),remove_and_keep_content(),replace(): the start tag is not serialized, so neither is the end tag;can_have_content()staysfalse, soprepend/append/set_inner_contentremain no-ops andend_tag_handlers()staysNone(the written end tag runs no handlers). The rename only keeps the element empty and closed; it does not turn it into a container.The void-name list moves from
selectors_vm::stack::is_void_elementtoTag::is_void_html_elementso both callers share it (the ESI special case stays in the stack).Nine existing tests in
element.rsrename a matched<img>as a marker (sanitizer_bypass1,void_element, ...); their expectations now include the end tag. New tests cover the cases above in UTF-8 and EUC-JP.