Fix crashes found hunting the last open fuzzing record (#8524)

* specialize: check the member descriptor's type before caching its slot offset

The LOAD_ATTR/STORE_ATTR specializations cached the slot offset of any member
descriptor found on the owner's type and then guarded the specialized
instruction on the type version alone, while descr_get()/descr_set() check on
every access that the instance belongs to the type the descriptor was defined
for. A descriptor taken from a wider class and bound to a narrower one read
past the instance's slot array once the cache warmed up:

    class Big:
        __slots__ = ("a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7")
    class Narrow:
        __slots__ = ("z",)
    Narrow.x = Big.__dict__["a7"]
    o = Narrow()
    for _ in range(1000):
        try: o.x
        except TypeError: pass
    # index out of bounds: the len is 1 but the index is 7 (object/core.rs)

A class with no slots at all reached the ext_ref().unwrap() on the same line.

Assisted-by: Claude

* socket: reserve recv()'s buffer fallibly

recv() and recvfrom() handed the caller's bufsize straight to
Vec::with_capacity, so an unreachable size aborted the process through
handle_alloc_error before any syscall was made:

    socket.socket().recv(2**62)
    # memory allocation of 4611686018427387904 bytes failed -> SIGABRT

try_reserve_exact reports MemoryError instead, which is what CPython raises.

Assisted-by: Claude

* types: count the __call__ and __get__ slot dispatches as recursion

Both wrappers re-enter Python without pushing a frame, so nothing counted the
nesting when the special method named the object it was looked up on:

    class C: pass
    c = C(); C.__call__ = c
    c()                                    # native stack overflow, SIGSEGV

    class D: pass
    d = D(); D.__get__ = d; D.x = d
    d.x                                    # the same, through descr_get

with_recursion around the two dispatches raises RecursionError instead, the
way Py_EnterRecursiveCall bounds a tp_call dispatch. It costs about 5% on a
__call__ dispatch and 3% on a __get__ dispatch through these wrappers.

Assisted-by: Claude

* typevar: show a ParamSpecArgs origin by its repr

ParamSpecArgs and ParamSpecKwargs fell back to a Rust `{:?}` of __origin__ when
it had no __name__. That walks the object graph natively through Debug for
PyInner, where no recursion guard sits, so a single repr() of a deeply nested
chain overflowed the native stack:

    a = object()
    for _ in range(30000):
        a = typing.ParamSpecArgs(a)
    repr(a)                                # SIGSEGV

The origin is formatted with its repr now, which is guarded, and a ParamSpec
origin is recognized by its type rather than by carrying a __name__.

Assisted-by: Claude

* Do not hold a lock across a call back into Python

Three places kept a lock while running code that can reach the same object, so
a callback that touched it wedged the process:

    _asyncio.future_add_to_awaited_by(fut, waiter)   # waiter.__hash__ adds again
    select.select(elements, [], [], 0)               # fileno() clears `elements`
    select.poll().poll(1000)                         # SIGALRM handler registers

The future's awaited-by field is read and written under its lock but the set is
built outside it, the list extraction re-reads the list on each step the way
map_iterable_object() does, and poll() waits on a copy of its descriptors. All
three ran forever before and now finish the way they do on CPython.

Assisted-by: Claude

* Validate memoryview.cast() arguments and export negative strides correctly

cast() accepted any struct format and any shape element. A zero-size
format ('0s') and a 0 in the shape both reached a division by zero;
cast() now takes only a native single character format, optionally
'@'-prefixed, and shape elements that are ints greater than zero.

A view with a negative stride starts at its last item, so the bytes it
exported began there and its own offsets walked off the front of them.
Such a view now exports the whole underlying buffer with `start` folded
into the descriptor's offsets, and zip_eq() hands over a whole run only
when both sides are contiguous in the last dimension.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* Charge native recursion to the stack, not to the frame limit

with_recursion() checked the limit sys.setrecursionlimit() sets and
incremented the same counter that pushing a frame does, so a guard on a
native dispatch spent what Python code had left to call with, and did so
where sys._getframe() cannot see it: test.support.get_recursion_available()
reported frames that were no longer there. Py_EnterRecursiveCall bounds
the native stack instead, which is a separate budget, and the C stack
check with_recursion already performs is that bound.

The snippets pinning the guarded paths nest deep enough to reach the
stack rather than the frame limit.

Assisted-by: Claude

* Report a size that cannot be allocated instead of aborting on it

A size taken from Python went straight into an infallible allocation in
several places, so the process aborted through handle_alloc_error before
any exception could be raised:

- str/bytes/bytearray center(), ljust(), rjust() and zfill() reserved
  the padded result for the caller's width
- expandtabs() built its runs of spaces from a tabsize of any width;
  the argument is a C int, and a wider one does not fit
- Buffered{Reader,Writer,Random} allocated buffer_size, and read(),
  read1() and FileIO.read() their read size
- bytes(n) and bytearray(n) allocated n
- pbkdf2_hmac() allocated the derived key length, which is a C int

Each of these now reports MemoryError, or OverflowError where the
argument does not fit the type it is declared with.

new_zeroed_bytes() leaves the zeroing to the allocator, so a large
request costs the pages that are written to rather than all of them.

Assisted-by: Claude

* marshal: answer allow_code where a code object is written or read

allow_code was answered by walking the whole result a second time, with
no depth counter and no record of what it had already seen, so a value
that referred back to itself or nested deeply enough ran off the native
stack. w_object() and r_object() answer it where the code object is,
inside the walk that already bounds its depth and resolves references.

A container length is read the way r_long() reads one: it is signed, so
a length with the top bit set is out of range rather than four billion
items to reserve room for.

load() no longer holds a borrow of the buffer read() returned across the
seek() it makes afterwards.

Assisted-by: Claude

* Do not lock an object while running code that can reach it

Several places held a lock or a borrow of an object across a call back
into Python, so a callback that touched the same object waited on a lock
its own caller was holding:

- memoryview slice assignment read a source overlapping the destination,
  and __setitem__ converted the value while holding the write borrow
- BytesIO.readinto() read into a buffer viewing the same BytesIO
- array.__setitem__ converted the value under the array's write lock,
  and mmap.write() read a source viewing the same map
- bytearray.join() and bytearray.__mod__ drove Python with the
  bytearray borrowed
- array and bytearray answered "is this resizable" after taking the
  write lock, though an export is exactly a borrow someone else holds

A TextIOWrapper cookie now has to name a position inside what was
decoded in characters as well as in bytes; only the byte offset was
checked, and the character count is what read() and tell() index with.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* Stop asserting a pbkdf2 message that depends on the width of a C long

The snippet asserted "key length is too great.", which pbkdf2_hmac()
only reaches once the length has been converted; where a C long is
narrower than the length asked for, the conversion fails first and says
so instead. Both are OverflowError, which is what the case is about.

test_support.test_get_recursion_depth passes now that a native recursion
guard no longer spends frames get_recursion_depth() cannot see.

Assisted-by: Claude

* Publish and read the same pointer for a thread's top frame

set_current_frame() casts the `Py<FrameObject>` it publishes straight to
`*mut FrameObject`, so ThreadSlot::top_frame holds the object's base.
sys._current_frames() read it back through Py::from_payload_ptr(), which
subtracts the payload offset from what it is given. The reference it took
therefore incremented, and later decremented, a word 48 bytes ahead of
the frame -- inside the object allocated before it, whose OnceLock state
word sits exactly there for two frames adjacent in the size class. The
neighbour then read an initialized-looking cold pointer that had never
been written and locked whatever the uninitialized word addressed, so
the thread that owned it crashed rather than the one that read.

The slot now holds `*mut Py<FrameObject>`, which is what both sides mean.

A thread parked in a call has no FrameObject for its topmost frame, so
top_frame is null there and the reader takes the materialize path
instead: test_sys.test_current_frames never reaches the branch. The
snippet takes _current_frames() against threads that are running.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* Decide stop-the-world parking under the thread registry lock

do_suspend() published SUSPENDED first and only then re-read `requested`,
restoring itself to ATTACHED if the stop had ended in the meantime. That
made a thread the second writer able to leave SUSPENDED, so a stop whose
completion check had already observed the thread parked could be undone
behind the requester's back:

  worker    CAS ATTACHED -> SUSPENDED
  requester all_non_requester_suspended() -> true, world_stopped = true
  requester start_the_world(): requested = false, then walks the registry
  worker    reads requested == false, stores ATTACHED

With the store landing inside that walk the debug assertion in
start_the_world fires; with the walk already past the slot, a following
stop force-parks the thread DETACHED -> SUSPENDED, counts it as stopped,
and the store then puts it back to ATTACHED with the world declared
stopped and the thread running bytecode.

`requested` is set in init_thread_countdown() and cleared in
start_the_world() with the registry held, and start_the_world() keeps
holding it while releasing every SUSPENDED thread. Taking the registry
around the check and the transition therefore makes the two orders the
only ones possible: park before that release pass and be woken by it, or
find the request already withdrawn and stay ATTACHED. The requester is
left as the only writer that takes a thread out of SUSPENDED, and the
self-restore is gone.

suspend_if_needed() takes the VirtualMachine to reach the registry.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* Keep an atexit callback alive while it is being compared

atexit.unregister() releases the callback list around each __eq__ call and
identified the entry it had compared by the address of its Box. __eq__ can
call atexit._clear(), which drops that Box, and atexit.register(), whose
new Box lands on the freed allocation; the identity search then matched
the freshly registered callback and removed it.

  atexit.register(a); atexit.register(b); atexit.register(c)
  # __eq__ runs _clear() then register(d), returns True
  atexit.unregister(probe)

left no callbacks registered where CPython leaves d.

Entries are Arc-shared now, so unregister() holds the one it is comparing
and matches it with Arc::ptr_eq: an address cannot be reused while the
comparison that named it is still running.

Assisted-by: Claude

* Hold atexit entries in PyRc rather than Arc

PyObjectRef is Send and Sync only under the threading feature, so an Arc
over a callback entry trips clippy::arc_with_non_send_sync in builds
without it, such as the wasm package. PyRc is Arc there and Rc otherwise.

Assisted-by: Claude

* Do not hold a buffer's storage while waiting for a peer

FileIO.readinto() and socket.recv_into()/recvfrom_into() took the target
buffer's write borrow and kept it for the whole call, including the wait
for data a pipe, socket or terminal may never deliver. What CPython holds
across that wait is the export, which only forbids resizing; the borrow is
a lock every other thread touching the same object waits on, so

    threading.Thread(target=lambda: sock.recv_into(buf)).start()
    len(buf)

did not answer until the peer sent. A thread parked on that lock is
ATTACHED and never reaches a safepoint, so gc.collect() in a third thread
waited for the peer as well: one incidental read of the buffer stopped the
world from being stopped at all.

The wait now runs against storage of its own and the bytes are copied over
once they arrive, with the export held throughout so the target still
cannot be resized meanwhile. A seekable file answers from itself rather
than from a peer, so FileIO.readinto() writes into the target directly
there and the buffered read path is unchanged.

Assisted-by: Claude

* Do not hold a buffer's storage while waiting to hand it over

socket.send()/sendall()/sendto()/sendmsg() and FileIO.write() kept the
source buffer's read borrow for the whole call, including the wait for a
peer that may never make room. That borrow is a lock every other thread
writing to the same object waits on, so

    threading.Thread(target=lambda: sock.sendall(buf)).start()
    buf[0] = 1

did not return until the peer read; and a thread parked there is ATTACHED
and never reaches a safepoint, so gc.collect() in a third thread waited
for the peer too -- the same wedge readinto() had on the receiving side.

ArgBytesLike::borrow_buf_unlocked() answers with bytes that survive the
borrow being dropped. An immutable object hands out a plain reference and
locks nothing, so those are sent where they lie and bytes and memoryviews
over them cost nothing; only bytes reached through a lock are copied out
first. The export is held throughout either way, so the source still
cannot be resized while it is being sent.

The regression snippet covers both directions now and is renamed for it.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* Check select()'s descriptor limit while the sequence is walked

seq2set() collected the whole sequence and compared the result's length
against FD_SETSIZE afterwards. Selectable::try_from_object() calls
fileno(), which runs Python and can append to the list being walked, and
the walk re-reads the list on every step, so the collection had no end to
reach and the comparison was never made. seq2set in Modules/selectmodule.c
checks the count per element instead.

stdlib_select.py gains a fileno() that appends to its own list, and
releases its sockets with close() rather than by dropping the name.

Assisted-by: Claude

* Bound native recursion where the C stack cannot be measured

check_c_stack_overflow() answers no unconditionally under miri and on
musl, where the stack pointer is not read. Since 9c9905aff that check is
all with_recursion() does, so every guard placed on native recursion --
__call__ and __get__ dispatch among them -- was a no-op on those targets
and the nesting ran until the stack ran out.

with_recursion() now counts its own depth on those targets and refuses
past NATIVE_RECURSION_LIMIT_UNMEASURED. The count is separate from the
frame limit sys.setrecursionlimit() sets, and compiles away where the
stack pointer can be read.

Assisted-by: Claude

* Report the failure to allocate pbkdf2's key and Take.readinto's scratch

Both buffers are sized from an argument -- pbkdf2_hmac's dklen accepts up
to i32::MAX, and readinto's from the length of the destination -- and were
built with vec![0u8; n], which aborts the process on allocation failure.
new_zeroed_bytes() raises MemoryError instead.

Assisted-by: Claude

* Read the frozen-code tuple length as a signed length

The '(' branches in read_marshal_str_vec() and read_marshal_const_tuple()
took the length with read_u32() as usize, so a value with the top bit set
read as four billion items rather than as out of range. read_len() is what
every other length in this file goes through, and it reinterprets as i32.

Both readers serve deserialize_code(), which reads only the frozen modules
baked in at build time, so this changes no reachable behavior;
marshal.loads() already went through read_len().

Assisted-by: Claude

* Make the snippets from the fuzzer sweep assert what they check

builtin_str.py expanded a tab to 2**31-1 columns, allocating 2 GiB to
observe that the width is accepted; a string with no tab observes the same
acceptance without laying anything out.

stdlib_array.py caught the refusal of frombytes() on its own exported
buffer and passed silently when nothing was raised. stdlib_hashlib.py
used a bare `assert False` as its failure branch. Both now say so through
the same shapes the other snippets use.

stdlib_socket.py also accepts OverflowError from recv() with a size that
does not fit the platform's C int.

stdlib_threading_current_frames.py indexed the frame chain for "f123"
without first asserting it is there.

Assisted-by: Claude

* Ask for a marshal container's room instead of assuming it

A flagged tuple or list is published in the reference table before its
children are read, and the placeholder was built with vec![none; len].
The length is the input's to choose and read_len() lets it reach
i32::MAX, so marshal.loads(b"\xa8\xff\xff\xff\x7f") -- five bytes -- asks
for 17 GB of element slots and aborts the process where the allocator
cannot serve it. r_object() allocates the container up front too, but
PyTuple_New() reports what it cannot get.

The elements are now reserved with try_reserve_exact() and a refusal is
raised as MemoryError through the decoder's pending-error channel.
PyTuple::new_marshal_placeholder() held nothing but that allocation and
is gone; the caller builds the elements and uses new_ref().

Assisted-by: Claude

* Compare the blocking-buffer snippet against something

measure() asserted len(buf) == len(buf), which holds whatever the length
is; it now takes the length the caller expects. The pipe case compared
the drained total against the whole source, while an unbuffered write()
reports only what it transferred and a signal can cut that short; it now
compares against what write() returned.

Assisted-by: Claude

* Refuse array.frombytes() a source whose items are not bytes

frombytes() reads its argument as bytes, but accepted any contiguous
buffer, so array("i").frombytes(memoryview(array("d", [1.0]))) appended a
double's bytes read as ints instead of raising. array_array_frombytes_impl
requires an itemsize of 1; ArgBytesLike now reports the itemsize so the
same check can be made here.

The BufferError that a resize meets while the array is exported also names
the array rather than repeating bytearray's wording.

stdlib_array.py's frombytes-of-itself case used typecode "i", where the
new check answers before the resize guard is reached; it uses "b" so the
guard is what refuses, and asserts the wider case separately.

Assisted-by: Claude

* Tell apart the ways marshal data can be bad

A type byte no reader knows, a back reference that names nothing, and
TYPE_NULL all came out as a bare "bad marshal data" ValueError. r_object()
answers the first two with "bad marshal data (unknown type code)" and
"bad marshal data (invalid reference)", and read_object() answers the
third with a TypeError, "NULL object in marshal data for object", since
TYPE_NULL stands for no object rather than for a value.

MarshalError gains the three cases and deserialize_value() maps them.
The container-specific wording r_object() uses for a NULL read inside a
tuple or list is not reproduced; the exception type is.

Assisted-by: Claude

* Hash the collector's tables by address rather than by SipHash

A collection keys three sets and two maps by object address, and it visits
every tracked object and every edge between them, so the hashing is a
per-edge cost. Those tables used the default RandomState, whose SipHash
buys resistance against a caller choosing colliding keys -- and nothing
chooses these keys: they are addresses this process handed out into tables
that live and die inside one collection. A profile of gc.collect() over a
423k-object heap spent 45% of its samples in SipHash.

They now hash with a splitmix64 finalizer. The shifts matter: a table
picks its bucket from the low bits and an address arrives with those bits
zeroed by alignment, so a plain multiply leaves every object in a handful
of buckets and is slower than SipHash was.

The reachability walk also copied each object's referent vector out of the
map it was cached in, a second pass over every edge; it reads them in
place, and reference subtraction hands its vector to the map instead of
cloning it.

Measured over 423k live objects: 0.93s to 0.15s. Over 843k dead ones:
3.00s to 0.79s. extra_tests/snippets/stdlib_threading_gc_import.py, whose
collector thread calls gc.collect() in a loop, ran anywhere from 2.7s to
28s and now runs in 3.1-3.5s: a collection that takes longer leaves more
garbage for the next one to walk, so the cost fed back on itself.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* Keep the collection's candidates and their counts in one table

A collection built a set of candidates and, beside it, a map from the same
addresses to their reference counts. Both were probed for every edge in
the heap -- membership from the set, the count from the map -- so each
edge paid to hash the same address twice, and each candidate paid to be
inserted twice. The map alone answers both questions.

The candidates also keep a walkable order now, which the reference
subtraction pass needs since it writes the counts while reading the
candidates, and which the unreachable set is built from instead of a
set difference.

Over the 423k-object heap measured in the previous commit: 0.15s to
0.13s live, and 0.79s to 0.49s dead.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* gc: collect referents into one buffer instead of a vector per object

Step 3 allocated a `Vec` for every tracked object to hold its referents
and kept them all in a map until step 4 read them back. The referents now
go into a single growing buffer, with the map holding each object's range
into it.

Adds `PyObject::gc_extend_referent_ptrs`, which appends to a caller's
buffer; `gc_get_referent_ptrs` calls it with a fresh one.

Assisted-by: Claude

* memoryview and struct: match the checks and errors of the reference

memoryview:
- `cast()` accepted a source and destination that are both item types,
  which reinterprets the items rather than re-dividing the bytes; one side
  now has to be a byte format.
- A cast to `shape=()` returned without checking that the buffer holds
  exactly the one item that shape describes.
- `hash()` hashes the bytes, so it now raises ValueError for a view whose
  items are not bytes, rather than returning a hash that disagrees with the
  value the view compares equal to.
- `tobytes()` takes the `order` argument, with 'F' walking a
  multidimensional view down its columns; `BufferDescriptor` gained
  `for_each_segment_fortran` for that walk.

struct:
- A value the format has no room for reported "argument out of range"
  instead of naming the format and its range. The format character is now
  passed to the packing functions to report it.
- `Struct.__new__` no longer reads the format; `__init__` does, so
  `__init__` can be called again and a subclass can pass the format up.
  Methods raise RuntimeError until it has run, and `Struct` is a base type.

Removes the expectedFailure from test_Struct_reinitialization and
test_struct_subclass_instantiation.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* io: decide the readinto path by file type, not by seekability

FileIO.readinto wrote straight into the caller's buffer, holding its write
borrow, when the fd was seekable; otherwise it read aside into scratch and
copied. Seekability stood in for "this read answers without waiting on a
peer", which a pipe on Windows breaks: lseek on one succeeds, so the pipe
took the borrow-holding path and every other thread touching that bytearray
waited for the peer.

host_io::reads_without_waiting answers it directly -- seekability elsewhere,
GetFileType() == FILE_TYPE_DISK on Windows.

The regression snippet times each operation separately, so a failure names
the one that waited; it asserts the transfer is still in flight before
checking the export; and the socket case fills the connection until it
refuses rather than assuming a size that outruns it, which SO_SNDBUF on an
already-connected pair does not settle.

Assisted-by: Claude
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-08-17 00:33:27 +09:00
1 parent c603eb7c9a
commit a78d0eb577
56 files changed
+2145 -500

No files matched your search

+19
View File
@@ -747,3 +747,22 @@ assert not s.istitle(), f"{s}"
assert "123A".istitle(), f"{s}"
assert not "123a".istitle(), f"{s}"
assert not "123A\ta".istitle(), f"{s}"
def test_huge_size():
# sizes that cannot be allocated are MemoryError, not an aborted process
for factory in (bytes, bytearray):
assert_raises(MemoryError, lambda factory=factory: factory(2**62))
for meth in ("center", "ljust", "rjust", "zfill"):
assert_raises(
MemoryError,
lambda factory=factory, meth=meth: getattr(factory(b"a"), meth)(
1 << 62
),
)
assert_raises(
OverflowError, lambda factory=factory: factory(b"\ta").expandtabs(2**31)
)
test_huge_size()
+3 -2
View File
@@ -35,9 +35,10 @@ with assert_raises(TypeError):
# slot dispatch is what recurses, so that is where the depth is checked.
if sys.implementation.name == "rustpython":
# CPython, which also runs this snippet, survives this depth unguarded.
# Deep enough to reach the native stack guard; CPython, which also runs
# this snippet, dies on the same value.
deep_tuple = ()
for _ in range(sys.getrecursionlimit() * 2):
for _ in range(100_000):
deep_tuple = (deep_tuple,)
with assert_raises(RecursionError):
hash(deep_tuple)
+161
View File
@@ -514,3 +514,164 @@ def test_fortran_contiguity():
test_fortran_contiguity()
def test_cast_arguments():
# cast() takes a native single character format, optionally '@'-prefixed;
# a zero-size format used to reach a division by zero.
assert memoryview(b"abcd").cast("@i").itemsize == 4
for fmt in ("0s", "4s", "<i", "", "ss"):
assert_raises(ValueError, lambda fmt=fmt: memoryview(b"abcd").cast(fmt))
# every element of shape is an int > 0; a 0 used to divide by zero while
# checking the product against SSIZE_MAX
for shape in ([0], [0, 4], [4, 0], [-1, 4], [0, 0]):
assert_raises(
ValueError, lambda shape=shape: memoryview(b"abcd").cast("B", shape)
)
class Index:
def __index__(self):
return 4
for shape in ([2.0, 2], [Index()], ["4"]):
assert_raises(
TypeError, lambda shape=shape: memoryview(b"abcd").cast("B", shape)
)
assert memoryview(b"abcd").cast("B", [True, 4]).tolist() == [[97, 98, 99, 100]]
test_cast_arguments()
def test_negative_stride():
# A reversed view starts at its last byte, so walking it from there runs
# off the front of the exported slice.
assert memoryview(b"dcba") == memoryview(b"abcd")[::-1]
assert memoryview(b"abcd")[::-1] == memoryview(b"dcba")
assert not memoryview(b"abcd") == memoryview(b"abcd")[::-1]
b = bytearray(b"____")
memoryview(b)[0:4] = memoryview(b"abcd")[::-1]
assert b == bytearray(b"dcba"), b
a = array.array("i", [1, 2, 3])
assert memoryview(array.array("i", [3, 2, 1])) == memoryview(a)[::-1]
assert memoryview(a)[::-1].tolist() == [3, 2, 1]
test_negative_stride()
def test_write_through_same_object():
# Reading the source and writing the destination lock the same object
# when they overlap, and converting a value runs Python that can reach it.
b = bytearray(b"abcd")
memoryview(b)[0:4] = b
assert b == bytearray(b"abcd"), b
b = bytearray(b"abcd")
memoryview(b)[0:4] = memoryview(b)[::-1]
assert b == bytearray(b"dcba"), b
b = bytearray(b"abcd")
memoryview(b)[0:2] = memoryview(b)[2:4]
assert b == bytearray(b"cdcd"), b
b = bytearray(b"abcd")
view = memoryview(b)
class Index:
def __index__(self):
view[1] = 66
return 65
view[0] = Index()
assert b == bytearray(b"ABcd"), b
test_write_through_same_object()
def test_cast_between_non_byte_formats():
# A cast re-divides bytes into items; going from one item type straight to
# another would reinterpret what is already there.
view = memoryview(b"abcd").cast("i")
for fmt in ("h", "i", "f"):
try:
view.cast(fmt)
except TypeError as e:
assert "cannot cast between two non-byte formats" in str(e), e
else:
raise AssertionError(f"expected TypeError for cast to {fmt!r}")
# Either side being bytes is allowed.
assert view.cast("B").tolist() == [97, 98, 99, 100]
assert view.cast("b").format == "b"
assert view.cast("c").tolist() == [b"a", b"b", b"c", b"d"]
assert memoryview(b"abcd").cast("c").cast("i").format == "i"
def test_cast_to_zero_dim():
# A zero-dimensional view holds exactly one item, so the buffer has to be
# that one item and no more.
assert memoryview(b"abcd").cast("I", shape=()).tobytes() == b"abcd"
assert memoryview(b"a").cast("B", shape=()).tobytes() == b"a"
for source, fmt in ((b"abcd", "B"), (b"abcdefgh", "I"), (b"ab", "b")):
try:
memoryview(source).cast(fmt, shape=())
except TypeError as e:
assert "product(shape) * itemsize != buffer size" in str(e), e
else:
raise AssertionError(f"expected TypeError for {source!r} as {fmt!r}")
def test_hash_restricted_to_byte_formats():
# The hash is over the bytes, so it agrees with the hash of those bytes
# only where an item is a byte.
data = b"abcdefgh"
assert hash(memoryview(data)) == hash(data)
assert hash(memoryview(data).cast("c")) == hash(data)
assert hash(memoryview(data).cast("b")) == hash(data)
for fmt in ("I", "i", "h", "d"):
try:
hash(memoryview(data).cast(fmt))
except ValueError as e:
assert "hashing is restricted to formats" in str(e), e
else:
raise AssertionError(f"expected ValueError for format {fmt!r}")
def test_tobytes_order():
view = memoryview(b"abcdefgh")
for order in (None, "C", "F", "A"):
assert view.tobytes(order=order) == b"abcdefgh", order
# A multidimensional view is laid out C-contiguously, so a Fortran-ordered
# copy walks it down the columns instead.
grid = memoryview(b"abcdefgh").cast("B", shape=(2, 4))
assert grid.tolist() == [[97, 98, 99, 100], [101, 102, 103, 104]]
assert grid.tobytes() == b"abcdefgh"
assert grid.tobytes(order="C") == b"abcdefgh"
assert grid.tobytes(order="A") == b"abcdefgh"
assert grid.tobytes(order="F") == b"aebfcgdh"
cube = memoryview(b"abcdefgh").cast("B", shape=(2, 2, 2))
assert cube.tobytes(order="F") == b"aecgbfdh"
for order in ("Z", "c", "f", ""):
try:
view.tobytes(order=order)
except ValueError as e:
assert str(e) == "order must be 'C', 'F' or 'A'", e
else:
raise AssertionError(f"expected ValueError for order {order!r}")
test_cast_between_non_byte_formats()
test_cast_to_zero_dim()
test_hash_restricted_to_byte_formats()
test_tobytes_order()
+15
View File
@@ -900,3 +900,18 @@ assert id(b) != id(b * 0)
assert id(b) != id(b * 1)
assert id(b) != id(1 * b)
assert id(b) != id(b * 2)
def test_huge_width():
# A width that cannot be allocated is a MemoryError, not an aborted
# process, and a tabsize wider than a C int does not fit at all.
for meth in ("center", "ljust", "rjust", "zfill"):
assert_raises(MemoryError, lambda meth=meth: getattr("a", meth)(1 << 62))
assert_raises(OverflowError, lambda: "\ta".expandtabs(1 << 62))
assert_raises(OverflowError, lambda: "\ta".expandtabs(2**31))
# The widest tabsize that still fits is accepted. With no tab to expand
# there is nothing to lay out, so the width is never allocated.
assert "a".expandtabs(2**31 - 1) == "a"
test_huge_width()
+30
View File
@@ -687,3 +687,33 @@ def foo():
code = compile(stmts, "<test>", "exec")
assert code.co_names == ("blah", "foo")
# A slot descriptor carries the layout it was defined for. Reached from another
# class, it has to report that rather than read the slot at its own offset,
# whether the access is fresh or has been seen often enough to be specialized.
class WideSlots:
__slots__ = ("s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7")
class NarrowSlots:
__slots__ = ("only",)
class NoSlots:
__slots__ = ()
NarrowSlots.borrowed = WideSlots.__dict__["s7"]
NoSlots.borrowed = WideSlots.__dict__["s7"]
for owner in (NarrowSlots(), NoSlots()):
for _ in range(1000):
with assert_raises(TypeError):
owner.borrowed
with assert_raises(TypeError):
owner.borrowed = 1
with assert_raises(TypeError):
del owner.borrowed
+33
View File
@@ -11,3 +11,36 @@ foo = Foo()
# Since the default __str__ implementation calls __repr__ and __repr__ is
# actually __str__, str(foo) should raise a RecursionError.
assert_raises(RecursionError, str, foo)
# A __call__ that is the object being called dispatches through the call slot
# again, and none of that pushes a Python frame.
class Caller:
pass
caller = Caller()
Caller.__call__ = caller
assert_raises(RecursionError, caller)
# The same shape through the descriptor protocol: resolving the attribute
# fetches __get__, which is the descriptor itself.
class Descr:
pass
descr = Descr()
Descr.__get__ = descr
Descr.x = descr
try:
descr.x
except (RecursionError, TypeError):
# RecursionError here, TypeError from the call of a non-callable elsewhere
pass
else:
raise AssertionError("descr.x should not resolve")
+33
View File
@@ -143,3 +143,36 @@ class _ReenteringWriter:
arr = array("b", range(128))
arr.tofile(_ReenteringWriter(arr))
assert len(arr) == 129
def test_setitem_reentrant():
# Converting the value runs Python, which can reach the array, so the
# array is not locked while it happens.
a = array("i", [1, 2, 3])
class Index:
def __index__(self):
a[1] = 9
return 7
a[0] = Index()
assert a == array("i", [7, 9, 3]), a
test_setitem_reentrant()
def test_frombytes_of_itself():
# Resizing is refused while a buffer is exported, before any lock is taken.
# The typecode is "b" so the view's items are bytes and the resize is what
# the call is refused for.
a = array("b", [1, 2, 3])
m = memoryview(a)
with assert_raises(BufferError):
a.frombytes(m)
del m
# A view of wider items is not a source of bytes at all.
wide = array("i", [1, 2, 3])
with assert_raises(TypeError):
wide.frombytes(memoryview(wide))
+27
View File
@@ -72,4 +72,31 @@ finally:
asyncio.InvalidStateError = saved_invalid_state_error
asyncio.exceptions.InvalidStateError = saved_invalid_state_error
# The awaited-by set is built with the waiter's __hash__, which can come back
# to the same future; the field must not be locked while that runs.
class Reentrant:
def __hash__(self):
_asyncio.future_add_to_awaited_by(awaited, Reentrant())
return 1
def __eq__(self, other):
return self is other
awaited = _asyncio.Future(loop=object())
_asyncio.future_add_to_awaited_by(awaited, Reentrant())
with assert_raises(RecursionError):
# converting the single waiter into a set hashes both of them
_asyncio.future_add_to_awaited_by(awaited, Reentrant())
plain = _asyncio.Future(loop=object())
waiter = object()
_asyncio.future_add_to_awaited_by(plain, waiter)
_asyncio.future_add_to_awaited_by(plain, object())
assert waiter in plain._asyncio_awaited_by
_asyncio.future_discard_from_awaited_by(plain, waiter)
assert waiter not in plain._asyncio_awaited_by
print("ok")
+101
View File
@@ -0,0 +1,101 @@
"""atexit.unregister() compares callbacks with arbitrary Python code.
The comparison runs with the callback list unlocked, so __eq__ may clear it
and register something new. unregister() then has to tell whether the entry
it compared is still there, and must not mistake a later registration that
happens to occupy the same storage for that entry.
"""
import atexit
def make(name):
def f():
ran.append(name)
f.tag = name
return f
ran = []
a, b, c, d = (make(n) for n in "abcd")
class Probe:
def __init__(self, action=None, result=True):
self.action = action
self.result = result
self.seen = []
def __eq__(self, other):
self.seen.append(getattr(other, "tag", "?"))
if self.action is not None:
self.action()
return self.result
def remaining():
del ran[:]
atexit._run_exitfuncs()
return list(ran)
# A callback the probe does not match is left alone.
atexit._clear()
atexit.register(a)
atexit.register(b)
probe = Probe(result=False)
atexit.unregister(probe)
assert probe.seen == ["a", "b"], probe.seen
assert remaining() == ["b", "a"], ran
# Matching callbacks are dropped, oldest compared first.
atexit._clear()
atexit.register(a)
atexit.register(b)
atexit.register(c)
probe = Probe(result=True)
atexit.unregister(probe)
assert probe.seen == ["a", "b", "c"], probe.seen
assert atexit._ncallbacks() == 0
assert remaining() == [], ran
# __eq__ empties the list: there is nothing left to drop.
atexit._clear()
atexit.register(a)
atexit.register(b)
probe = Probe(action=atexit._clear, result=True)
atexit.unregister(probe)
assert probe.seen == ["a"], probe.seen
assert remaining() == [], ran
# __eq__ empties the list and registers a replacement. The replacement is a
# different callback, so it survives however its storage was reused.
atexit._clear()
atexit.register(a)
atexit.register(b)
atexit.register(c)
def replace():
atexit._clear()
atexit.register(d)
probe = Probe(action=replace, result=True)
atexit.unregister(probe)
assert probe.seen == ["a", "d"], probe.seen
assert remaining() == ["d"], ran
# __eq__ registers without clearing: every entry the walk had already passed
# stays, and so does each newly registered one.
atexit._clear()
atexit.register(a)
atexit.register(b)
probe = Probe(action=lambda: atexit.register(c), result=True)
atexit.unregister(probe)
assert probe.seen == ["a", "c"], probe.seen
assert remaining() == ["c", "c", "b", "a"], ran
atexit._clear()
print("ok")
+8
View File
@@ -2,6 +2,8 @@ import _md5
import _sha1
import hashlib
from testutils import assert_raises
# print(hashlib.md5)
h = hashlib.md5()
h.update(b"a")
@@ -56,3 +58,9 @@ assert (
assert _md5.md5(b"").hexdigest() == "d41d8cd98f00b204e9800998ecf8427e"
assert _sha1.sha1(b"").hexdigest() == "da39a3ee5e6b4b0d3255bfef95601890afd80709"
# a derived key wider than a C int does not fit, and never gets allocated.
# Which OverflowError comes out depends on the width of a C long: where it is
# narrower than the length asked for, converting the argument fails first.
with assert_raises(OverflowError):
hashlib.pbkdf2_hmac("sha256", b"password", b"salt", 1, 2**62)
+45
View File
@@ -197,3 +197,48 @@ expect_value_error(
f"cannot fit '{truncated_non_ascii_type_name}' into an index-sized integer",
lambda: setattr(textio, "_CHUNK_SIZE", NonAsciiNamedChunkSize()),
)
# A buffer size or read size that cannot be allocated is a MemoryError, not an
# aborted process.
assert_raises(MemoryError, lambda: BufferedReader(BytesIO(b"a"), buffer_size=2**62))
assert_raises(MemoryError, lambda: BufferedReader(BytesIO(b"a")).read(2**62))
assert_raises(MemoryError, lambda: BufferedReader(BytesIO(b"a")).read1(2**62))
def _text_cookie(
start_pos=0,
dec_flags=0,
bytes_to_feed=0,
chars_to_skip=0,
need_eof=0,
bytes_to_skip=0,
):
packed = (
start_pos.to_bytes(8, "little", signed=True)
+ dec_flags.to_bytes(4, "little", signed=True)
+ bytes_to_feed.to_bytes(4, "little", signed=True)
+ chars_to_skip.to_bytes(4, "little", signed=True)
+ bytes([need_eof])
+ bytes_to_skip.to_bytes(4, "little", signed=True)
)
return int.from_bytes(packed, "little")
# A cookie names a position both in characters and in bytes, and everything
# read back from it indexes what was decoded, so a position past the end is
# refused rather than stored.
for _bad in (
_text_cookie(bytes_to_feed=10, chars_to_skip=1000, bytes_to_skip=0),
_text_cookie(bytes_to_feed=10, chars_to_skip=100000, bytes_to_skip=3),
_text_cookie(bytes_to_feed=10, chars_to_skip=1, bytes_to_skip=1000),
):
_textio = TextIOWrapper(BytesIO(b"hello world " * 20), encoding="utf-8")
_textio.read(1)
try:
_textio.seek(_bad)
except (OSError, OverflowError):
pass
else:
assert _textio.read(50) is not None
_textio.tell()
@@ -0,0 +1,176 @@
"""Transfers that wait for a peer must not hold the buffer they were given.
A pipe or a socket answers when the other end does, which may be never. The
buffer is exported for the whole call, so it cannot be resized meanwhile, but
nothing else about it changes: another thread can still read it, write to it,
and the interpreter can still stop the world. An implementation that holds the
buffer's storage for the duration of the wait takes all of that away, and a
thread parked on that storage never reaches a safepoint, so a collection that
wants every thread stopped ends up waiting for the peer too.
"""
import gc
import os
import socket
import threading
import time
# The peer acts after DELAY; the checks below have to finish well inside it.
DELAY = 1.0
SLACK = DELAY / 2
def measure(buf, expected_len, writable):
"""Time each operation on `buf` that does not need the peer, separately, so
a failure names the one that waited rather than the group."""
elapsed = {}
def timed(name, operation):
start = time.monotonic()
value = operation()
elapsed[name] = time.monotonic() - start
return value
assert timed("len", lambda: len(buf)) == expected_len, len(buf)
assert isinstance(timed("bytes", lambda: bytes(buf)), bytes)
if writable:
timed("setitem", lambda: buf.__setitem__(0, buf[0]))
timed("gc.collect", gc.collect)
return elapsed
def run(buf, blocking_call, release_peer, writable):
started = threading.Event()
expected_len = len(buf)
result = []
def transfer():
started.set()
result.append(blocking_call(buf))
def peer():
time.sleep(DELAY)
release_peer()
threads = [threading.Thread(target=transfer), threading.Thread(target=peer)]
for t in threads:
t.start()
started.wait()
time.sleep(0.2) # the transfer is now waiting on its peer
elapsed = measure(buf, expected_len, writable)
waited = ["%s %.2fs" % item for item in elapsed.items() if item[1] >= SLACK]
assert not waited, "waited on the peer: " + ", ".join(waited)
# The transfer is still in flight, so its export is still held and the
# buffer cannot be resized. An operating system that took the whole
# transfer without a peer leaves nothing here to observe.
assert not result, "the transfer finished without its peer"
try:
buf.append(0)
except BufferError:
pass
else:
raise AssertionError("append during an export should raise BufferError")
for t in threads:
t.join()
return result[0]
# --- reading: the buffer is written into, so nothing else may touch it at all
read_fd, write_fd = os.pipe()
pipe = open(read_fd, "rb", buffering=0)
try:
target = bytearray(16)
n = run(target, pipe.readinto, lambda: os.write(write_fd, b"pipe"), writable=False)
assert n == 4, n
assert bytes(target[:4]) == b"pipe", bytes(target)
finally:
pipe.close()
os.close(write_fd)
if hasattr(socket, "socketpair"):
left, right = socket.socketpair()
try:
target = bytearray(16)
n = run(target, left.recv_into, lambda: right.send(b"socket"), writable=False)
assert n == 6, n
assert bytes(target[:6]) == b"socket", bytes(target)
finally:
left.close()
right.close()
# --- writing: the buffer is only read, so it stays writable meanwhile
read_fd, write_fd = os.pipe()
sink = open(write_fd, "wb", buffering=0)
try:
# More than any pipe will hold, so the write cannot finish on its own.
source = bytearray(4 * 1024 * 1024)
drained = []
def drain():
with open(read_fd, "rb", buffering=0) as f:
while True:
chunk = f.read(1 << 16)
if not chunk:
break
drained.append(len(chunk))
reader = threading.Thread(target=drain, daemon=True)
# One unbuffered write() reports what it transferred, which a signal can
# cut short, so the reader is measured against that rather than the source.
written = run(source, sink.write, reader.start, writable=True)
sink.close()
reader.join()
assert sum(drained) == written, (sum(drained), written)
finally:
if not sink.closed:
sink.close()
if hasattr(socket, "socketpair"):
left, right = socket.socketpair()
try:
# How much a connection holds before it makes the sender wait is the
# operating system's to decide, and asking for a small send buffer does
# not settle it -- a socketpair is already connected, and on Windows it
# is a loopback pair whose receiver has a window of its own. So fill it
# until it refuses rather than guess a size that outruns it.
left.setblocking(False)
filled = 0
while True:
try:
filled += left.send(bytes(1 << 16))
except (BlockingIOError, InterruptedError):
break
left.setblocking(True)
source = bytearray(1 << 16)
received = []
def receive():
wanted = filled + len(source)
while sum(received) < wanted:
chunk = right.recv(1 << 16)
if not chunk:
break
received.append(len(chunk))
reader = threading.Thread(target=receive, daemon=True)
run(source, left.sendall, reader.start, writable=True)
reader.join()
assert sum(received) == filled + len(source), (
sum(received),
filled,
len(source),
)
finally:
left.close()
right.close()
print("ok")
@@ -106,3 +106,11 @@ if __name__ == "__main__":
test_05()
test_06()
test_07()
# Reading into a buffer that views this same object locks it twice unless the
# read finishes first.
_bio = BytesIO(b"x" * 60)
assert _bio.readinto(_bio.getbuffer()) == 60
_bio = BytesIO(b"x" * 60)
assert _bio.readinto(memoryview(_bio.getbuffer())) == 60
+58
View File
@@ -96,5 +96,63 @@ class MarshalTests(unittest.TestCase):
self.assertIs(loaded_code.co_consts[0], loaded_shared)
class AllowCodeTests(unittest.TestCase):
"""allow_code is answered where a code object is written or read, so a
graph that walks back on itself is not a second walk of its own."""
def test_recursive_value(self):
recursive = []
recursive.append(recursive)
loaded = marshal.loads(
marshal.dumps(recursive, allow_code=False), allow_code=False
)
self.assertIs(loaded[0], loaded)
def test_too_deeply_nested(self):
nested = []
for _ in range(100_000):
nested = [nested]
with self.assertRaises(ValueError):
marshal.dumps(nested, allow_code=False)
def test_code_is_rejected(self):
code = compile("1", "", "exec")
for value in (code, [code], (code,), {0: code}):
with self.assertRaises(ValueError):
marshal.dumps(value, allow_code=False)
data = marshal.dumps(value)
with self.assertRaises(ValueError):
marshal.loads(data, allow_code=False)
class BadDataTests(unittest.TestCase):
def test_container_size_out_of_range(self):
import struct
# a length is signed, so the top bit set is out of range rather than
# four billion items to reserve room for
for marker in b"([<>":
data = bytes([marker | 0x80]) + struct.pack("<I", 0xFFFFFF00)
with self.assertRaises(ValueError):
marshal.loads(data)
def test_unknown_type_code(self):
for data in (b"\x00", b"\x01", b"?", b"\xff"):
with self.assertRaises(ValueError):
marshal.loads(data)
def test_null_object(self):
# TYPE_NULL stands for no object, which is not a value to hand back
with self.assertRaises(TypeError):
marshal.loads(b"0")
def test_invalid_reference(self):
import struct
for index in (0, 0xFFFFFFFF):
with self.assertRaises(ValueError):
marshal.loads(b"r" + struct.pack("<I", index))
if __name__ == "__main__":
unittest.main()
+72 -1
View File
@@ -1,4 +1,5 @@
import select
import signal
import socket
import sys
@@ -72,8 +73,78 @@ if sys.platform != "win32":
max_fd = sock.fileno()
max_fd_sock = sock
assert_raises(ValueError, select.select, [max_fd_sock], [], [], 0)
for sock in sockets:
sock.close()
del sockets
a, b = socket.socketpair()
# CPython disallows this on *nix systems too.
assert_raises(ValueError, select.select, [a] * TOO_MANY_SELECT_FDS, [], [], 0)
del a, b
a.close()
b.close()
# fileno() runs while the sequence is being read, and it can mutate the very
# list it was handed.
mutable_pair, other_end = socket.socketpair()
class MutatesTheList:
def __init__(self, elements, fd):
self.elements = elements
self.fd = fd
def fileno(self):
self.elements.clear()
self.elements.append(self)
return self.fd
elements = []
elements.extend([MutatesTheList(elements, mutable_pair.fileno())] * 40)
assert select.select(elements, [], [], 0) == ([], [], [])
class GrowsTheList:
def __init__(self, elements, fd):
self.elements = elements
self.fd = fd
def fileno(self):
self.elements.append(self)
return self.fd
# A list that grows by one on every fileno() never reaches its own end, so the
# limit has to be answered during the walk rather than from the final length.
elements = []
elements.append(GrowsTheList(elements, mutable_pair.fileno()))
assert_raises(ValueError, select.select, elements, [], [], 0)
mutable_pair.close()
other_end.close()
# poll() waits with signal handlers able to run, and a handler may register on
# the same poll object.
if hasattr(select, "poll") and hasattr(signal, "setitimer"):
poller = select.poll()
idle, idle_peer = socket.socketpair()
poller.register(idle.fileno(), select.POLLIN)
handled = []
def register_from_handler(signum, frame):
poller.register(idle_peer.fileno(), select.POLLIN)
handled.append(signum)
previous = signal.signal(signal.SIGALRM, register_from_handler)
try:
signal.setitimer(signal.ITIMER_REAL, 0.05)
try:
poller.poll(1000)
except InterruptedError:
pass
finally:
signal.setitimer(signal.ITIMER_REAL, 0)
signal.signal(signal.SIGALRM, previous)
assert handled == [signal.SIGALRM], handled
idle.close()
idle_peer.close()
+15
View File
@@ -171,3 +171,18 @@ for exc, expected_name in [
# assert socket.timeout.__module__ == "builtins"
# assert socket.timeout.__name__ == "TimeoutError"
# recv() sizes its buffer from the argument, so an unreachable size has to be
# reported rather than reserved.
sizes = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
with sizes:
for bufsize in (2**62, 2**48):
try:
sizes.recv(bufsize)
except (MemoryError, OSError, OverflowError):
# A size that does not fit the platform's C int is reported while
# converting the argument, before there is anything to reserve.
pass
with assert_raises(ValueError):
sizes.recvfrom(-1)
+64
View File
@@ -92,3 +92,67 @@ with assert_raises(UnicodeEncodeError):
with assert_raises(struct.error):
struct.Struct(b"\xff")
# A value the format has no room for names the format and the range it holds.
for fmt, message in (
("B", "'B' format requires 0 <= number <= 255"),
("b", "'b' format requires -128 <= number <= 127"),
(">H", "'H' format requires 0 <= number <= 65535"),
(">i", "'i' format requires -2147483648 <= number <= 2147483647"),
("N", "'N' format requires 0 <= number <= 18446744073709551615"),
("P", "int too large to convert"),
):
try:
struct.pack(fmt, 10**30)
except struct.error as e:
assert str(e) == message, (fmt, str(e))
else:
raise AssertionError(f"expected struct.error for {fmt!r}")
try:
struct.pack("B", "x")
except struct.error as e:
assert str(e) == "required argument is not an integer", e
else:
raise AssertionError("expected struct.error")
# __init__ reads a new format into a Struct that already holds one.
s = struct.Struct(">h")
s.__init__(">hh")
assert s.format == ">hh"
assert s.size == 4
assert s.pack(1, 2) == b"\x00\x01\x00\x02"
assert s.unpack(b"\x00\x01\x00\x02") == (1, 2)
# A format that cannot be read leaves the Struct as it was.
for bad in ("\udc00", "$"):
with assert_raises((UnicodeEncodeError, struct.error)):
s.__init__(bad)
assert s.format == ">hh"
assert s.pack(1, 2) == b"\x00\x01\x00\x02"
# A subclass may do its own __init__ and pass the format up.
class BigShort(struct.Struct):
def __init__(self):
super().__init__(">h")
assert BigShort().pack(12345) == b"\x30\x39"
# Until __init__ runs there is no format to answer with.
blank = struct.Struct.__new__(struct.Struct)
assert blank.size == -1
for call in (
lambda: blank.format,
lambda: blank.pack(1),
lambda: blank.unpack(b"aa"),
lambda: blank.unpack_from(b"aaaa"),
lambda: blank.pack_into(bytearray(4), 0, 1),
lambda: blank.iter_unpack(b"aa"),
lambda: repr(blank),
):
with assert_raises(RuntimeError):
call()
@@ -0,0 +1,100 @@
"""Take sys._current_frames() while other threads are running Python.
The frame each thread is executing is published for cross-thread readers, and
_current_frames() takes a reference to it with the world stopped. A reader that
disagrees with the publisher about what the published pointer addresses reads
and reference-counts the wrong memory, which corrupts a neighbouring object
rather than failing at the read: the damage surfaces later, in the thread that
owns it, as a crash or a wedge.
Workers therefore run ordinary Python calls (which publish a frame) in a tight
loop while the main thread hammers _current_frames().
"""
import sys
import threading
import time
DURATION = 1.5
def leaf():
return sum(range(8))
def nest(n):
if n:
return nest(n - 1)
return leaf()
def worker(stop):
while not stop.is_set():
nest(16)
def frames_are_sane(frames):
# Every key is a thread id, every value a frame of this process.
for tid, frame in frames.items():
assert isinstance(tid, int), tid
assert tid > 0, tid
assert type(frame).__name__ == "frame", frame
assert isinstance(frame.f_lineno, int), frame
assert isinstance(frame.f_code.co_name, str), frame
# The main thread sees itself where it stands.
me = sys._current_frames()[threading.get_ident()]
assert me is sys._getframe(), me
stop = threading.Event()
threads = [threading.Thread(target=worker, args=(stop,)) for _ in range(4)]
for t in threads:
t.start()
deadline = time.time() + DURATION
calls = 0
while time.time() < deadline:
frames_are_sane(sys._current_frames())
calls += 1
stop.set()
for t in threads:
t.join()
assert calls > 0, calls
# A thread parked in a call the main thread can name is reported inside it,
# with its callers reachable through f_back.
entered = threading.Event()
leave = threading.Event()
seen = []
def g456():
seen.append(threading.get_ident())
entered.set()
leave.wait()
def f123():
g456()
t = threading.Thread(target=f123)
t.start()
entered.wait()
try:
chain = []
frame = sys._current_frames()[seen[0]]
while frame is not None:
chain.append(frame.f_code.co_name)
frame = frame.f_back
assert "g456" in chain, chain
assert "f123" in chain, chain
assert chain.index("g456") < chain.index("f123"), chain
finally:
leave.set()
t.join()
print("ok")
+2 -2
View File
@@ -47,14 +47,14 @@ if sys.implementation.name == "rustpython":
list[self_referential]
nested = [0]
for _ in range(sys.getrecursionlimit() * 2):
for _ in range(100_000):
nested = [nested]
with assert_raises(RecursionError):
list[nested]
# hashing an alias walks the same shape
deep_alias = int
for _ in range(sys.getrecursionlimit() * 2):
for _ in range(100_000):
deep_alias = list[deep_alias]
with assert_raises(RecursionError):
hash(deep_alias)
+18
View File
@@ -45,3 +45,21 @@ class ClassWithUnionParams:
assert _typing._idfunc(1) == 1
with assert_raises(TypeError):
_typing._idfunc()
# ParamSpecArgs shows a non-ParamSpec origin by its repr, which is where the
# recursion guard lives; nesting them deeply must not walk the native stack.
from typing import ParamSpec, ParamSpecArgs
spec = ParamSpec("spec")
assert repr(spec.args) == "spec.args"
assert repr(spec.kwargs) == "spec.kwargs"
nested = object()
for _ in range(2000):
nested = ParamSpecArgs(nested)
try:
repr(nested)
except RecursionError:
pass