gh-149816: Fix a RC in `_random.Random.__init__` method by sobolevn · Pull Request #149824 · python/cpython
I am not sure that it would be easy to repro / test in a real env, but it can be a potential problem.
Original description from 17.md:
Vulnerability #17
Title: Unlocked init races with PRNG state access
Category: Concurrency and Race Conditions
Tags: race,read,dos
CWEs: CWE-125, CWE-367
CVSS: CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:L/SC:N/SI:N/SA:N
Severity: Low (2.1)
Location: cpython/Modules/_randommodule.c:572:572 in function random_init
Description
random_init reseeds internal MT state by calling random_seed without acquiring the object critical section at cpython/Modules/_randommodule.c:572, while normal methods use Py_BEGIN_CRITICAL_SECTION(self) (e.g. cpython/Modules/clinic/_randommodule.c.h:26, cpython/Modules/clinic/_randommodule.c.h:139). During reseed, init_genrand writes self->index = N at cpython/Modules/_randommodule.c:212. A concurrent reader in genrand_uint32 can pass the bounds check self->index >= N at cpython/Modules/_randommodule.c:143 and then use a raced value at cpython/Modules/_randommodule.c:160, causing mt[624] access.
Trigger Conditions
Pre-conditions:
- CPython is built in free-threaded mode (
Py_GIL_DISABLED); with the GIL enabled, the race is serialized. - The same
_random.Randominstance is shared across threads. - The attacker can cause concurrent calls to
__init__andrandom()/getrandbits()on that same object (directly or through an exposed application API).
Data flow:
- Create
r = _random.Random(0), then advance to a near-boundary index (e.g., callr.getrandbits(32)623 times so index becomes 623). - Thread A calls
r.getrandbits(32)and entersgenrand_uint32; it evaluatesself->index >= Nas false atcpython/Modules/_randommodule.c:143. - Thread B concurrently calls
_random.Random.__init__(r, 1)(orr.__init__(1)), reaching unlockedrandom_initatcpython/Modules/_randommodule.c:572and theninit_genrandsettingself->index = 624atcpython/Modules/_randommodule.c:212. - Thread A resumes and executes
y = mt[self->index++]atcpython/Modules/_randommodule.c:160, reading out-of-bounds elementstate[624].
Impact
In the free-threaded runtime, this race introduces undefined behavior and a concrete out-of-bounds read on PRNG object state. Most severe outcomes are process crash (availability impact) or unintended memory disclosure via corrupted/random output, depending on allocator/layout and timing. Exploitability requires precise concurrency and access to concurrent method invocation on the same object, which lowers reliability but does not remove the bug.
Remediation
- In
random_init()(cpython/Modules/_randommodule.c), wrap therandom_seed(...)call in the same object critical section used by other mutating/accessor methods, so__init__cannot race withrandom(),getrandbits(),getstate(),setstate(), orseed()on the same instance. - Ensure the lock/unlock structure in
random_init()is exception-safe on all return paths (including failures), matching existing critical-section patterns used in generated wrappers. - Keep locking semantics consistent for inherited
tp_inituse as well (the fix should apply regardless of whetherRandomis used directly or via subclasses reusing thistp_init). - Add a free-threaded regression test in
Lib/test(random module tests) that repeatedly racesr.__init__(...)againstr.random()/r.getrandbits(...)on the same object and asserts no crash/UB under stress.
Reproduction
- Build a free-threaded CPython (
Py_GIL_DISABLED) with a sanitizer-enabled/debug configuration (ASAN or UBSAN strongly recommended) so races and out-of-bounds reads are visible instead of silently ignored. - In one process, create a single shared
random.Randominstance and drive it close to the MT boundary state (consume enough outputs soindexis near623), then keep using that same object from multiple threads. - Run two concurrent activities against that same object:
- one thread repeatedly calling
random()orgetrandbits(32), - another thread repeatedly calling
obj.__init__(seed_value)(re-initializing the existing object, not creating a new one).
- one thread repeatedly calling
- Keep both operations running at high iteration counts; if nothing appears immediately, increase contention (more CPU load, more iterations, pinning threads, repeated retries).
- Confirm the issue by observing at least one of:
- sanitizer report indicating a race or out-of-bounds access in
_randommodule.c(notably aroundgenrand_uint32/ seed-init paths), - intermittent crash/abort in free-threaded builds during this concurrent
__init__+random/getrandbitsworkload.
- sanitizer report indicating a race or out-of-bounds access in
Code Context
return random_seed(RandomObject_CAST(self), arg);