Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
oschwald
force-pushed
the
greg/stf-1957
branch
from
October 3, 2026 16:17
43f3dfb to
690e7f7
Compare
oschwald
force-pushed
the
greg/stf-1956
branch
from
October 3, 2026 16:17
43a5b53 to
4b4d606
Compare
oschwald
force-pushed
the
greg/stf-1957
branch
from
October 3, 2026 16:38
690e7f7 to
4146d9c
Compare
oschwald
force-pushed
the
greg/stf-1957
branch
from
October 5, 2026 15:32
4146d9c to
cd376ca
Compare
oschwald
force-pushed
the
greg/stf-1956
branch
from
October 5, 2026 15:32
4b4d606 to
3fa4439
Compare
The Reader, Metadata and iterator types are heap types, so each instance holds a reference to its type. The dealloc functions did not release that reference, so each object leaked one reference to its type, and the types were never freed. A second Reader_init did not close the open database, so it leaked. It also left existing iterators with records that point into the old database, so the next step of such an iterator read freed memory. A second init now reopens the reader, as in the pure Python reader. It closes the old database under the write lock before it opens the new one, so a failed open leaves the reader closed. Each open increments a generation count, and an iterator from an older generation raises ValueError. Metadata_init also accepted a second init, which leaked the old field values. Refuse it with a ValueError. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The module defines PY_SSIZE_T_CLEAN, so the y# format reads the length as a Py_ssize_t. ReaderIter_next passed an int, which is undefined behavior on 64-bit platforms: Py_BuildValue reads 8 bytes from a 4-byte argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
from_map built the dict, then returned NULL without releasing it when a key failed to decode, for example a map key that is not valid UTF-8. The sibling path for a failed value already released the dict. The cyclic garbage collector does not free an object with a leaked reference, so each failed lookup kept one empty dict, about 64 bytes. The new test makes 2,000 failed lookups and checks the memory that tracemalloc reports. Before the fix, the lookups kept about 128 KB. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
from_map ignored a failure from PyDict_SetItem, such as a MemoryError while the dict resizes. It then returned the dict with the exception still set, and the caller raised SystemError. Now it releases the dict and returns NULL, so the original exception reaches the caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
from_map read every map key from the utf8_string member of the entry union. libmaxminddb does not check the key type, so a database with a key of another type, such as a uint16, made from_map read an integer as a pointer. The process crashed with a segmentation fault. Raise InvalidDatabaseError for a key that is not a UTF-8 string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
from_entry_data_list passed uint32 values to PyLong_FromLong. On platforms where a C long has 32 bits, such as Windows and 32-bit Linux, a value of 2**31 or more became negative, for example an ASN of 4200000000. The pure Python reader returns the correct value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reader.metadata() returned NULL without releasing the decoded object when it was not a dict. libmaxminddb validates the metadata when it opens the database, so an opened database reaches this path only through a bug, but the refcount handling was still wrong. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reader__exit__ and Reader_dealloc called Reader_close and ignored the result. Each successful close leaked a reference to None, which matters before Python 3.12, where None is not immortal. On free-threaded builds, a failure to take the write lock left an exception set: __exit__ hid it behind a successful return, and dealloc left it pending for unrelated code to find. __exit__ now returns the result of Reader_close. dealloc no longer calls Reader_close. It closes the database directly, without the lock. At a reference count of 0 no other thread can use the reader, because each iterator holds a reference to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Metadata is an immutable value object, but Metadata_init set its fields. That allowed a Metadata with NULL fields, after Metadata.__new__ or a failed init, and a second init. Each state needed its own guard: a reinit check, a critical section on free-threaded builds, and Py_XDECREF in dealloc. Metadata_new now parses the arguments and sets every field, and the type has no tp_init. Each Metadata then has all fields set, and a second __init__ call changes nothing, because object.__init__ ignores the arguments when a type overrides tp_new. Metadata.__new__ with no arguments now raises TypeError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
oschwald
force-pushed
the
greg/stf-1957
branch
from
October 5, 2026 16:29
cd376ca to
8f9492c
Compare
oschwald
force-pushed
the
greg/stf-1956
branch
from
October 5, 2026 16:29
3fa4439 to
19b8ce4
Compare
A second __init__ on the pure Python Reader replaced the buffer, but it did not close the old one. An iterator from before the second init then walked the new database with node numbers from the old one, and returned networks that are not in either database. The C extension now closes the old database and stops such an iterator. Do the same here: __init__ closes the old buffer once the new one is loaded and increments a generation count. An iterator from an older generation raises the same ValueError. A count is needed, because a source can return the same buffer object again: BytesIO.read() does after seek(0). The check is one integer comparison for each node during iteration. Lookups do not change. The reinitialization tests now run for every mode of both readers, so the two keep the same behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReaderIter_next held the read lock while it called ipaddress.ip_network, which runs Python code. That caused two hangs on free-threaded builds: - If the code closed the reader on the same thread, for example from a signal handler, close() waited for the write lock that the thread's own read lock blocked. - If the code ran the garbage collector while another thread waited in close() for the write lock, the collector stopped the world and waited for that thread, which waited for the read lock. The network uses only the iterator's own record, so release the lock after the record is decoded. Now no thread runs Python code while it holds the lock. A SIGALRM handler that closes the reader during iteration, and a wrapped ip_network that calls gc.collect() while another thread calls close(), both hung before this change and work after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReaderIter_next takes records off the iterator's pending list and adds their children while it holds only the shared read lock. On free-threaded builds, two threads that called next() on the same iterator could take the same record and both free it. The process aborted with heap corruption. Hold a critical section on the iterator for each next() call. With the GIL, the list changes already run without interruption. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No CI job ran a free-threaded interpreter, so the reader locks compiled to no-ops in every test run. The tests for the lock lifetime, the lock release in the iterator, and the shared iterator could not fail, and macOS, where a zeroed pthread_rwlock_t is invalid, was never tested. Add a 3.14t tox environment and run it on each CI platform. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
oschwald
force-pushed
the
greg/stf-1957
branch
from
October 5, 2026 17:00
8f9492c to
f3c264c
Compare
oschwald
force-pushed
the
greg/stf-1956
branch
from
October 5, 2026 17:00
19b8ce4 to
0b7589f
Compare
oschwald
force-pushed
the
greg/stf-1956
branch
from
October 5, 2026 17:13
0b7589f to
9a25a09
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes two free-threading bugs in the C extension. Stacked on #466; review only the commits in this PR.
Deadlock.
ReaderIter_nextcalledipaddress.ip_networkwhile it held the read lock. Aclose()from a signal handler on the same thread, or from another thread during a garbage collection that the callback started, waited forever. The lock is now released before any Python code runs.Heap corruption. Two threads that advanced the same iterator could take and free the same pending record. Each
next()call now holds a critical section on the iterator.CI. No job ran a free-threaded interpreter, so the locks compiled to no-ops in every test run. A new
3.14ttox environment runs on each CI platform, including macOS.A subprocess test with a timeout closes the reader from
ip_network, and a free-threaded test shares one iterator between eight threads and checks that each network comes out once. Both fail without the fixes.STF-1957
🤖 Generated with Claude Code