Skip to content

fix: correct rare-case bugs from adversarial audit across deadlocks, caching, serialization, memory safety, and Python frontend - #290

Open
XuehaiPan wants to merge 5 commits into
metaopt:mainfrom
XuehaiPan:fix/correctness-audit
Open

fix: correct rare-case bugs from adversarial audit across deadlocks, caching, serialization, memory safety, and Python frontend#290
XuehaiPan wants to merge 5 commits into
metaopt:mainfrom
XuehaiPan:fix/correctness-audit

Conversation

@XuehaiPan

@XuehaiPan XuehaiPan commented Jul 27, 2026

Copy link
Copy Markdown
Member

Description

A systematic adversarial correctness audit of optree, landed as five self-contained commits, one per area. Every finding is a latent bug on a non-common path rather than a reported regression, and most are only observable under free-threading, subinterpreters, debug assertions, or on Windows. The series is kept bisectable rather than squashed, since a failure in this matrix is far easier to attribute per commit.

Commit Area
fix(registry,traversal): correct lock ordering and re-entrancy deadlocks
refactor(pytypes): extract WeakKeyCache and make the type caches interpreter-safe caching
fix(treespec): map struct sequence fields by offset and validate pickled state serialization
fix(treespec): correct namespace merging, GC reporting and walker safety GC, memory safety
fix(ops,accessors,utils): correct field selection, broadcasting and total ordering Python frontend

Each commit message describes its own fixes, and the CHANGELOG.md entries are distributed to match.

Motivation and Context

These are latent correctness bugs rather than reported regressions. Reaching any of them takes a deliberate, non-common path: registering a node concurrently with flattening on another thread, an is_leaf predicate that mutates the container being flattened, a __hash__ that walks gc.get_objects(), a hand-built pickle payload, or a class registered generically rather than through the integration helper. Most are invisible on a release build of a single interpreter, which is why they had not been caught.

The set grew over three adversarial review rounds, each trying to break the code rather than confirm it. Beyond the original audit it now also covers two segfaults (a pickled CUSTOM node naming a built-in type; flatten_up_to() exposing a garbage-collected list with uninitialized slots), an abort from a C++ exception crossing a tp_traverse slot, three further places where Python ran while the registry lock was held, an out-of-bounds list read on Python 3.9-3.12, and an uninterruptible hang when a leaf predicate advances its own iterator.

Not linked to an existing issue; the changes came out of an adversarial audit rather than a bug report.

  • I have raised an issue to propose this change (required for new features and bug fixes)

Types of changes

What types of changes does your code introduce? Put an x in all the boxes that apply:

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds core functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation (update in the documentation)

Implemented Tasks

  • Keep Python off every path that holds sm_mutex, reject re-entrant PyTreeIter iteration, and read the registry size under a single lock
  • Extract WeakKeyCache, key the type caches by (interpreter_id, address), and take ownership before publishing an entry
  • Map struct sequence fields by offset, resolve the pure-Python fallback by probing, and validate pickled treespec state in both directions
  • Reject namespace merges that rebind custom nodes, report a registration's members once per treespec, and never expose a partially built object to user code
  • Resolve accessor children from the registration record, broadcast without re-applying is_leaf, and keep insertion order when keys cannot be compared

Checklist

Go over all the following points, and put an x in all the boxes that apply.
If you are unsure about any of these, don't hesitate to ask. We are here to help!

  • I have read the CONTRIBUTION guide. (required)
  • My change requires a change to the documentation.
  • I have updated the tests accordingly. (required for a bug fix or a new feature)
  • I have updated the documentation accordingly.
  • I have reformatted the code using make format. (required)
  • I have checked the code using make lint. (required)
  • I have ensured make test pass. (required)

@XuehaiPan XuehaiPan self-assigned this Jul 27, 2026
@XuehaiPan XuehaiPan added bug Something isn't working enhancement New feature or request cxx Something related to the CXX source code python Something related to the Python source code thread-safety Multi-threading issues labels Jul 27, 2026
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (e94c440) to head (3c20a98).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #290   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           15        15           
  Lines         1585      1617   +32     
  Branches       212       218    +6     
=========================================
+ Hits          1585      1617   +32     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)
unittests-cp310-Linux 100.00% <100.00%> (ø)
unittests-cp310-Windows 100.00% <100.00%> (ø)
unittests-cp310-macOS 100.00% <100.00%> (ø)
unittests-cp311-Linux 100.00% <100.00%> (ø)
unittests-cp311-Windows 100.00% <100.00%> (ø)
unittests-cp311-macOS 100.00% <100.00%> (ø)
unittests-cp312-Linux 100.00% <100.00%> (ø)
unittests-cp312-Windows 100.00% <100.00%> (ø)
unittests-cp312-macOS 100.00% <100.00%> (ø)
unittests-cp313-Linux 100.00% <100.00%> (ø)
unittests-cp313-Windows 100.00% <100.00%> (ø)
unittests-cp313-macOS 100.00% <100.00%> (ø)
unittests-cp313t-Linux 100.00% <100.00%> (ø)
unittests-cp313t-Windows 100.00% <100.00%> (ø)
unittests-cp313t-macOS 100.00% <100.00%> (ø)
unittests-cp314-Linux 100.00% <100.00%> (ø)
unittests-cp314-Windows 100.00% <100.00%> (ø)
unittests-cp314-macOS 100.00% <100.00%> (ø)
unittests-cp314t-Linux 100.00% <100.00%> (ø)
unittests-cp314t-Windows 100.00% <100.00%> (ø)
unittests-cp314t-macOS 100.00% <100.00%> (ø)
unittests-cp39-Linux 100.00% <100.00%> (ø)
unittests-cp39-Windows 100.00% <100.00%> (ø)
unittests-cp39-macOS 100.00% <100.00%> (ø)
unittests-pp311-Linux 100.00% <100.00%> (ø)
unittests-pp311-Windows 100.00% <100.00%> (ø)
unittests-pp311-macOS 100.00% <100.00%> (ø)
unittests-pydebug 100.00% <100.00%> (ø)
unittests-pydebug-cp310d-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp310d-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp310d-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp311d-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp311d-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp311d-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp312d-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp312d-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp312d-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp313d-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp313d-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp313d-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp313td-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp313td-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp313td-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp314d-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp314d-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp314d-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp314td-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp314td-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp314td-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp39d-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp39d-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp39d-macOS 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@XuehaiPan
XuehaiPan force-pushed the fix/correctness-audit branch 10 times, most recently from 55bb6ae to 948330e Compare July 30, 2026 07:36
No Python may run while `sm_mutex` is held. A concurrent flatten holds the GIL and waits on the read
lock, so anything under the write lock that executes bytecode can hand the GIL to that thread and never
get it back, wedging the whole interpreter. Several sites broke that rule.

- `Register` and `Unregister` ran the namedtuple / PyStructSequence classification and `PyErr_WarnEx`
  under the write lock. Both run Python and can release the GIL. Classify and warn before taking the
  lock; emitting the override warning before any insert also keeps registration atomic under
  warnings-as-errors.
- Their error paths still formatted messages with `PyRepr`, which runs the type's `__repr__` as
  bytecode. `RegisterImpl` and `UnregisterImpl` now only report a `RegistryStatus`, and the caller
  formats and raises after unlocking. An ordinary `enum.Enum` is enough to hit this: its metaclass
  `__repr__` is plain Python.
- `Unregister` and `Clear` dropped the registry's own references while still locked. Dropping the last
  reference to a flatten function runs `__del__` and weakref callbacks, which can re-enter the registry.
  The registrations are now detached under the lock and destroyed after it.
- `Init` recorded the interpreter as alive before `atexit.register` succeeded. That call runs Python
  under the lock, and if it raises the id is left behind with no callback able to remove it. Register
  the cleanup first, mark the interpreter only on success, and roll back on throw.
- `Lookup`, `Register`, `Unregister` and `GetRegistrySize` called `GetSingleton()` under the lock. Under
  `per_interpreter_gil` that releases the GIL once any subinterpreter has imported the module. Acquire
  the singletons first.
- `FlattenInto` read-locked `sm_dict_order_mutex` and then called `IsDictInsertionOrdered`, which locks
  it again. `std::shared_mutex` is not recursive, so the nested shared acquisition is undefined
  behaviour and deadlocks under writer preference. Add `GetDictInsertionOrderedFlags`, which returns
  both flags from one lock, and make `IsDictInsertionOrdered` delegate to it.
- `GetRegistrySize` took `Size()` twice, dropping the lock between the two reads, so a concurrent
  registration could slip in and trip the consistency check. Split out `SizeImpl` and read both
  registries under a single lock.

Re-entrancy is the other half. `PyTreeIter::Next` releases the GIL, takes the non-recursive `m_mutex`,
and only then runs the leaf predicate and any custom `flatten_func`. A callback that advances the same
iterator blocks on a mutex its own thread holds, with the GIL released, so nothing can interrupt it.
Track the thread inside `Next` and reject re-entry with a `RuntimeError`, mirroring CPython's
"generator already executing"; `ToString` and `HashValue` already guard themselves this way.

`RegisterImpl` and `UnregisterImpl` no longer need to be templated on `NoneIsLeaf` now that the callers
hold both singletons, so they become plain member functions.
…rpreter-safe

The namedtuple classification, PyStructSequence classification and struct sequence field-name caches
each hand-rolled the same cache, weakref and locking dance. Extract one `WeakKeyCache` class and route
all three through it, then fix the subinterpreter and lifetime hazards in one place.

Entries are keyed by `(interpreter_id, object address)` rather than the address alone. The address can
be shared across interpreters, since a type like `int` is immortal and lives at the same address in
each, while a computed reference value belongs to the interpreter that produced it. An address-only key
would hand that value to another interpreter, which could use it after the owner freed it on
finalization.

A per-entry weakref evicts an entry when its key is collected, so a later key that reuses the freed
address cannot read a stale value. A per-interpreter `atexit` callback clears that interpreter's
entries, covering what the weakref cannot: an immortal key is never collected, and interpreter ids
restart from 0 after a `Py_Finalize` and `Py_Initialize` cycle, so a fresh interpreter must not inherit
a finalized one's entries.

Two orderings matter, and both were wrong. The lookup releases the read lock and re-acquires the GIL in
that order before touching the borrowed value, so the GIL is never re-acquired while the lock is held,
which would invert the order against the weakref eviction callback. The insert registers the
interpreter's cleanup callback and takes ownership of the value *before* publishing the entry, marking
the interpreter as registered only once that succeeds and rolling back if it does not. Publishing first
left a window where a raising `atexit.register` abandoned an entry whose value was unowned and whose key
had no eviction callback, so the next lookup read it after it was freed.

`GetCurrentPyInterpreterID` sits on that lookup path, twice per leaf through the `Is*Instance` caches,
so it tests the documented failure returns rather than probing the global error state: neither
`PyInterpreterState_Get()` nor `PyInterpreterState_GetID()` can set an exception where it is called.

`hashing.h` gains `std::equal_to` and `std::not_equal_to` for the two pair key types in use. They are
full specializations, not a partial one over `std::pair<T, U>`: a partial specialization of a class
template already instantiated elsewhere in the program is ill-formed, and GCC rejects it across every
translation unit that has already used the pair while clang accepts it silently. The duplication is
therefore deliberate. `std::hash<std::pair<T, U>>` can stay generic because no primary
`std::hash<std::pair>` exists to pre-instantiate. The header also gained the `<string>` it had always
used through a transitive pybind11 include.

Value-returning helpers in `pytypes.h` are marked `[[nodiscard]]`, and the unused `stdutils.h` include
is dropped.
@XuehaiPan
XuehaiPan force-pushed the fix/correctness-audit branch from 948330e to 984d7c2 Compare July 30, 2026 11:46
…led state

Both areas run through `serialization.cpp` and the struct sequence helpers.

`tp_members` lists only the named fields, so indexing it by position mislabelled every slot after the
first unnamed one: `os.stat_result` slots 7, 8 and 9 were reported as `st_atime`, `st_mtime` and
`st_ctime`. Map by the byte offset each member carries instead, and fill the remaining slots with the
unnamed marker. The pure Python `structseq_fields` cannot read those offsets, so it recovers each named
field's position from a probe instance holding distinct sentinels and matches them by identity; unnamed
slots may sit anywhere, not only at the tail. The marker is exported as `PyStructSequence_UnnamedField`
and is now the single test for "this slot is unnamed": the treespec repr renders such a slot as
`<unnamed@N>`, and `StructSequenceEntry` compares against the constant rather than guessing from
`str.isidentifier()`, so its repr reports the index instead of printing the raw marker as if it were a
field name, and `codify` falls back to index access. `PyStructSequenceUnnamedField()` works around the
symbol being unexported before 3.11.0a2, where referencing it broke the import.

`FromPicklable` trusted its input, so a crafted pickle could build a spec that later read out of bounds
or aborted in repr. Validate the kind before the narrowing cast, the arity against the children the
traversal provides, node data on childless kinds, dict key count and distinctness, defaultdict shape,
deque `maxlen`, namedtuple and struct sequence arity, and the whole traversal rather than only its last
node. A `Custom` node must also name a type whose registration is a custom one: the built-in
registrations share the same map but carry empty flatten and unflatten callables, which `MakeNode`
would have called.

Aliasing had to be fixed in both directions. `ToPicklable` copies the node's mutable containers so the
state cannot alias the spec, and `FromPicklable` copies the containers it is given so the restored spec
cannot alias the caller's state, which mutating that state afterwards would otherwise reorder. Both go
through `ListCopy` and `DictCopy`, which use the C API rather than a Python-level `.copy()` and so cost
no attribute lookup, bound-method allocation or vectorcall per node. `__reduce__` makes protocols 0 and
1 reconstruct through `cls.__new__(cls)` instead of aborting.

`is_namedtuple_instance()` and `is_structseq_instance()` accept `object`, matching what they already
did at runtime.
Namespace merging. `compose`, `broadcast_to_common_suffix`, `transform` and
`treespec_from_collection` let an empty namespace adopt the other side's namespace. If a custom node
resolved globally but resolves to a *different* registration under the adopted namespace, the result
claimed the namespace while carrying the wrong registration. Add `FindReregisteredCustomType` and
reject those merges; a type registered only globally still resolves by fallback and is allowed. The
dict constructors also dropped the namespace, losing insertion-ordered key order, and
`MakeFromCollection` ignored the return value of `PyErr_WarnEx`, so an escalated warning surfaced as a
confusing `SystemError`. A childless root goes the other way: a leaf or `None` resolves no custom type
and no key order, so it has no namespace dependency, and keeping the caller's made otherwise identical
treespecs compare unequal.

GC. `PyTreeIter` never visited or cleared its leaf predicate, so a cycle through that callback leaked.
`PyTreeSpec` reports a registration's members once, and only when its own nodes hold every reference
to it: the registration holds one reference to each member however many nodes point at it, so
reporting per node decrements the same object once per node and underflows the collector's shadow
refcount, which is fatal on debug builds. Counting the holders per treespec, rather than testing one
node for sole ownership, lets a spec with repeated custom nodes report correctly. A cycle spanning
several treespecs still survives, since none of them owns every reference; a strict `xfail` pins that.
Neither slot may throw either: both are invoked from `extern "C"` code, where an escaping exception
calls `std::terminate`, and `tp_clear` empties the traversal itself, so the sanity check would abort on
a cleared but still-live spec.

Walker safety. `PathsImpl`, `AccessorsImpl` and `BroadcastToCommonSuffixImpl` recurse once per tree
level, so a deeply nested spec overflowed the native stack and crashed instead of raising. Thread a
depth through them and raise `RecursionError` at `MAX_RECURSION_DEPTH`, lowered further for debug
builds on Windows, where the frames are large and the throw still has to unwind them. A walk also
hands control to arbitrary user code before its own state is consistent. `FlattenUpTo` pre-sized a
`py::list`, which is garbage-collected with NULL slots from the moment it is created, so a custom
`flatten_func` or a dict key `__hash__` could reach it through `gc.get_objects()` and read an unwritten
slot; collect the leaves into a vector and build the list once every element is known. `ListGetItemAs`
read the length once and then ran that same user code, so a list shrunk mid-loop was indexed past its
buffer on versions without `PyList_GetItemRef`; bounds-check on every version instead.

Also fixes `IsPrefix` snapshotting a dict node's subtree from the pristine traversal rather than the
working copy (a processed ancestor may have relocated it), `broadcast_to_common_suffix` sorting the
argument spec's live key list in place while building an error message, custom node entries being
dropped when broadcasting, and `Transform` reading pending counts after `pop_back`. The `traverse` and
`walk` stubs gain overloads for the accumulating form, and the pickle and iterator constructors
document that `cls.__new__(cls)` leaves the C++ value unbuilt.
…otal ordering

An integer accessor entry indexes the children a registered flatten function emitted, not every
declared field. `children_fields` derived them from a field predicate instead, so a class holding a
metadata or non-`init` field resolved an integer entry to the wrong attribute, and a class registered
through the generic `register_pytree_node()` raised `IndexError`. Read the children from the record
`register_node()` leaves on the class, taken from its own `__dict__` so a subclass does not inherit a
stale one, and fall back to the declared field order when there is none.

`register_node` set its `_FIELDS` guard before `register_pytree_node()` ran, so a failed registration
left the class marked as registered and unusable afterwards. Set the guard only on success, and say in
the error which API to use to register the class in another namespace. `InitVar` pseudo-fields are
excluded from `dataclasses.fields()`, so they are neither children nor metadata and cannot round trip:
reject them and point at the generic API. Both modules now document that the generated unflatten
rebuilds via `cls(**fields)`, which re-runs `__init__`, so the round trip is exact only when it
returns each field unchanged.

Accessor identity was wrong in three ways. Equality and hashing compared the bytecode of `__call__`
and `codify` rather than the methods, so two entry classes that happened to share an implementation
compared equal. `PyTreeAccessor` overrode `__eq__` but inherited `tuple.__ne__`, leaving both `==` and
`!=` false against a plain tuple of equal entries. `GetAttrEntry.codify` emitted invalid syntax for a
field name that is not an identifier, now a `getattr()` call.

`tree_broadcast_common` fills an internal tree with a private sentinel and then re-applied the
caller's `is_leaf` to it, so a predicate that inspects leaf values could crash on the sentinel or
collapse the filled subtree and under-replicate. Do not apply the predicate to the internal tree. The
single-input `tree_broadcast_map` variants flattened their tree twice, which broke a one-shot flatten
function and contradicted their documented equivalence with `tree_map`; they now delegate to it. The
transpose-map family and `tree_partition` no longer require a leaf in the outer structure when an
explicit `inner_treespec` leaves nothing to infer.

`prefix_errors` raised `AssertionError` for a custom node whose per-instance entries differ while its
metadata does not, a pair `broadcast_prefix` accepts, and raised `TypeError` while formatting a key
mismatch between keys of different types.

Both total-order sorts fell back incorrectly. `total_order_sorted` handed `key` to `sorted()`, so a
`TypeError` from the callback was mistaken for a comparison failure and swallowed, and the callback
ran twice per element; apply it up front instead. `TotalOrderSort` sorted the same list on both
attempts, committing a partial order when the second comparison also raised; each attempt now sorts a
copy and only a fully sorted one is kept.

`treespec_entry()` and `treespec_child()` accept any `SupportsInt` or `SupportsIndex` object, which
their annotations rejected. `treespec_is_prefix` and `treespec_is_suffix` gain the preorder semantics
they actually implement: reflexive and transitive, but neither antisymmetric nor total.
`tree_broadcast_common` no longer claims its results share one structure, and `broadcast_common`
documents that it returns two lists of leaves rather than two pytrees.
@XuehaiPan
XuehaiPan force-pushed the fix/correctness-audit branch from 984d7c2 to 3c20a98 Compare July 30, 2026 13:02
@XuehaiPan XuehaiPan changed the title fix: fix issues for rare cases across deadlocking, caching, serialization, GC, and Python frontend fix: correct rare-case bugs from adversarial audit across deadlocks, caching, serialization, memory safety, and Python frontend Jul 30, 2026
@XuehaiPan
XuehaiPan requested a review from Copilot July 30, 2026 16:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR applies a broad adversarial correctness audit across optree’s C++ core and Python frontend, focusing on rare-path failures involving lock ordering, interpreter-scoped caching, treespec serialization safety, GC integration, and broadcasting/ordering edge cases.

Changes:

  • Fixes deadlocks and re-entrancy hazards in the registry and PyTreeIter, including GIL/lock-order inversions and re-entrant next() hangs.
  • Hardens treespec behavior: safer pickling/unpickling validation, namespace merge safety checks, recursion-depth guards, and safer traversal/flattening behavior under adversarial user code.
  • Updates Python utilities and APIs (total_order_sorted, broadcasting/map helpers, accessor codification/equality) and adds extensive regression tests + changelog entries.

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test_utils.py Adds regression coverage for total_order_sorted key-callback error propagation and call counts.
tests/test_typing.py Adds structseq field/accessor tests (incl. CPython unnamed slots) and subprocess tests for interpreter-safe type caches.
tests/test_registry.py Adds subprocess deadlock/regression tests for registry locking, error paths, and namespace lookup precedence.
tests/test_prefix_errors.py Adds regressions for heterogeneous dict keys in error formatting and custom-node entry variability handling.
tests/test_ops.py Adds regressions for dict ordering with mid-sort failures, iterator re-entrancy, flatten safety, list shrinking under is_leaf, zero-leaf partitioning, and broadcasting sentinel behavior.
tests/test_dataclasses.py Adds characterization + regressions for dataclass reconstruction pitfalls, InitVar rejection, guard leakage, and entry indexing.
tests/test_accessors.py Adds regressions for accessor __ne__, entry hashing/equality robustness, codify escaping, and unnamed structseq entry repr.
tests/integrations/test_attrs.py Adds characterization + regressions for attrs reconstruction/converters, guard leakage, and entry indexing.
tests/helpers.py Adds skip marker for deferred type refs on free-threaded builds < 3.14.
tests/concurrent/test_subinterpreters.py Adds subinterpreter churn tests for type-cache correctness and registry init failure rollback/deadlock avoidance.
src/treespec/treespec.cpp Adds namespace rebind guards, recursion-depth protection in broadcasting/walking, and avoids in-place key sorting corruption.
src/treespec/traversal.cpp Adds re-entrant iterator next() detection and strengthens iterator safety.
src/treespec/serialization.cpp Improves treespec repr and significantly hardens pickled-state copying + validation.
src/treespec/richcomparison.cpp Fixes prefix comparison to snapshot subtrees before overlapping permutations.
src/treespec/gc.cpp Adjusts GC traversal/clear semantics for registrations and iterator leaf predicate visibility.
src/treespec/flatten.cpp Fixes dict-order flag reads, avoids exposing partially-filled GC-tracked leaf lists, and hardens flattening behavior.
src/treespec/constructors.cpp Uses consistent dict-order flag snapshotting, improves warning/error handling, and adds namespace rebind guard.
src/registry.cpp Refactors (un)registration to avoid Python execution under locks, adds rollback for init failures, and fixes deadlock-prone ordering.
src/optree.cpp Exposes unnamed-field marker, improves pickle protocol support via explicit __reduce__, and documents pybind11 construction limitations.
optree/utils.py Reworks total_order_sorted to avoid swallowed callback errors, avoid partial in-place sort artifacts, and ensure stable fallbacks.
optree/typing.py Makes type checks more robust, exposes unnamed-field marker, and fixes structseq field mapping (incl. unnamed-slot behavior).
optree/ops.py Fixes zero-leaf transpose/partition behavior, rewrites broadcast helpers to avoid reapplying is_leaf to internal sentinels, and improves docs/types.
optree/integrations/attrs.py Fixes integer entry field selection, improves docs/errors, and prevents guard leakage on failed registrations.
optree/dataclasses.py Adds InitVar rejection, improves docs/errors, and prevents guard leakage on failed registrations.
optree/accessors.py Fixes entry equality/hashing robustness, corrects codify for non-identifiers, adds unnamed-slot handling, and fixes accessor __ne__.
optree/_C.pyi Updates typing: overloads for walk/traverse, broader accepted index types, and exposes unnamed-field marker.
include/optree/treespec.h Adds recursion-depth special-casing for Windows debug/free-threaded, introduces iterator re-entrancy state, and adds helper declarations.
include/optree/registry.h Adds dict-order flag snapshot API, registry status enum, and consistent sizing under a single lock.
include/optree/pytypes.h Introduces WeakKeyCache, safer list access pre-3.13, C-API copy helpers, structseq field mapping by offset, and safer total-order sorting.
include/optree/pymacros.h Adds safe access to PyStructSequence_UnnamedField across Python versions and hardens interpreter-id retrieval.
include/optree/hashing.h Adds hashing/equality for (interpreter_id, handle) cache keys.
docs/source/spelling_wordlist.txt Adds new technical terms used in docs.
CHANGELOG.md Adds detailed “Fixed” entries for the audit findings and their resolutions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/treespec/gc.cpp
Comment on lines +134 to +137
if (self.m_leaf_predicate) [[likely]] {
// Drop the owned leaf predicate reference to break cycles that pass through it.
Py_CLEAR(self.m_leaf_predicate->ptr());
}
Comment on lines +452 to +456
// The keys must be hashable and distinct; a duplicate or unhashable key would
// collapse or fail when the dict is rebuilt, desyncing the keys from the children.
if (DistinctCount(node.node_data) != node.arity) [[unlikely]] {
throw malformed("the keys are not distinct");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cxx Something related to the CXX source code enhancement New feature or request python Something related to the Python source code thread-safety Multi-threading issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants