NamespaceResolver::push (src/name.rs) increments its depth counter with an unguarded self.nesting_level += 1; on a u16 field (nesting_level, declared near src/name.rs:514). push is called once per Start/Empty event by NsReader::process_event (src/reader/ns_reader.rs), and there is no depth cap anywhere on the read_event path — so a document with 65 536 nested elements overflows the counter. Note the asymmetry: the matching pop() already saturates (self.set_level(self.nesting_level.saturating_sub(1));, src/name.rs:763), but the increment does not. Reachable from the public NsReader::read_resolved_event* on untrusted XML.
Impact — two facets, depending on build profile:
overflow-checks builds (all debug builds; release profiles that opt in): the 65 536-th push panics attempt to add with overflow → remote, pre-auth DoS from a ~490 KB document.
- Default
--release (overflow-checks off): the u16 silently wraps 65535 → 0, after which set_level's scope-truncation (rposition(|n| n.level <= level)) runs on non-monotonic levels and corrupts namespace resolution — bindings from closed scopes leak, in-scope bindings vanish. For any consumer that trusts resolved namespaces (XML-DSig, SOAP action routing, element-by-namespace access decisions) this is a security-relevant correctness bug, not just a crash.
This is live on master (src/name.rs still self.nesting_level += 1; with pop saturating_sub), and is distinct from the closed #970 / #972, which capped declarations per element (max_declarations_per_element / TooManyDeclarations) — this is the orthogonal depth counter.
How to reproduce
cargo new repro && cd repro, add quick-xml = "0.41" to Cargo.toml, drop this into src/main.rs:
use quick_xml::reader::NsReader;
use quick_xml::events::Event;
fn main() {
let depth = 70_000;
let xml = "<a>".repeat(depth) + &"</a>".repeat(depth); // ~490 KB
let mut r = NsReader::from_str(&xml);
let mut n = 0u64;
loop {
match r.read_resolved_event() {
Ok((_, Event::Eof)) => break,
Ok(_) => n += 1,
Err(e) => { eprintln!("Err after {n} events: {e:?}"); break; }
}
}
eprintln!("done, {n} events");
}
-
RUSTFLAGS="-Coverflow-checks=on" cargo run --release → thread 'main' panicked at src/name.rs:...: attempt to add with overflow.
-
Default cargo run --release → no panic, but the resolver is corrupted. Nesting one xmlns:p outside and a different xmlns:p inside a nested <x>, then resolving the same prefix after popping back out:
| depth |
inner <p:e> (expect BBB) |
outer <p:f> (expect AAA) |
| 3 (control) |
BBB ✓ |
AAA ✓ |
| 65534 |
BBB |
BBB ✗ (closed-scope binding leaked) |
| ≥65535 |
BBB |
Unknown("p") ✗ (in-scope binding vanished) |
Suggested fix
A saturating_add (mirroring pop) removes the panic but not the misresolution — at saturation all levels ≥ u16::MAX collapse to one and set_level still over-truncates (verified). The correct fix is a depth cap that returns a clean error at the u16 boundary, in the same spirit as the existing TooManyDeclarations guard. I have a patch (a NamespaceError::TooDeeplyNested variant + checked_add, all existing tests pass incl. --features serialize) and am happy to open a PR.
Found by the rust-in-peace security pipeline.
NamespaceResolver::push(src/name.rs) increments its depth counter with an unguardedself.nesting_level += 1;on au16field (nesting_level, declared nearsrc/name.rs:514).pushis called once perStart/Emptyevent byNsReader::process_event(src/reader/ns_reader.rs), and there is no depth cap anywhere on theread_eventpath — so a document with 65 536 nested elements overflows the counter. Note the asymmetry: the matchingpop()already saturates (self.set_level(self.nesting_level.saturating_sub(1));,src/name.rs:763), but the increment does not. Reachable from the publicNsReader::read_resolved_event*on untrusted XML.Impact — two facets, depending on build profile:
overflow-checksbuilds (all debug builds; release profiles that opt in): the 65 536-thpushpanicsattempt to add with overflow→ remote, pre-auth DoS from a ~490 KB document.--release(overflow-checksoff): theu16silently wraps 65535 → 0, after whichset_level's scope-truncation (rposition(|n| n.level <= level)) runs on non-monotonic levels and corrupts namespace resolution — bindings from closed scopes leak, in-scope bindings vanish. For any consumer that trusts resolved namespaces (XML-DSig, SOAP action routing, element-by-namespace access decisions) this is a security-relevant correctness bug, not just a crash.This is live on
master(src/name.rsstillself.nesting_level += 1;withpopsaturating_sub), and is distinct from the closed #970 / #972, which capped declarations per element (max_declarations_per_element/TooManyDeclarations) — this is the orthogonal depth counter.How to reproduce
cargo new repro && cd repro, addquick-xml = "0.41"toCargo.toml, drop this intosrc/main.rs:RUSTFLAGS="-Coverflow-checks=on" cargo run --release→thread 'main' panicked at src/name.rs:...: attempt to add with overflow.Default
cargo run --release→ no panic, but the resolver is corrupted. Nesting onexmlns:poutside and a differentxmlns:pinside a nested<x>, then resolving the same prefix after popping back out:<p:e>(expectBBB)<p:f>(expectAAA)BBB✓AAA✓BBBBBB✗ (closed-scope binding leaked)BBBUnknown("p")✗ (in-scope binding vanished)Suggested fix
A
saturating_add(mirroringpop) removes the panic but not the misresolution — at saturation all levels ≥u16::MAXcollapse to one andset_levelstill over-truncates (verified). The correct fix is a depth cap that returns a clean error at theu16boundary, in the same spirit as the existingTooManyDeclarationsguard. I have a patch (aNamespaceError::TooDeeplyNestedvariant +checked_add, all existing tests pass incl.--features serialize) and am happy to open a PR.Found by the rust-in-peace security pipeline.