Skip to content

Stop DeepClone/DeepCopy and node teardown from recursing per tree level - #1091

Open
afonsojanu wants to merge 1 commit into
leethomason:masterfrom
afonsojanu:fix/deep-tree-teardown-and-clone-stack-overflow
Open

Stop DeepClone/DeepCopy and node teardown from recursing per tree level#1091
afonsojanu wants to merge 1 commit into
leethomason:masterfrom
afonsojanu:fix/deep-tree-teardown-and-clone-stack-overflow

Conversation

@afonsojanu

Copy link
Copy Markdown

Fixes the stack overflow half of #839.

A tree built up at runtime through the normal Insert*Child API isn't bounded by XMLDocument::DepthTracker the way a parsed one is, so a long chain of single-child nodes can push both operations past the stack limit. DeepClone (and DeepCopy, which just calls it once per top level node) walked children recursively. Node teardown had a partial fix in #1075 for the sibling case, but a node still recurses through DeleteNode once per level for anything nested more than a couple of levels deep, since destroying a node destroys its child, whose destructor then destroys its own child, and so on.

Both now gather the subtree with an explicit, heap backed stack instead of recursing. For teardown specifically, every descendant gets its own _firstChild link cleared before it's actually destroyed, so each one's (possibly virtual, derived class) destructor still runs correctly but finds no children left to recurse into, since they're already queued up for destruction in the same pass.

Added a test that builds a 60000 level deep tree, which reliably overflowed the stack on my machine before this change and now finishes in well under a second, covering both DeepCopy/DeepClone and plain destruction.

Accept() has the same recursive shape (mentioned in the issue too) but it's a separate code path with its own visitor dispatch, so I left it out of this one to keep the diff focused. Happy to take a pass at that separately if useful.

Ran the full xmltest suite locally (530 tests) before and after: 1 crash pre-fix (SIGSEGV, confirmed with the new test reverted onto the old code), 530 passing post-fix.

A tree built up at runtime through the normal Insert*Child API isn't
bounded by XMLDocument::DepthTracker the way a parsed one is, so a long
enough chain of single-child nodes can push both operations past their
stack limit. DeepClone (and DeepCopy, which just calls it per top level
node) walked children recursively, and even after the destructor got its
sibling-iteration cleanup in leethomason#1075, tearing down a node still recursed
once per level through DeleteNode for anything nested more than a couple
of siblings deep.

Both now gather the subtree with an explicit, heap backed stack instead.
For teardown, every descendant has its own _firstChild link cleared
before it's actually destroyed, so each one's derived destructor runs
without trying to recurse back into children that are already queued up
in the same pass.

Added a test with a 60000 level deep tree that reliably overflowed the
stack before this change and now runs in well under a second. Reproduces
the crash reported in leethomason#839 for both DeepClone/DeepCopy and plain
destruction; Accept() has the same recursive shape but is a separate
code path and isn't touched here.
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