blitz-dom: connecting or removing a deep subtree overflows the stack
Generated from repros/blitz-dom-subtree-walk-recursive/README.md when the site was built at 7aa23e2.
What niso does
Section titled “What niso does”Nothing it can change: niso connects and removes subtrees through DocumentMutator. Its test of 200k-deep trees
(crates/niso-quickjs/tests/deep_trees.rs) runs on a 1 GiB stack.
Known production limit. On an 8 MiB stack (the usual main thread), removing a connected subtree more than about
30k levels deep aborts the process with a stack overflow: 30k works and 40k does not.
niso_runtime.h says so at niso_dom_new. niso’s own walks over a tree (cloning, serializing,
textContent, isEqualNode) keep stacks of their own, so they handle 200k-deep trees on an 8 MiB stack.
Expected vs actual
Section titled “Expected vs actual”BaseDocument::iter_subtree_mut (packages/blitz-dom/src/traversal.rs:152-170) recurses once per level, and
process_added_subtree and process_removed_subtree (mutator.rs) walk every subtree that joins or leaves the
document with it. Connecting a 100k-deep chain to the body overflows an 8 MiB stack. Browsers keep these trees
working: Gecko and Blink walk subtrees iteratively.
connecting and removing a 100000-deep chain on an 8 MiB stackexpected: steps ["built", "connected", "removed"], exit successactual: steps ["built"], signal: 6 (SIGABRT) (core dumped)CARGO_TARGET_DIR=~/.capy/work/repro-target cargo run --releaseUpstream main
Section titled “Upstream main”Same code. Blitz main was at b6ace6a0c33e17b654e8e6d0b14a7e04e83756fe when checked (2026-11-09):
iter_subtree_mut_inner still calls itself at traversal.rs:167.
Existing issue or PR
Section titled “Existing issue or PR”None found in a search of DioxusLabs/blitz issues and PRs for “stack overflow”, “deep tree” and “recursion”.
Issue text
Section titled “Issue text”Title: iter_subtree_mut recurses per level, so connecting or removing a deep subtree overflows the stack
Body:
iter_subtree_mut calls iter_subtree_mut_inner once per level of the tree, and process_added_subtree and
process_removed_subtree go through it, so appending or removing a deep enough subtree aborts with a stack
overflow: a 100k-deep chain on an 8 MiB stack does. Walking with an explicit stack of (node, next child index)
pairs (or taking each node’s children list as today and pushing them) keeps it in constant stack.
use std::process::Command;
use blitz_dom::{DocumentConfig, LocalName, QualName, ns};use blitz_html::HtmlDocument;
const DEPTH: usize = 100_000;
/// Builds a chain of DEPTH divs outside the document, connects it to the body/// and removes it again, on a thread with an 8 MiB stack (the main thread's/// usual size).fn connect_and_remove_chain() { let mut doc = HtmlDocument::from_html("<!DOCTYPE html><body></body>", DocumentConfig::default()); let html = doc.root_element().id; let body = *doc.get_node(html).unwrap().children.last().unwrap(); let mut m = doc.mutate(); let mut top = m.create_element(QualName::new(None, ns!(html), LocalName::from("div")), vec![]); for _ in 1..DEPTH { let parent = m.create_element(QualName::new(None, ns!(html), LocalName::from("div")), vec![]); m.append_children(parent, &[top]); top = parent; } eprintln!("built"); m.append_children(body, &[top]); eprintln!("connected"); m.remove_node(top); eprintln!("removed");}
fn main() { if std::env::args().nth(1).as_deref() == Some("child") { std::thread::Builder::new().stack_size(8 << 20).spawn(connect_and_remove_chain).unwrap().join().unwrap(); return; } let out = Command::new(std::env::current_exe().unwrap()).arg("child").output().unwrap(); let steps: Vec<String> = String::from_utf8_lossy(&out.stderr).lines().filter(|l| ["built", "connected", "removed"].contains(l)).map(str::to_string).collect(); println!("connecting and removing a {DEPTH}-deep chain on an 8 MiB stack"); println!("expected: steps [\"built\", \"connected\", \"removed\"], exit success"); println!("actual: steps {steps:?}, {}", out.status); if !out.status.success() { std::process::exit(1); }}