From 4603c05ab29651a89b45f8877eb10cc248345206 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:58:49 +0000 Subject: [PATCH 1/2] fix(dom): neutralize "-->" when serializing comment contents The comment serializer wrote its contents verbatim. The HTML parser can never produce a comment whose data contains "-->" (it ends the comment), but script can via document.createComment("a-->b"). Serializing that verbatim closed the comment early and let the trailing text escape into markup. Replace "-->" with "-->" so the output round-trips as a single comment. Fixes #342. --- crates/obscura-dom/src/serialize.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/obscura-dom/src/serialize.rs b/crates/obscura-dom/src/serialize.rs index 7a489a5..608b5b4 100644 --- a/crates/obscura-dom/src/serialize.rs +++ b/crates/obscura-dom/src/serialize.rs @@ -72,7 +72,17 @@ impl DomTree { } NodeData::Comment { contents } => { buf.push_str("" (that ends the comment), but script can via + // document.createComment("a-->b"). Emitting it verbatim would close + // the comment early and let the trailing text escape into markup. + // Neutralize the terminator so the serialized output round-trips as + // a single comment. + if contents.contains("-->") { + buf.push_str(&contents.replace("-->", "-->")); + } else { + buf.push_str(contents); + } buf.push_str("-->"); } NodeData::ProcessingInstruction { target, data } => { From e7300c5df77d9921bb6c4add5bcba02088b47e1a Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:58:49 +0000 Subject: [PATCH 2/2] fix(cdp): make DOMSnapshot walk iterative to bound recursion depth The DOMSnapshot walk recursed one frame per nesting level. The MAX_NODES cap bounds the node count but not the depth, so a deeply nested linear chain (script can build thousands of nested elements) could overflow the stack before the count guard triggered. Rewrite it with an explicit stack so depth lives on the heap. Pre-order traversal and the MAX_NODES cap are preserved. Fixes #341. --- crates/obscura-cdp/src/domains/domsnapshot.rs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/obscura-cdp/src/domains/domsnapshot.rs b/crates/obscura-cdp/src/domains/domsnapshot.rs index 9ba6091..a02409e 100644 --- a/crates/obscura-cdp/src/domains/domsnapshot.rs +++ b/crates/obscura-cdp/src/domains/domsnapshot.rs @@ -84,21 +84,32 @@ impl Interner { } /// Pre-order DFS from the document, recording each node and its parent's index. +/// Iterative with an explicit stack: the MAX_NODES cap bounds the node count, +/// but recursion depth is a separate axis. A deeply nested linear chain (script +/// can build thousands of nested elements) would recurse that many frames deep +/// and could overflow the stack before the count guard triggers. An explicit +/// stack keeps the depth on the heap (issue #341). fn walk( dom: &DomTree, - id: NodeId, - parent: i64, + root: NodeId, + root_parent: i64, order: &mut Vec, parent_idx: &mut Vec, ) { - if order.len() >= MAX_NODES { - return; - } - let my = order.len() as i64; - order.push(id); - parent_idx.push(parent); - for child in dom.children(id) { - walk(dom, child, my, order, parent_idx); + // (node, parent_index). Children are pushed in reverse so they pop in + // document order, preserving the original pre-order traversal. + let mut stack: Vec<(NodeId, i64)> = vec![(root, root_parent)]; + while let Some((id, parent)) = stack.pop() { + if order.len() >= MAX_NODES { + break; + } + let my = order.len() as i64; + order.push(id); + parent_idx.push(parent); + let children: Vec = dom.children(id); + for child in children.into_iter().rev() { + stack.push((child, my)); + } } }