Skip to content

fix(node): guard node_ptr_mut with RefCell::try_borrow_mut, not Rc::strong_count#203

Merged
dginev merged 1 commit into
masterfrom
node-ptr-mut-try-borrow
Jun 21, 2026
Merged

fix(node): guard node_ptr_mut with RefCell::try_borrow_mut, not Rc::strong_count#203
dginev merged 1 commit into
masterfrom
node-ptr-mut-try-borrow

Conversation

@dginev

@dginev dginev commented Jun 21, 2026

Copy link
Copy Markdown
Member

Summary

Node::node_ptr_mut gated mutable access on Rc::strong_count <= NODE_RC_MAX_GUARD (default 2). That threshold counts live Node clones, which is not an aliasing conflict — the owning Document caches one wrapper per node (_wrap/ptr_as_option return a clone of the cached Rc<RefCell<_Node>>) and callers hold their own handles, so any non-trivial document exceeds it. The result was spurious Can not mutably reference a shared Node errors on perfectly valid documents (deeply-shared trees reach clone counts in the thousands), and borrow_mut could still panic on the genuine conflict the guard was meant to catch.

All clones of a node share a single RefCell, so RefCell::try_borrow_mut is the exact per-node exclusion check: it returns Err only when the node is actively borrowed (a real re-entrant aliasing conflict), and never on benign clone count.

Changes

  • node_ptr_mut now uses try_borrow_mut (same Result signature, so no caller changes).
  • NODE_RC_MAX_GUARD / set_node_rc_guard are deprecated no-ops, retained for API compatibility; the crate never consults them (set_node_rc_guard has an empty body).
  • tests/mutability_guards.rs rewritten to assert the corrected behavior: benign clones mutate fine, and the deprecated setter has no effect (locks in the no-op).
  • Scope/limits documented on node_ptr_mut: the shared-RefCell invariant holds for linked nodes; unlinked/imported nodes are evicted from the cache (set_unlinked/import_node, intentional vs pointer reuse) and re-wrap to an independent RefCell — unchanged from before. This guards the Rust wrapper, not the C node.

Testing

cargo test green; cargo clippy clean for the changed code. Verified downstream against a large arXiv→XML corpus (latexml-oxide): the spurious "shared Node" errors disappear with no regressions (full downstream suite green), and the former set_node_rc_guard(8192) workaround is no longer needed.

🤖 Generated with Claude Code

…trong_count

node_ptr_mut rejected mutation whenever Rc::strong_count exceeded
NODE_RC_MAX_GUARD (default 2). But strong_count counts live Node clones,
which is not an aliasing conflict: the owning Document caches one wrapper
per node and callers hold their own handles, so any non-trivial document
trips it (e.g. heavily-shared trees reach clone counts in the thousands),
producing spurious 'Can not mutably reference a shared Node' errors -- and
borrow_mut could still panic on the very conflict it was meant to catch.

All clones to one node share a single RefCell, so RefCell::try_borrow_mut
is the exact per-node exclusion check: it fails only on an active
re-entrant borrow, never on benign clone count.

NODE_RC_MAX_GUARD and set_node_rc_guard become deprecated no-ops (kept for
API compatibility); node_ptr_mut never consults them. The mutability_guards
test is rewritten to assert the corrected behavior (benign clones succeed;
the setter has no effect).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dginev dginev requested a review from triptec June 21, 2026 15:00
@dginev dginev merged commit 95f02f3 into master Jun 21, 2026
18 checks passed
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.

1 participant