Skip to content

_ctypes, signal, _multiprocessing: the #1357 review findings - #1371

Merged
youknowone merged 1 commit into
mainfrom
winapi
Aug 20, 2026
Merged

_ctypes, signal, _multiprocessing: the #1357 review findings#1371
youknowone merged 1 commit into
mainfrom
winapi

Conversation

@youknowone

Copy link
Copy Markdown
Owner

The 15 review findings left on #1357, triaged after it merged. 11 real, 4 refuted.

Fixed

_ctypes — the BSTR store frees before it converts. X_set allocates the
new string first and frees what the slot held only immediately before storing,
so a value it refuses leaves the previous one readable. Four stores had the
opposite order — value_setter plus the struct-field, array-element and
pointer-element stores in metaclass.rs; the bot reported one. It reads back
correctly until the allocator hands the block to someone else:

b = BSTR("xy"); b.value = 1        # TypeError
[BSTR("ab") for _ in range(64)]    # same-size churn reclaims the block
b.value                            # 'zz' before, 'xy' after — CPython says 'xy'

SysAllocStringLen answering null now raises MemoryError instead of storing
a null pointer.

_ctypes_build_callargs. Its outmask/inoutmask bits were set with
an unbounded 1 << i; the masks are the C int pair and _build_result reads
back only the first 32, so param_bit stops at the same width. The same loop
held its collected values in a plain Vec across out_parameter, which
instantiates the argtype and so runs Python — they are pinned as collected and
read back out of their root slots.

_ctypesPyErr_SetFromWindowsErr(0).
PyErr_SetExcFromWindowsErrWithFilenameObjects reads GetLastError() when the
code is 0, so an explicit zero no longer names ERROR_SUCCESS:

SetLastError(5); pythonapi.PyErr_SetFromWindowsErr(0)   # .winerror 0 -> 5

signal — the Windows wakeup fd was never written. The handler flagged the
signal and returned, so set_wakeup_fd was a record and nothing reached the
descriptor. signals.c:145-167 writes on both platforms (the #ifdef _WIN32
only changes the type of res) and PYPYSIG_USE_SEND picks send for a
descriptor that answered the getsockopt probe. That probe already ran in
set_wakeup_fd; its verdict now reaches the handler, which writes the
signal-number byte with send or the C runtime's write, keeps the EINTR
retry and the warn_on_full_buffer drop, and restores the caller's errno.
Both paths now answer as CPython does — b'\x02' for a socketpair and for an
os.pipe() write end.

_multiprocessing. semlock_acquire saturated a Windows timeout at
u32::MAX; interp_semaphore.py:272-273 refuses one at half of INFINITE
with OverflowError("timeout is too large"), and rounds with
int(timeout + 0.5) rather than upwards. A wait returning WAIT_OBJECT_0 then
ran checksignals_now(), which can raise after the count has already been
taken — interp_semaphore.py:311-315 reports the acquisition before anything
that can raise, and a signal pending at that moment is delivered at the next
checkpoint like any other.

_thread. interrupt_main reported PyLong_AsLong's message for an
i32::try_from. It stages through the platform's C long first, so MSVC —
where a long is 32 bits — keeps that one message for everything out of range,
and an LP64 build gets the two getargs.c gives its 'i' conversion.

Plus: one SIGBREAK constant instead of two, and @test_*_tmp* unanchored in
.gitignore (the directory a stdlib test leaves scratch files in is wherever
the run started, not the repo root).

Refuted

  • "rename c_long_overflow to c_int_overflow." The oracle disagrees:
    python3.14 -c "_thread.interrupt_main(2**200)" says Python int too large to convert to C long. MSVC's long is 32 bits, so PyLong_AsLong catches
    everything first. The line did have a real defect, fixed above.
  • "the Windows tzname arm needs the POSIX " " fallback."
    interp_time.py:517-527's _WIN branch has no fallback; it is POSIX-only,
    for an empty tm_zone.
  • "gate the Windows time helpers on host_env." Real premise, wrong scope.
    The Windows --no-default-features build fails with 17 errors, 15 of them
    pre-existing in mmap/sys/importing/posix (git blame against the merge base).
    Gating two turns compile errors into silent behaviour changes without making
    anything build, and nothing uses that config — pyrex reaches
    pyre-interpreter through pyre-jit with default features, so host_env is
    always on.
  • "require S_OK before reading COM error info." GetComError gates
    InterfaceSupportsErrorInfo on SUCCEEDED(hr), so S_FALSE passes, and
    reserves the strict S_OK test for GetErrorInfo. That is exactly the split
    already implemented (supports < 0 vs GetErrorInfo(...) != 0).

Not done

The rooting fix closes the window inside build_callargs, which is what was
reported. A wider one remains: cfuncptr_call holds callargs.args across
marshal_typed_arg (which runs from_param) with nothing rooted, and
_build_result reads those raw refs again after the call. Closing it means
restructuring the FFI call path, so it is left out of a review-fix change.

Verification

cargo test --all --no-default-features --features dynasm exit 0.
extra_tests/parity_tests 441/441 across cpython, dynasm and cranelift.
pyre/check.pydynasm 442/442, cranelift 442/442.

One red, on a leg this platform's CI does not run: wasm fib_recursive ratio 5.9x > gate 4x, reproducible at 5.5x / 5.9x / 6.0x over three runs. Windows
keeps no wasm32 target (pyre-ci.yml:625-632), the fixture's recorded wasm
jit-stats match in every run so the compiled trace is unchanged, and the 4x
ceiling with its fib_recursive 3.6x datum was set in #1272 on a Linux-only
wasm leg. Not measured against a pre-change control build.

_ctypes
-------

`X_set` allocates the new BSTR first and frees what the slot held only
immediately before the store, so a value it refuses leaves the previous
string readable.  Four stores released the slot before the conversion that
can fail — `value_setter` and the struct-field, array-element and
pointer-element stores in `metaclass.rs` — which frees the string and leaves
the freed pointer in the slot.  It reads back correctly until the allocator
hands the block to someone else:

    b = BSTR("xy"); b.value = 1        # TypeError
    [BSTR("ab") for _ in range(64)]    # same-size churn reclaims the block
    b.value                            # 'zz' here, 'xy' under CPython

`SysAllocStringLen` answering null now raises `MemoryError` rather than
storing a null pointer.

`_build_callargs` set its `outmask`/`inoutmask` bits with an unbounded
`1 << i`.  The masks are the C `int` pair and `_build_result` reads back only
the first 32 bits, so `param_bit` stops at the same width.  The same loop held
the values it had collected in a plain `Vec` across `out_parameter`, which
instantiates the argtype and so runs Python; they are pinned as they are
collected and read back out of their root slots.

`PyErr_SetExcFromWindowsErrWithFilenameObjects` reads `GetLastError()` when the
code it is handed is 0, so an explicit zero no longer names `ERROR_SUCCESS`:

    SetLastError(5); pythonapi.PyErr_SetFromWindowsErr(0)
    # .winerror was 0, is 5

signal
------

The Windows handler flagged the signal and returned, so `set_wakeup_fd` was a
record and no byte was ever written.  `signals.c:145-167` writes on both
platforms — the `#ifdef _WIN32` only changes the type of `res` — and
`PYPYSIG_USE_SEND` selects `send` for a descriptor that answered the
`getsockopt` probe.  That probe already ran in `set_wakeup_fd`; its verdict now
reaches the handler, which writes the signal-number byte with `send` or the C
runtime's `write`, keeps the EINTR retry and the warn_on_full_buffer drop, and
restores the caller's errno.  `rsocket_rffi` gained `WSAEWOULDBLOCK` for the
drop test, and `builtins` a `set_crt_errno` beside `clear_crt_errno`.

Both paths now answer as CPython does:

    signal.set_wakeup_fd(sock.fileno()); raise_signal(SIGINT)  # b'\x02'
    signal.set_wakeup_fd(pipe_w);        raise_signal(SIGINT)  # b'\x02'

`SIGBREAK` was spelled 21 twice in one file.

_multiprocessing
----------------

`semlock_acquire` saturated a Windows timeout at `u32::MAX`;
`interp_semaphore.py:272-273` refuses one at half of `INFINITE` with
`OverflowError("timeout is too large")`, and rounds with `int(timeout + 0.5)`
rather than upwards.  A wait that returned `WAIT_OBJECT_0` then ran
`checksignals_now()`, which can raise after the count has already been taken;
`interp_semaphore.py:311-315` reports the acquisition before anything that can
raise, and a signal pending at that moment is delivered at the next checkpoint.

_thread
-------

`interrupt_main` reported `PyLong_AsLong`'s message for an `i32::try_from`.  It
stages through the platform's C `long` first, so MSVC — where a `long` is 32
bits — keeps that one message for everything out of range, and an LP64 build
gets the two `getargs.c` gives its `'i'` conversion.

.gitignore
----------

`@test_*_tmp*` is unanchored: the working directory a stdlib test leaves its
scratch files in is wherever the run started, not the repo root.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@youknowone, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bc87769f-6e6a-4765-a67b-0b928c2a9b0e

📥 Commits

Reviewing files that changed from the base of the PR and between 712fc16 and 91c0c6d.

📒 Files selected for processing (10)
  • .gitignore
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/module/_ctypes/cdata.rs
  • pyre/pyre-interpreter/src/module/_ctypes/funcptr.rs
  • pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs
  • pyre/pyre-interpreter/src/module/_multiprocessing/mod.rs
  • pyre/pyre-interpreter/src/module/_socket/rsocket_rffi.rs
  • pyre/pyre-interpreter/src/module/signal/interp_signal.rs
  • pyre/pyre-interpreter/src/module/signal/signalstate.rs
  • pyre/pyre-interpreter/src/module/thread/mod.rs

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 91c0c6ddd8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

pub fn set_wakeup_fd(fd: i32, warn_on_full: bool, use_send: bool) -> i32 {
WAKEUP_WARN_ON_FULL.store(warn_on_full, Ordering::SeqCst);
#[cfg(windows)]
WAKEUP_USE_SEND.store(use_send, Ordering::SeqCst);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Publish the wakeup descriptor and transport atomically

On Windows, when set_wakeup_fd switches between a pipe and a socket while a signal is delivered concurrently, the handler can observe the new WAKEUP_USE_SEND value with the old WAKEUP_FD: switching pipe→socket then calls send on the old pipe, while socket→pipe calls CRT write on the old socket. Either case can lose the wakeup byte or target an unrelated descriptor with the same numeric value. Store the descriptor and transport selection as one atomic state rather than separate atomics.

AGENTS.md reference: AGENTS.md:L142-L144

Useful? React with 👍 / 👎.

Comment on lines +221 to +223
// `interp_semaphore.py:268-275` — a negative timeout is a poll,
// and one at half of `INFINITE` (about 25 days) is refused rather
// than saturated, so no wait silently becomes a different one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Cite upstream symbols instead of line ranges

The new citation here identifies the upstream behavior only as interp_semaphore.py:268-275, and the commit adds the same line-range pattern for the acquisition branch and several signals.c claims. These references silently rot whenever upstream lines move; name the owning symbols such as semlock_acquire, signal_setflag_handler, and pypysig_set_wakeup_fd and remove the numeric ranges.

AGENTS.md reference: AGENTS.md:L188-L191

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 91c0c6d).
Updated: 2026-08-20T07:17:59.194Z

Files in the reviewed diff
.gitignore
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/module/_ctypes/cdata.rs
pyre/pyre-interpreter/src/module/_ctypes/funcptr.rs
pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs
pyre/pyre-interpreter/src/module/_multiprocessing/mod.rs
pyre/pyre-interpreter/src/module/_socket/rsocket_rffi.rs
pyre/pyre-interpreter/src/module/signal/interp_signal.rs
pyre/pyre-interpreter/src/module/signal/signalstate.rs
pyre/pyre-interpreter/src/module/thread/mod.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-interpreter/src/module/_ctypes/funcptr.rs:1170 ↔ lib_pypy/_ctypes/function.py:387: param_bit() returns zero for every OUT/INOUT parameter at index 32 or above. build_result() therefore omits those outputs, while PyPy retains every OUT value in its unbounded outargs list and returns all of them. A 33rd sole OUT parameter becomes () in pyre rather than that parameter’s value.

  • pyre/pyre-interpreter/src/module/thread/mod.rs:1987 ↔ pypy/module/thread/os_thread.py:263: the new C-int conversion rejects a c_long-representable signum such as 2**31 with OverflowError; PyPy’s @unwrap_spec(signum=int) reaches check_signum_in_range and raises ValueError("signal number out of range") at pypy/module/thread/os_thread.py:274. This cannot be filed as a CPython structural adaptation: the local pinned CPython artefact only asserts the in-range conversion case at lib-python/3/test/test_threading.py:2425, not this overflow contract, so required test (b) is missing.

3. Pre-existing mismatches (already present before this patch)

  • pyre/pyre-interpreter/src/module/_ctypes/funcptr.rs:1079 ↔ lib_pypy/_ctypes/function.py:537: CallArgs has fixed-width u32 OUT/INOUT masks, whereas PyPy stores OUT values in an unbounded list. This 32-parameter design predates the patch; the new param_bit() merely makes its post-32 behavior deterministic rather than correcting it.

4. Structural adaptations

  • pyre/pyre-interpreter/src/module/signal/signalstate.rs:438 ↔ rpython/translator/c/src/signals.c:156: using Rust’s WinSock wrapper and atomics to select send() for socket wakeup descriptors is a fundamental Rust/FFI adaptation of PyPy’s PYPYSIG_USE_SEND path, not an observable semantic deviation.

@youknowone
youknowone merged commit cb2da3a into main Aug 20, 2026
17 checks passed
@youknowone
youknowone deleted the winapi branch August 20, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant