Skip to content

ctypes: fix Windows pip truststore - #8314

Merged
youknowone merged 4 commits into
RustPython:mainfrom
youknowone:agent/fix-ctypes-errcheck
Jul 20, 2026
Merged

ctypes: fix Windows pip truststore#8314
youknowone merged 4 commits into
RustPython:mainfrom
youknowone:agent/fix-ctypes-errcheck

Conversation

@youknowone

@youknowone youknowone commented Jul 18, 2026

Copy link
Copy Markdown
Member

Summary

  • preserve the foreign-call result when a ctypes errcheck callback returns the exact args tuple
  • accept c_char_p and c_wchar_p instances when initializing matching pointer arrays
  • add focused stdlib_ctypes.py regressions and a Windows pip HTTPS smoke test using pip's vendored truststore
  • implement PyEval_SaveThread/PyEval_RestoreThread so blocking C API callers detach from stop-the-world

Closes #8281.

Root cause

RustPython unconditionally replaced a foreign function's result with the value returned by errcheck. Truststore returns the original args tuple to request normal result processing, so RustPython changed the Windows certificate-store handle into a tuple. Passing that tuple to the next c_void_p argument raised TypeError: wrong type.

After preserving the handle, truststore reached its enhanced-key-usage setup and initialized a c_char_p array from an existing c_char_p instance. RustPython's specialized string-pointer array writer accepted bytes and integer addresses, but not a matching ctypes pointer instance.

CI hang root cause

The Ubuntu C API job exposed an existing threading bug: PyEval_SaveThread and PyEval_RestoreThread were no-ops. A parent test thread could therefore remain attached while joining a child that requested stop-the-world, leaving each thread waiting for the other. The implementation now preserves the VM stack and GILState-owned VM across detach/restore, and the C API test forces a stop-the-world cycle to cover the deadlock deterministically.

Validation

  • uvx prek run --all-files
  • cargo clippy -p rustpython-vm -p rustpython-capi --all-targets
  • supported Windows workspace test suite
  • Linux C API suite (98 tests), including 100 repeated parallel runs
  • all snippet tests under CPython 3.14 and RustPython (400 passed, 4 skipped)
  • uvx zizmor --min-severity low .github/workflows/ci.yaml
  • isolated ensurepip followed by pip download six over HTTPS on Windows

Summary by CodeRabbit

  • New Features
    • Added native thread save/restore support that preserves VM/GIL context when threads detach and later restore (threading feature).
  • Bug Fixes
    • Improved _ctypes errcheck handling so the original result is only replaced when the checker returns a different value.
    • Enhanced _ctypes array element writing for c_char_p/c_wchar_p-style pointer types to write pointer values directly from compatible simple instances.
    • Updated Windows overlapped I/O completion to run with threads allowed to prevent blocking VM thread execution.
  • Tests
    • Expanded _ctypes snippet coverage for char pointers and Windows kernel32 ctypes validation.
  • CI
    • Added a Windows-only HTTPS trust-store connectivity smoke test for pip snippet execution.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: a9f07945-4467-4577-b2cd-1951d6523970

📥 Commits

Reviewing files that changed from the base of the PR and between c7d7ece and c200885.

📒 Files selected for processing (3)
  • crates/capi/src/pystate.rs
  • crates/stdlib/src/overlapped.rs
  • crates/vm/src/vm/thread.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/vm/src/vm/thread.rs

📝 Walkthrough

Walkthrough

The change adds _ctypes pointer and errcheck handling, implements VM thread-state save and restore for the C API, releases the VM during Windows overlapped waits, expands ctypes tests, and adds a Windows pip HTTPS smoke test.

Changes

ctypes behavior and Windows validation

Layer / File(s) Summary
ctypes pointer and errcheck handling
crates/vm/src/stdlib/_ctypes/function.rs, crates/vm/src/stdlib/_ctypes/array.rs
Pointer-backed "z" and "Z" values are written from PyCSimple buffers, while unchanged errcheck tuples no longer replace the result.
ctypes regression coverage
extra_tests/snippets/stdlib_ctypes.py
Tests cover c_char_p arrays and Windows WinDLL calls, including preserved and transformed errcheck results.
Windows pip HTTPS smoke check
.github/workflows/ci.yaml
Windows CI downloads six with RustPython pip using the runner trust store; cargo documentation step formatting is adjusted.

Native thread VM state preservation

Layer / File(s) Summary
Thread state save and restore primitives
crates/vm/src/vm/thread.rs
Thread-local VM stacks and GIL state can be saved, detached, restored, and reattached.
C API thread state bridge
crates/capi/src/pystate.rs
PyEval_SaveThread and PyEval_RestoreThread now exchange saved VM state through an FFI-compatible allocation, with expanded thread tests.
Overlapped wait thread release
crates/stdlib/src/overlapped.rs
GetQueuedCompletionStatus performs its host wait through vm.allow_threads.

Sequence Diagram(s)

sequenceDiagram
  participant NativeThread
  participant PyEval_SaveThread
  participant SavedThreadState
  participant PyEval_RestoreThread

  NativeThread->>PyEval_SaveThread: detach and save VM state
  PyEval_SaveThread->>SavedThreadState: allocate saved state payload
  PyEval_SaveThread-->>NativeThread: return thread-state pointer
  NativeThread->>PyEval_RestoreThread: provide thread-state pointer
  PyEval_RestoreThread->>SavedThreadState: reconstruct saved payload
  PyEval_RestoreThread-->>NativeThread: restore and reattach VM state
Loading

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: shaharnaveh, bschoenmaeckers

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly reflects the main Windows ctypes/truststore fix.
Linked Issues check ✅ Passed The ctypes, C API, and Windows smoke-test changes align with fixing pip truststore failures on Windows 11 25H2.
Out of Scope Changes check ✅ Passed The workflow and runtime changes appear supportive of the Windows pip fix rather than unrelated scope creep.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@youknowone
youknowone marked this pull request as ready for review July 18, 2026 10:03
@youknowone youknowone changed the title ctypes: preserve result when errcheck returns args ctypes: fix Windows pip truststore Jul 18, 2026
@youknowone
youknowone marked this pull request as draft July 18, 2026 14:43
@youknowone
youknowone marked this pull request as ready for review July 19, 2026 01:00
@youknowone
youknowone force-pushed the agent/fix-ctypes-errcheck branch from 9b3ae9d to 7eed565 Compare July 19, 2026 02:38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/capi/src/pystate.rs (1)

101-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a direct PyEval_SaveThread/PyEval_RestoreThread regression test.

This test exercises Python::attach/detach and stop-the-world transitions, but doesn't directly call the newly-implemented PyEval_SaveThread/PyEval_RestoreThread pair, so the actual save/restore payload round-trip (and the gap noted in thread.rs) isn't covered.

#[test]
fn save_restore_thread() {
    Python::attach(|_py| {
        assert!(current_vm_is_set());
        let saved = PyEval_SaveThread();
        assert!(!current_vm_is_set());
        unsafe { PyEval_RestoreThread(saved) };
        assert!(current_vm_is_set());
    });
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/capi/src/pystate.rs` around lines 101 - 126, Add a direct regression
test for the PyEval_SaveThread/PyEval_RestoreThread pair alongside the existing
thread-state test. Within Python::attach, assert the VM is set, save the thread
state, assert it is cleared, restore the saved payload, and assert the VM is set
again; preserve the unsafe restore call as required by the API.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/vm/src/vm/thread.rs`:
- Around line 251-280: Update restore_current_thread and the existing VmRef::new
and attach_current_thread paths to share an attach_vm_to_current_thread helper
that initializes the thread slot with init_thread_slot_if_needed before calling
attach_thread. Ensure the helper accepts the VM to attach and preserves each
caller’s existing VM source, so restoring state on a different native thread
registers it for ATTACHED/QSBR tracking before updating the VM stack.

---

Nitpick comments:
In `@crates/capi/src/pystate.rs`:
- Around line 101-126: Add a direct regression test for the
PyEval_SaveThread/PyEval_RestoreThread pair alongside the existing thread-state
test. Within Python::attach, assert the VM is set, save the thread state, assert
it is cleared, restore the saved payload, and assert the VM is set again;
preserve the unsafe restore call as required by the API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: e979c9cc-fe81-4dc5-a393-410cb3d16348

📥 Commits

Reviewing files that changed from the base of the PR and between 7eed565 and c7d7ece.

📒 Files selected for processing (2)
  • crates/capi/src/pystate.rs
  • crates/vm/src/vm/thread.rs

Comment on lines +251 to +280
/// Restore a VM context previously returned by [`save_current_thread`].
#[cfg(feature = "threading")]
pub fn restore_current_thread(state: SavedThreadState) {
assert!(
!current_vm_is_set(),
"restore_current_thread() called with an attached VM"
);
let SavedThreadState {
vm_stack,
gilstate_vm,
} = state;
let vm = vm_stack
.last()
.copied()
.expect("saved thread state has no VM");

GILSTATE_VM.with(|current| {
let mut current = current.borrow_mut();
assert!(
current.is_none(),
"restore_current_thread() called with a GILState VM"
);
*current = gilstate_vm;
});

// SAFETY: borrowed VMs remain alive for the dynamic save/restore scope,
// while an owned GILState VM was restored above before this dereference.
attach_thread(unsafe { vm.as_ref() });
VM_STACK.with(|vms| *vms.borrow_mut() = vm_stack);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

restore_current_thread skips thread-slot initialization before attach_thread.

attach_thread (Line 380-410) only performs the ATTACHED/DETACHED CAS and QSBR.online when CURRENT_THREAD_SLOT is already Some; if the slot is None it silently no-ops. Every other path that calls attach_threadVmRef::new (Line 183-203) and attach_current_thread (context snippet) — first calls init_thread_slot_if_needed(vm). restore_current_thread calls attach_thread directly without that init step.

This is safe only when restore always runs on the exact same native thread that previously called save_current_thread (whose slot was already initialized before the save). But CPython's own PyEval_RestoreThread contract explicitly allows attaching "whichever thread calls it" — i.e. handing the saved state to a different native thread is a legitimate, documented use case. On such a thread, this gap leaves the thread un-registered for stop-the-world/QSBR tracking while VM_STACK/current_vm_is_set() reports it as attached — a silent, hard-to-diagnose state divergence rather than a clear panic.

As per coding guidelines, "When branches differ only in a value but share common logic, extract the differing value first, then call the common logic once to avoid duplicate code" — extracting a shared attach_vm_to_current_thread helper (used by VmRef::new, attach_current_thread, and restore_current_thread) would both fix this gap and remove the duplication.

🔧 Proposed fix
     // SAFETY: borrowed VMs remain alive for the dynamic save/restore scope,
     // while an owned GILState VM was restored above before this dereference.
-    attach_thread(unsafe { vm.as_ref() });
+    let vm_ref = unsafe { vm.as_ref() };
+    init_thread_slot_if_needed(vm_ref);
+    attach_thread(vm_ref);
     VM_STACK.with(|vms| *vms.borrow_mut() = vm_stack);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Restore a VM context previously returned by [`save_current_thread`].
#[cfg(feature = "threading")]
pub fn restore_current_thread(state: SavedThreadState) {
assert!(
!current_vm_is_set(),
"restore_current_thread() called with an attached VM"
);
let SavedThreadState {
vm_stack,
gilstate_vm,
} = state;
let vm = vm_stack
.last()
.copied()
.expect("saved thread state has no VM");
GILSTATE_VM.with(|current| {
let mut current = current.borrow_mut();
assert!(
current.is_none(),
"restore_current_thread() called with a GILState VM"
);
*current = gilstate_vm;
});
// SAFETY: borrowed VMs remain alive for the dynamic save/restore scope,
// while an owned GILState VM was restored above before this dereference.
attach_thread(unsafe { vm.as_ref() });
VM_STACK.with(|vms| *vms.borrow_mut() = vm_stack);
}
/// Restore a VM context previously returned by [`save_current_thread`].
#[cfg(feature = "threading")]
pub fn restore_current_thread(state: SavedThreadState) {
assert!(
!current_vm_is_set(),
"restore_current_thread() called with an attached VM"
);
let SavedThreadState {
vm_stack,
gilstate_vm,
} = state;
let vm = vm_stack
.last()
.copied()
.expect("saved thread state has no VM");
GILSTATE_VM.with(|current| {
let mut current = current.borrow_mut();
assert!(
current.is_none(),
"restore_current_thread() called with a GILState VM"
);
*current = gilstate_vm;
});
// SAFETY: borrowed VMs remain alive for the dynamic save/restore scope,
// while an owned GILState VM was restored above before this dereference.
let vm_ref = unsafe { vm.as_ref() };
init_thread_slot_if_needed(vm_ref);
attach_thread(vm_ref);
VM_STACK.with(|vms| *vms.borrow_mut() = vm_stack);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm/src/vm/thread.rs` around lines 251 - 280, Update
restore_current_thread and the existing VmRef::new and attach_current_thread
paths to share an attach_vm_to_current_thread helper that initializes the thread
slot with init_thread_slot_if_needed before calling attach_thread. Ensure the
helper accepts the VM to attach and preserves each caller’s existing VM source,
so restoring state on a different native thread registers it for ATTACHED/QSBR
tracking before updating the VM stack.

Source: Coding guidelines

@youknowone

Copy link
Copy Markdown
Member Author

@bschoenmaeckers Could you please review if C API changes are reasonable?

Comment thread crates/capi/src/pystate.rs Outdated
}

#[repr(C)]
struct SavedPyThreadState {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This can be merged into the PyThreadState struct. You may add private fields after the interp pub field.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thank you so much!

@youknowone
youknowone force-pushed the agent/fix-ctypes-errcheck branch from c7d7ece to c200885 Compare July 20, 2026 04:17
@youknowone
youknowone merged commit 4c9b5d8 into RustPython:main Jul 20, 2026
26 checks passed
@youknowone
youknowone deleted the agent/fix-ctypes-errcheck branch July 20, 2026 06:21
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.

pip is failing to run on Windows 11 25H2.

2 participants