Fix fork-child SIGSEGV and yield-from throw() args (#8759)

* Unskip passing flaky CPython tests

Remove RUSTPYTHON skip overlays after Linux release repeats passed
for enum, threading, logging, subprocess, socket, int, re, strtod,
and regrtest cases.

Assisted-by: Grok:4.6

* Fix fork-child SIGSEGV and yield-from throw args

Forget JoinHandle of threads that did not survive fork instead
of dropping them. Zero-reinit the import lock and condvars in
the child rather than unlock/notify. Resume the world if
os.fork() fails.

Call a yield-from subiterator throw() without None fillers so
a 1-arg throw stays 1-arg.

Drop the stale Windows expectedFailure on logging queue config
and unskip test_reinit_tls_after_fork plus the asyncio
local_addr family test.

Assisted-by: Grok:4.6

* Restore import lock depth after fork; add WASI clock info

Zeroing IMP_LOCK in the child discarded its recursion count, so
imp.release_lock() raised and test_fork1 exited 1. Re-acquire the
saved depth, then drop the extra pre-fork hold.

Expose time.get_clock_info on WASI via clock_getres so test_int
Stopwatch can run.

Unskip lock_tests test_acquire_contended and test_thread_leak.

Assisted-by: Grok:4.6

* Honor chmod follow_symlinks=False via fchmodat

Linux chmod followed the symlink, so dangling links raised
FileNotFoundError instead of NotImplementedError. Use fchmodat
with AT_SYMLINK_NOFOLLOW and map ENOTSUP to NotImplementedError.

Unskip httpservers truncated Content-Length (CPython already
skips as root) and tempfile symlink-mode cleanup on Linux.

Assisted-by: Grok:4.6

* Unskip httplib local bad-hostname HTTPS test

The skip was leftover. With default ssl-rustls the test passes
under -W error on Linux.

Assisted-by: Grok:4.6

* Fix list cmp deadlock, wasm digit overflow, REPL prompt

Do not hold list/deque locks across item comparison callbacks.
Compute int-to-str digit bounds in u64 so the check cannot wrap
on wasm32. Snapshot set contents when list.extend copies a set.

For a non-TTY stdin, write the REPL prompt to stderr and read
lines from stdin. Flush stdout/stderr after each interactive
command. Set sys.ps1/ps2 to ">>> " and "... ".

Unskip list mutation compares, asyncio all_tasks race, importlib
deadlock/parallel/circular-import tests, and REPL stdout flush.

Assisted-by: Grok:4.6

* Avoid unnecessary u64 cast in int-to-str digit check

bits() is already u64, so `bits as u64` fails clippy
unnecessary_cast under -Dwarnings.

Assisted-by: Grok:4.6
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-09-21 20:53:53 +09:00
1 parent a46627a33c
commit 8fda0eccee
35 files changed
+283 -123

No files matched your search

-2
View File
@@ -162,7 +162,6 @@ class BaseLockTests(BaseTestCase):
self.assertFalse(result[0])
lock.release()
@unittest.skip("TODO: RUSTPYTHON; sometimes hangs")
def test_acquire_contended(self):
lock = self.locktype()
lock.acquire()
@@ -210,7 +209,6 @@ class BaseLockTests(BaseTestCase):
with Bunch(f, 1):
pass
@unittest.skip("TODO: RUSTPYTHON; sometimes hangs")
def test_thread_leak(self):
# The lock shouldn't leak a Thread instance when used from a foreign
# (non-threading) thread.
-1
View File
@@ -789,7 +789,6 @@ class EventLoopTestsMixin:
self.assertEqual(port, expected)
tr.close()
@unittest.skipIf(sys.platform == "linux", "TODO: RUSTPYTHON; Flaky on CI")
@socket_helper.skip_if_tcp_blackhole
def test_create_connection_local_addr_skip_different_family(self):
# See https://github.com/python/cpython/issues/86508
-8
View File
@@ -189,10 +189,6 @@ class TestPyFreeThreading(TestFreeThreading, TestCase):
def factory(self, loop, coro, **kwargs):
return asyncio.tasks._PyTask(coro, loop=loop, **kwargs)
@unittest.skip("TODO: RUSTPYTHON; hangs - Python _current_tasks dict not thread-safe")
def test_all_tasks_race(self):
return super().test_all_tasks_race()
@unittest.skipUnless(hasattr(asyncio.tasks, "_c_all_tasks"), "requires _asyncio")
class TestCFreeThreading(TestFreeThreading, TestCase):
@@ -224,10 +220,6 @@ class TestEagerPyFreeThreading(TestPyFreeThreading):
def factory(self, loop, coro, eager_start=True, **kwargs):
return asyncio.tasks._PyTask(coro, loop=loop, **kwargs, eager_start=eager_start)
@unittest.skip("TODO: RUSTPYTHON; hangs - Python _current_tasks dict not thread-safe")
def test_all_tasks_race(self):
return super().test_all_tasks_race()
@unittest.skipUnless(hasattr(asyncio.tasks, "_c_all_tasks"), "requires _asyncio")
class TestEagerCFreeThreading(TestCFreeThreading, TestCase):
+2 -4
View File
@@ -208,19 +208,17 @@ class CmdLineTest(unittest.TestCase):
self.assertIn(b' ~^~', stderr.readline())
self.assertIn(b'ZeroDivisionError', stderr.readline())
@unittest.skip("TODO: RUSTPYTHON; test hang in middle")
def test_repl_stdout_flush(self):
self.check_repl_stdout_flush()
@unittest.skip("TODO: RUSTPYTHON; test hang in middle")
def test_repl_stdout_flush_separate_stderr(self):
self.check_repl_stdout_flush(True)
@unittest.skip("TODO: RUSTPYTHON; test hang in middle")
@unittest.skip("TODO: RUSTPYTHON; <stdin> traceback omits the source line and caret")
def test_repl_stderr_flush(self):
self.check_repl_stderr_flush()
@unittest.skip("TODO: RUSTPYTHON; test hang in middle")
@unittest.skip("TODO: RUSTPYTHON; <stdin> traceback omits the source line and caret")
def test_repl_stderr_flush_separate_stderr(self):
self.check_repl_stderr_flush(True)
-2
View File
@@ -3972,7 +3972,6 @@ class OldTestFlag(unittest.TestCase):
self.assertEqual(Color.ALL.value, 7)
self.assertEqual(str(Color.BLUE), 'blue')
@unittest.skip("TODO: RUSTPYTHON; flaky test")
@threading_helper.reap_threads
@threading_helper.requires_working_threading()
def test_unique_composite(self):
@@ -4493,7 +4492,6 @@ class OldTestIntFlag(unittest.TestCase):
self.assertEqual(Color.ALL.value, 7)
self.assertEqual(str(Color.BLUE), 'blue')
@unittest.skip("TODO: RUSTPYTHON; flaky test")
@threading_helper.reap_threads
@threading_helper.requires_working_threading()
def test_unique_composite(self):
-1
View File
@@ -2166,7 +2166,6 @@ class HTTPSTest(TestCase):
self.addCleanup(resp.close)
self.assertEqual(resp.status, 404)
@unittest.skip("TODO: RUSTPYTHON; Flaky on CI")
def test_local_bad_hostname(self):
# The (valid) cert doesn't validate the HTTPS hostname
import ssl
-1
View File
@@ -1131,7 +1131,6 @@ class CGIHTTPServerTestCase(BaseTestCase):
res = self.request('/cgi-bin/file7.py', 'POST', body, headers)
self.assertEqual(res.read(), b'%d %d' % (size, size) + self.linesep)
@unittest.skip("TODO: RUSTPYTHON; flaky test")
def test_large_content_length_truncated(self):
with support.swap_attr(self.request_handler, 'timeout', 0.001):
for w in range(18, 65):
-2
View File
@@ -99,7 +99,6 @@ class DeadlockAvoidanceTests:
self.assertEqual(len(results), NTHREADS)
return results
@unittest.skip("TODO: RUSTPYTHON; sometimes hangs")
def test_deadlock(self):
results = self.run_deadlock_avoidance_test(True)
# At least one of the threads detected a potential deadlock on its
@@ -109,7 +108,6 @@ class DeadlockAvoidanceTests:
self.assertGreaterEqual(nb_deadlocks, 1)
self.assertEqual(results.count((True, True)), len(results) - nb_deadlocks)
@unittest.skip("TODO: RUSTPYTHON; flaky test")
def test_no_deadlock(self):
results = self.run_deadlock_avoidance_test(False)
self.assertEqual(results.count((True, False)), 0)
-3
View File
@@ -135,7 +135,6 @@ class ThreadedImportTests(unittest.TestCase):
if verbose:
print("OK.")
@unittest.skip("TODO: RUSTPYTHON; flaky")
@support.bigmemtest(size=50, memuse=76*2**20, dry_run=False)
def test_parallel_module_init(self, size):
self.check_parallel_module_init()
@@ -247,7 +246,6 @@ class ThreadedImportTests(unittest.TestCase):
__import__(TESTFN)
del sys.modules[TESTFN]
@unittest.skip("TODO: RUSTPYTHON; hang; Suspected cause of crashes in Windows CI - PermissionError: [WinError 32] Permission denied: \"C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\test_python_0cdrhhs_\\test_python_6340æ\"")
@support.bigmemtest(size=1, memuse=1.8*2**30, dry_run=False)
def test_concurrent_futures_circular_import(self, size):
# Regression test for bpo-43515
@@ -255,7 +253,6 @@ class ThreadedImportTests(unittest.TestCase):
'partial', 'cfimport.py')
script_helper.assert_python_ok(fn)
@unittest.skip("TODO: RUSTPYTHON; hang")
@support.bigmemtest(size=1, memuse=1.8*2**30, dry_run=False)
def test_multiprocessing_pool_circular_import(self, size):
# Regression test for bpo-41567
-1
View File
@@ -630,7 +630,6 @@ class IntStrDigitLimitsTests(unittest.TestCase):
self.assertIn('conversion', str(err.exception))
self.assertLess(sw_fail_extra_huge.seconds, sw_convert.seconds/2)
@unittest.skip("TODO: RUSTPYTHON; flaky test")
def test_denial_of_service_prevented_str_to_int(self):
"""Regression test: ensure we fail before performing O(N**2) work."""
maxdigits = sys.get_int_max_str_digits()
-2
View File
@@ -226,7 +226,6 @@ class ListTest(list_tests.CommonTest):
with self.assertRaises(TypeError):
(3,) + L([1,2])
@unittest.skip("TODO: RUSTPYTHON; hang")
def test_equal_operator_modifying_operand(self):
# test fix for seg fault reported in bpo-38588 part 2.
class X:
@@ -252,7 +251,6 @@ class ListTest(list_tests.CommonTest):
list4 = [1]
self.assertFalse(list3 == list4)
@unittest.skip("TODO: RUSTPYTHON; hang")
def test_lt_operator_modifying_operand(self):
# See gh-120298
class evil:
-3
View File
@@ -731,7 +731,6 @@ class HandlerTest(BaseTest):
# based on os.fork existing because that is what users and this test use.
# This helps ensure that when fork exists (the important concept) that the
# register_at_fork mechanism is also present and used.
@unittest.skip("TODO: RUSTPYTHON; Flaky")
@support.requires_fork()
@threading_helper.requires_working_threading()
@skip_if_asan_fork
@@ -4109,8 +4108,6 @@ class ConfigDictTest(BaseTest):
# log a message (this creates a record put in the queue)
logging.getLogger().info(message_to_log)
@unittest.skip("TODO: RUSTPYTHON; flaky EOFError")
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; SemLock not implemented on Windows")
@skip_if_tsan_fork
@support.requires_subprocess()
def test_multiprocessing_queues(self):
-1
View File
@@ -2476,7 +2476,6 @@ class ReTests(unittest.TestCase):
# gh-117594: The test is not slow by itself, but it relies on
# the absolute computation time and can fail on very slow computers.
@unittest.skip("TODO: RUSTPYTHON; flaky, improve perf")
@requires_resource('cpu')
def test_search_anchor_at_beginning(self):
s = 'x'*10**7
-7
View File
@@ -915,7 +915,6 @@ class ProgramsTestCase(BaseTestCase):
args = [*self.python_args, script, *self.regrtest_args, *self.tests]
self.run_tests(args)
@unittest.skip("TODO: RUSTPYTHON; flaky")
def test_module_test(self):
# -m test
args = [*self.python_args, '-m', 'test',
@@ -928,14 +927,12 @@ class ProgramsTestCase(BaseTestCase):
*self.regrtest_args, *self.tests]
self.run_tests(args)
@unittest.skip("TODO: RUSTPYTHON; flaky")
def test_module_autotest(self):
# -m test.autotest
args = [*self.python_args, '-m', 'test.autotest',
*self.regrtest_args, *self.tests]
self.run_tests(args)
@unittest.skip("TODO: RUSTPYTHON; flaky")
def test_module_from_test_autotest(self):
# from test import autotest
code = 'from test import autotest'
@@ -943,7 +940,6 @@ class ProgramsTestCase(BaseTestCase):
*self.regrtest_args, *self.tests]
self.run_tests(args)
@unittest.skip("TODO: RUSTPYTHON; flaky")
def test_script_autotest(self):
# Lib/test/autotest.py
script = os.path.join(self.testdir, 'autotest.py')
@@ -2033,7 +2029,6 @@ class ArgsTestCase(BaseTestCase):
for name in names:
self.assertFalse(os.path.exists(name), name)
@unittest.skip("TODO: RUSTPYTHON; flaky")
@unittest.skipIf(support.is_wasi,
'checking temp files is not implemented on WASI')
def test_leak_tmp_file(self):
@@ -2185,11 +2180,9 @@ class ArgsTestCase(BaseTestCase):
def test_random_seed(self):
self._check_random_seed(run_workers=False)
@unittest.skip("TODO: RUSTPYTHON; flaky")
def test_random_seed_workers(self):
self._check_random_seed(run_workers=True)
@unittest.skip("TODO: RUSTPYTHON; flaky")
def test_python_command(self):
code = textwrap.dedent(r"""
import sys
-8
View File
@@ -7066,14 +7066,6 @@ class SendfileUsingSendfileTest(SendfileUsingSendTest):
def meth_from_sock(self, sock):
return getattr(sock, "_sendfile_use_sendfile")
@unittest.skip("TODO: RUSTPYTHON; os.sendfile count parameter not handled correctly; flaky")
def testCount(self):
return super().testCount()
@unittest.skip("TODO: RUSTPYTHON; os.sendfile count parameter not handled correctly; flaky")
def testWithTimeout(self):
return super().testWithTimeout()
@unittest.skipUnless(HAVE_SOCKET_ALG, 'AF_ALG required')
class LinuxKernelCryptoAPI(unittest.TestCase):
-2
View File
@@ -146,7 +146,6 @@ class StrtodTests(unittest.TestCase):
digits *= 5
exponent -= 1
@unittest.skip("TODO: RUSTPYTHON; fails on debug mode, flaky in release mode")
def test_halfway_cases(self):
# test halfway cases for the round-half-to-even rule
for i in range(100 * TEST_SIZE):
@@ -213,7 +212,6 @@ class StrtodTests(unittest.TestCase):
s = '{}e{}'.format(digits, exponent)
self.check_strtod(s)
@unittest.skip("TODO: RUSTPYTHON; flaky test")
def test_parsing(self):
# make '0' more likely to be chosen than other digits
digits = '000000123456789'
-2
View File
@@ -3051,7 +3051,6 @@ class POSIXProcessTestCase(BaseTestCase):
p1.stdout.close()
p2.stdout.close()
@unittest.skip("TODO: RUSTPYTHON; flaky test")
def test_close_fds(self):
fd_status = support.findfile("fd_status.py", subdir="subprocessdata")
@@ -3183,7 +3182,6 @@ class POSIXProcessTestCase(BaseTestCase):
# descriptor of a pipe closed in the parent process is valid in the
# child process according to fstat(), but the mode of the file
# descriptor is invalid, and read or write raise an error.
@unittest.skip("TODO: RUSTPYTHON; flaky test")
@support.requires_mac_ver(10, 5)
def test_pass_fds(self):
fd_status = support.findfile("fd_status.py", subdir="subprocessdata")
-1
View File
@@ -1751,7 +1751,6 @@ class TestTemporaryDirectory(BaseTestCase):
d2.cleanup()
@unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON; flaky, sometimes pollute env on CI")
@unittest.expectedFailureIf(sys.platform in ('android', 'linux'), "TODO: RUSTPYTHON; FileNotFoundError: [Errno 2] No such file or directory: '<LONG_PATH>'")
@os_helper.skip_unless_symlink
def test_cleanup_with_symlink_modes(self):
# cleanup() should not follow symlinks when fixing mode bits (#91133)
-14
View File
@@ -685,7 +685,6 @@ class ThreadTests(BaseTestCase):
self.assertEqual(out, b'')
self.assertEqual(err, b'')
@unittest.skip("TODO: RUSTPYTHON; flaky")
@skip_unless_reliable_fork
def test_is_alive_after_fork(self):
# Try hard to trigger #18418: is_alive() could sometimes be True on
@@ -749,7 +748,6 @@ class ThreadTests(BaseTestCase):
"main ident True\n"
"current is main True\n")
@unittest.skip("TODO: RUSTPYTHON; flaky; process timeout after fork")
@skip_unless_reliable_fork
@unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()")
def test_main_thread_after_fork_from_nonmain_thread(self):
@@ -1470,7 +1468,6 @@ class ThreadJoinOnShutdown(BaseTestCase):
"""
self._run_and_join(script)
@unittest.skip("TODO: RUSTPYTHON; flaky test")
@skip_unless_reliable_fork
def test_3_join_in_forked_from_thread(self):
# Like the test above, but fork() was called from a worker thread
@@ -1571,7 +1568,6 @@ class ThreadJoinOnShutdown(BaseTestCase):
self.assertEqual(out.strip(), b"OK")
self.assertEqual(rc, 0)
@unittest.skip("TODO: RUSTPYTHON; - flaky, parking_lot mutex not fork-safe")
@skip_unless_reliable_fork
def test_reinit_tls_after_fork(self):
# Issue #13817: fork() would deadlock in a multithreaded program with
@@ -1852,7 +1848,6 @@ class ThreadingExceptionTests(BaseTestCase):
lock = threading.Lock()
self.assertRaises(RuntimeError, lock.release)
@unittest.skip("TODO: RUSTPYTHON; flaky test")
@requires_subprocess()
def test_recursion_limit(self):
# Issue 9670
@@ -2236,10 +2231,6 @@ class CRLockTests(lock_tests.RLockTests):
CustomRLock(1, b=2)
self.assertEqual(warnings_log, [])
@unittest.skip("TODO: RUSTPYTHON; flaky test")
def test_different_thread(self):
return super().test_different_thread()
class EventTests(lock_tests.EventTests):
eventtype = staticmethod(threading.Event)
@@ -2250,10 +2241,6 @@ class ConditionAsRLockTests(lock_tests.RLockTests):
def test_recursion_count(self):
self.skipTest("Condition does not expose _recursion_count()")
@unittest.skip("TODO: RUSTPYTHON; flaky test")
def test_different_thread(self):
return super().test_different_thread()
class ConditionTests(lock_tests.ConditionTests):
condtype = staticmethod(threading.Condition)
@@ -2413,7 +2400,6 @@ class InterruptMainTests(unittest.TestCase):
# Restore original handler
signal.signal(signum, handler)
@unittest.skip("TODO: RUSTPYTHON; flaky")
@requires_gil_enabled("gh-118433: Flaky due to a longstanding bug")
def test_interrupt_main_subthread(self):
# Calling start_new_thread with a function that executes interrupt_main
-1
View File
@@ -26,7 +26,6 @@ def target(local, weaklist):
class BaseLocalTest:
@unittest.skip("TODO: RUSTPYTHON; flaky test")
def test_local_refs(self):
self._local_refs(20)
self._local_refs(50)
+10
View File
@@ -177,6 +177,16 @@ pub fn chroot(path: &Path) -> std::io::Result<()> {
nix::unistd::chroot(path).map_err(std::io::Error::from)
}
#[cfg(all(unix, not(target_os = "redox")))]
pub fn fchmodat(dirfd: i32, path: &CStr, mode: libc::mode_t, flags: i32) -> std::io::Result<()> {
let ret = unsafe { libc::fchmodat(dirfd, path.as_ptr(), mode, flags) };
if ret == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd"))]
pub fn lchmod(path: &CStr, mode: libc::mode_t) -> std::io::Result<()> {
unsafe extern "C" {
+24 -13
View File
@@ -18,6 +18,22 @@ pub enum ReadlineResult {
Other(OtherError),
}
fn stdio_readline(prompt: &str) -> ReadlineResult {
use std::io::prelude::*;
eprint!("{prompt}");
if let Err(e) = io::stderr().flush() {
return ReadlineResult::Io(e);
}
let line = io::stdin().lock().lines().next();
match line {
Some(Ok(line)) => ReadlineResult::Line(line),
None => ReadlineResult::Eof,
Some(Err(e)) if e.kind() == io::ErrorKind::Interrupted => ReadlineResult::Interrupt,
Some(Err(e)) => ReadlineResult::Io(e),
}
}
#[allow(unused)]
pub mod basic_readline {
use super::*;
@@ -48,19 +64,7 @@ pub mod basic_readline {
}
pub fn readline(&mut self, prompt: &str) -> ReadlineResult {
use std::io::prelude::*;
print!("{prompt}");
if let Err(e) = io::stdout().flush() {
return ReadlineResult::Io(e);
}
let next_line = io::stdin().lock().lines().next();
match next_line {
Some(Ok(line)) => ReadlineResult::Line(line),
None => ReadlineResult::Eof,
Some(Err(e)) if e.kind() == io::ErrorKind::Interrupted => ReadlineResult::Interrupt,
Some(Err(e)) => ReadlineResult::Io(e),
}
stdio_readline(prompt)
}
}
}
@@ -126,6 +130,13 @@ pub mod rustyline_readline {
}
pub fn readline(&mut self, prompt: &str) -> ReadlineResult {
use std::io::IsTerminal;
// Pipes have no termios. Write the prompt to stderr and read
// stdin, matching PyOS_StdioReadline, so `python -i` under
// spawn still emits `>>> `.
if !io::stdin().is_terminal() {
return stdio_readline(prompt);
}
use rustyline::error::ReadlineError;
loop {
break match self.repl.readline(prompt) {
+11
View File
@@ -302,6 +302,17 @@ pub fn clock_getres(id: ClockId) -> std::io::Result<Duration> {
.map_err(std::io::Error::from)
}
#[cfg(target_os = "wasi")]
pub fn clock_getres(id: ClockId) -> std::io::Result<Duration> {
let mut ts = core::mem::MaybeUninit::<libc::timespec>::uninit();
let ret = unsafe { libc::clock_getres(id.as_raw(), ts.as_mut_ptr()) };
if ret != 0 {
return Err(std::io::Error::last_os_error());
}
let ts = unsafe { ts.assume_init() };
Ok(Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32))
}
#[cfg(all(unix, not(target_os = "redox"), not(target_vendor = "apple")))]
pub fn clock_settime(id: ClockId, time: Duration) -> std::io::Result<()> {
let ts = nix::sys::time::TimeSpec::from(time);
+5 -2
View File
@@ -707,8 +707,11 @@ pub(crate) fn check_int_to_str_digits(value: &BigInt, vm: &VirtualMachine) -> Py
return Ok(());
}
// Upper bound on decimal digit count: ⌈bits × log10(2)⌉ + 1, with log10(2) ≈ 0.30103.
let digits_upper = (bits as usize * 30103 / 100000) + 1;
if digits_upper > limit {
// Multiply with the u64 bit count: `bits as usize * 30103` wraps on
// 32-bit (wasm32) once bits ≳ 142_500, which silently accepts over-limit
// conversions.
let digits_upper = bits.saturating_mul(30103) / 100_000 + 1;
if digits_upper > u64::try_from(limit).unwrap_or(u64::MAX) {
return Err(vm.new_value_error(format!(
"Exceeds the limit ({limit} digits) for integer string conversion; \
use sys.set_int_max_str_digits() to increase the limit"
+15 -6
View File
@@ -14,7 +14,6 @@ use crate::{
class::PyClassImpl,
convert::ToPyObject,
function::{ArgSize, Either, FuncArgs, OptionalArg, PyComparisonValue},
iter::PyExactSizeIterator,
protocol::{PyIterReturn, PyMappingMethods, PySequenceMethods},
recursion::ReprGuard,
sequence::{MutObjectSequenceOp, OptionalRangeArgs, SequenceExt, SequenceMutExt},
@@ -590,11 +589,21 @@ impl Comparable for PyList {
return Ok(res.into());
}
let other = class_or_notimplemented!(Self, other);
let a = &*zelf.borrow_vec();
let b = &*other.borrow_vec();
a.iter()
.richcompare(b.iter(), op, vm)
.map(PyComparisonValue::Implemented)
// Item comparison can mutate either list (clear, resize). Holding the
// element lock across that callback deadlocks on the write side.
crate::iter::richcompare_mutating_seqs(
|i| {
let a = zelf.borrow_vec();
(a.len(), a.get(i).cloned())
},
|i| {
let b = other.borrow_vec();
(b.len(), b.get(i).cloned())
},
op,
vm,
)
.map(PyComparisonValue::Implemented)
}
}
+13 -1
View File
@@ -4033,7 +4033,19 @@ impl ExecutingFrame<'_> {
Either::A(coro) => coro
.throw(jen, exc_type, exc_val, exc_tb, vm)
.to_pyresult(vm),
Either::B(meth) => meth.call((exc_type, exc_val, exc_tb), vm),
Either::B(meth) => {
// Omit trailing None so a 1-arg throw() stays 1-arg.
// Passing None fillers makes throw() look like the
// deprecated 3-arg form and warns under -W error.
let args = if !vm.is_none(&exc_tb) {
vec![exc_type, exc_val, exc_tb]
} else if !vm.is_none(&exc_val) {
vec![exc_type, exc_val]
} else {
vec![exc_type]
};
meth.call(args, vm)
}
};
return ret.map(ExecutionResult::Yield).or_else(|err| {
// Add traceback entry for the yield-from/await point.
+48
View File
@@ -51,3 +51,51 @@ pub trait PyExactSizeIterator<'a>: ExactSizeIterator<Item = &'a PyObjectRef> + S
}
impl<'a, T> PyExactSizeIterator<'a> for T where T: ExactSizeIterator<Item = &'a PyObjectRef> + Sized {}
/// Compare two sequences whose item-comparison callbacks may mutate the
/// containers. `get(i)` must snapshot `(len, item_at_i)` and drop any
/// container lock before returning.
///
/// After an unequal item, sizes are read again: if a callback shrank a
/// sequence so `i` is now past the end, the result is a length comparison
/// rather than the item inequality.
pub fn richcompare_mutating_seqs(
mut get_a: impl FnMut(usize) -> (usize, Option<PyObjectRef>),
mut get_b: impl FnMut(usize) -> (usize, Option<PyObjectRef>),
op: PyComparisonOp,
vm: &VirtualMachine,
) -> PyResult<bool> {
if matches!(op, PyComparisonOp::Eq | PyComparisonOp::Ne) {
let (a_len, _) = get_a(0);
let (b_len, _) = get_b(0);
if a_len != b_len {
return Ok(op == PyComparisonOp::Ne);
}
}
let mut i = 0usize;
loop {
let (a_len, a_item) = get_a(i);
let (b_len, b_item) = get_b(i);
let (Some(a_item), Some(b_item)) = (a_item, b_item) else {
return Ok(op.eval_ord(a_len.cmp(&b_len)));
};
if vm.bool_eq(&a_item, &b_item)? {
i += 1;
continue;
}
let (a_len, _) = get_a(i);
let (b_len, _) = get_b(i);
if i >= a_len || i >= b_len {
return Ok(op.eval_ord(a_len.cmp(&b_len)));
}
return match op {
PyComparisonOp::Eq => Ok(false),
PyComparisonOp::Ne => Ok(true),
_ => a_item.rich_compare_bool(&b_item, op, vm),
};
}
}
+13 -6
View File
@@ -13,7 +13,6 @@ mod _collections {
common::lock::{PyMutex, PyRwLock, PyRwLockReadGuard, PyRwLockWriteGuard},
convert::ToPyObject,
function::{FuncArgs, KwArgs, OptionalArg, PyComparisonValue},
iter::PyExactSizeIterator,
object::{Traverse, TraverseFn},
protocol::{PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods},
recursion::ReprGuard,
@@ -587,11 +586,19 @@ mod _collections {
}
let other = class_or_notimplemented!(Self, other);
let lhs = zelf.borrow_deque();
let rhs = other.borrow_deque();
lhs.iter()
.richcompare(rhs.iter(), op, vm)
.map(PyComparisonValue::Implemented)
crate::iter::richcompare_mutating_seqs(
|i| {
let lhs = zelf.borrow_deque();
(lhs.len(), lhs.get(i).cloned())
},
|i| {
let rhs = other.borrow_deque();
(rhs.len(), rhs.get(i).cloned())
},
op,
vm,
)
.map(PyComparisonValue::Implemented)
}
}
+29 -12
View File
@@ -11,9 +11,22 @@ pub(super) use crate::vm::resolve_frozen_alias;
#[pymodule(sub)]
mod lock {
use crate::{PyResult, VirtualMachine, stdlib::_thread::RawRMutex};
use core::cell::Cell;
static IMP_LOCK: RawRMutex = RawRMutex::INIT;
thread_local! {
static IMP_LOCK_DEPTH: Cell<usize> = const { Cell::new(0) };
}
fn bump_depth() {
IMP_LOCK_DEPTH.with(|c| c.set(c.get() + 1));
}
fn drop_depth() {
IMP_LOCK_DEPTH.with(|c| c.set(c.get().saturating_sub(1)));
}
#[pyfunction]
fn acquire_lock(vm: &VirtualMachine) {
// Detach while blocking on IMP_LOCK. The import lock is held across
@@ -27,10 +40,11 @@ mod lock {
#[pyfunction]
fn release_lock(vm: &VirtualMachine) -> PyResult<()> {
if !IMP_LOCK.is_locked() {
if !IMP_LOCK.is_locked() || !IMP_LOCK.is_owned_by_current_thread() {
Err(vm.new_runtime_error("Global import lock not held"))
} else {
unsafe { IMP_LOCK.unlock() };
drop_depth();
Ok(())
}
}
@@ -42,41 +56,44 @@ mod lock {
pub(super) fn acquire_lock_for_fork() {
IMP_LOCK.lock();
bump_depth();
}
#[cfg(all(unix, feature = "host_env"))]
pub(super) fn release_lock_after_fork_parent() {
if IMP_LOCK.is_locked() && IMP_LOCK.is_owned_by_current_thread() {
unsafe { IMP_LOCK.unlock() };
drop_depth();
}
}
/// Reset import lock after fork() — only if held by a dead thread.
/// Reset the import lock after `fork()`.
///
/// `IMP_LOCK` is a reentrant mutex. If the *current* (surviving) thread
/// held it at fork time, the child must be able to release it normally.
/// Only reset if a now-dead thread was the owner.
/// Zero the lock so waiter queues from dead threads are discarded, then
/// re-acquire it as many times as this thread held it. `unlock()` on the
/// inherited lock would unpark those waiters.
///
/// # Safety
///
/// Must only be called from single-threaded child after fork().
#[cfg(all(unix, feature = "host_env"))]
pub(crate) unsafe fn reinit_after_fork() {
if IMP_LOCK.is_locked() && !IMP_LOCK.is_owned_by_current_thread() {
// Held by a dead thread — reset to unlocked.
unsafe { rustpython_common::lock::zero_reinit_after_fork(&IMP_LOCK) };
let depth = IMP_LOCK_DEPTH.with(Cell::get);
unsafe { rustpython_common::lock::zero_reinit_after_fork(&IMP_LOCK) };
for _ in 0..depth {
IMP_LOCK.lock();
}
}
/// Match CPython's `_PyImport_ReInitLock()` + `_PyImport_ReleaseLock()`
/// behavior in the post-fork child:
/// 1) if ownership metadata is stale (dead owner / changed tid), reset;
/// 2) if current thread owns the lock, release it.
/// Restore the child's import lock, then drop the extra hold taken
/// before `fork()`. Nested `imp.acquire_lock()` holds stay so the
/// child's `release_lock()` can unwind them.
#[cfg(all(unix, feature = "host_env"))]
pub(super) unsafe fn after_fork_child_reinit_and_release() {
unsafe { reinit_after_fork() };
if IMP_LOCK.is_locked() && IMP_LOCK.is_owned_by_current_thread() {
unsafe { IMP_LOCK.unlock() };
drop_depth();
}
}
}
+18 -4
View File
@@ -1399,14 +1399,20 @@ pub(crate) mod _thread {
}
inner_guard.state = ThreadHandleState::Done;
inner_guard.join_handle = None;
// The OS thread did not survive the fork. Dropping JoinHandle
// would pthread_detach a copied thread descriptor.
if let Some(handle) = inner_guard.join_handle.take() {
core::mem::forget(handle);
}
drop(inner_guard);
// Reinit and set the done event
// Reinit and set the done event. Do not notify: no other
// thread exists in the child, and notify_all would unpark
// waiters that did not survive the fork.
let (lock, cvar) = &*done_event;
reinit_parking_lot_mutex(lock);
reinit_parking_lot_condvar(cvar);
*lock.lock() = true;
cvar.notify_all();
true
});
@@ -1438,8 +1444,8 @@ pub(crate) mod _thread {
let (lock, cvar) = &*done_event;
reinit_parking_lot_mutex(lock);
reinit_parking_lot_condvar(cvar);
*lock.lock() = true;
cvar.notify_all();
false
});
@@ -1465,6 +1471,14 @@ pub(crate) mod _thread {
unsafe { rustpython_common::lock::zero_reinit_after_fork(mutex.raw()) };
}
/// Reset a parking_lot::Condvar after fork.
///
/// `notify_all()` would unpark waiters that did not survive the fork.
#[cfg(all(unix, feature = "host_env"))]
fn reinit_parking_lot_condvar(cvar: &parking_lot::Condvar) {
unsafe { rustpython_common::lock::zero_reinit_after_fork(cvar) };
}
// Thread handle state enum
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThreadHandleState {
+35 -6
View File
@@ -614,8 +614,8 @@ pub mod module {
#[cfg(feature = "threading")]
crate::stdlib::_thread::after_fork_child(vm);
// CPython parity: reinit import lock ownership metadata in child
// and release the lock acquired by PyOS_BeforeFork().
// Reinit import lock ownership metadata in child and release the lock
// acquired before fork.
#[cfg(feature = "threading")]
unsafe {
crate::stdlib::_imp::after_fork_child_imp_lock_release()
@@ -817,15 +817,18 @@ pub mod module {
Ok(0)
}
Ok(pid) => {
// Match CPython timing: capture this before parent after-fork hooks
// in case those hooks start threads.
// Capture this before parent after-fork hooks in case those
// hooks start threads.
let num_os_threads = get_number_of_os_threads();
py_os_after_fork_parent(vm);
// Match CPython timing: warn only after parent callback path resumes world.
// Warn only after parent callback path resumes the world.
warn_if_multi_threaded("fork", num_os_threads, vm);
Ok(pid)
}
Err(err) => Err(err.into_pyexception(vm)),
Err(err) => {
py_os_after_fork_parent(vm);
Err(err.into_pyexception(vm))
}
}
}
@@ -1002,6 +1005,32 @@ pub mod module {
vm: &VirtualMachine,
) -> PyResult<()> {
let [] = dir_fd.0;
#[cfg(all(
unix,
not(target_os = "redox"),
not(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd"))
))]
if !follow_symlinks.0 {
let err_path = path.clone();
let c_path = path.into_cstring(vm)?;
return rustpython_host_env::posix::fchmodat(
libc::AT_FDCWD,
&c_path,
mode as libc::mode_t,
libc::AT_SYMLINK_NOFOLLOW,
)
.map_err(|err| {
let enotsup = err.raw_os_error() == Some(libc::EOPNOTSUPP)
|| err.raw_os_error() == Some(libc::ENOTSUP);
if enotsup {
vm.new_not_implemented_error(
"chmod: follow_symlinks unavailable on this platform".to_owned(),
)
} else {
OSErrorBuilder::with_filename(&err, err_path, vm)
}
});
}
let err_path = path.clone();
let body = move || {
use std::os::unix::fs::PermissionsExt;
+2 -2
View File
@@ -248,9 +248,9 @@ pub mod sys {
};
#[pyattr(name = "ps1")]
const PS1: &str = ">>>>> ";
const PS1: &str = ">>> ";
#[pyattr(name = "ps2")]
const PS2: &str = "..... ";
const PS2: &str = "... ";
#[cfg(windows)]
#[pyattr(name = "_vpath")]
+39
View File
@@ -21,6 +21,11 @@ mod decl {
function::{Either, FuncArgs, OptionalArg},
types::{PyStructSequence, PyStructSequenceData, struct_sequence_new},
};
#[cfg(target_os = "wasi")]
use crate::{
PyRef,
builtins::{PyNamespace, PyUtf8StrRef},
};
#[cfg(any(unix, windows))]
use crate::{
common::wtf8::Wtf8Buf,
@@ -75,6 +80,40 @@ mod decl {
get_clock_time(ClockId::CLOCK_MONOTONIC, vm)
}
#[cfg(target_os = "wasi")]
fn clock_getres(id: ClockId, vm: &VirtualMachine) -> PyResult<f64> {
host_time::clock_getres(id)
.map(|d| d.as_secs_f64())
.map_err(|err| vm.new_os_error(err.to_string()))
}
#[cfg(target_os = "wasi")]
#[pyfunction]
fn get_clock_info(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<PyRef<PyNamespace>> {
let (adj, imp, mono, res) = match name.as_str() {
"monotonic" | "perf_counter" => (
false,
"time.clock_gettime(CLOCK_MONOTONIC)",
true,
clock_getres(ClockId::CLOCK_MONOTONIC, vm)?,
),
"time" => (
true,
"time.clock_gettime(CLOCK_REALTIME)",
false,
clock_getres(ClockId::CLOCK_REALTIME, vm)?,
),
_ => return Err(vm.new_value_error("unknown clock")),
};
Ok(py_namespace!(vm, {
"implementation" => vm.new_pyobj(imp),
"monotonic" => vm.ctx.new_bool(mono),
"adjustable" => vm.ctx.new_bool(adj),
"resolution" => vm.ctx.new_float(res),
}))
}
#[cfg(not(any(unix, windows, target_os = "wasi")))]
fn get_monotonic_time(vm: &VirtualMachine) -> PyResult<Duration> {
duration_since_system_now(vm)
+9 -2
View File
@@ -24,8 +24,9 @@ mod vm_ops;
use crate::{
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult,
builtins::{
self, PyBaseExceptionRef, PyBaseObject, PyDict, PyDictRef, PyInt, PyList, PyModule, PyStr,
PyStrInterned, PyStrRef, PyTypeRef, PyUtf8Str, PyUtf8StrInterned, PyWeak,
self, PyBaseExceptionRef, PyBaseObject, PyDict, PyDictRef, PyFrozenSet, PyInt, PyList,
PyModule, PySet, PyStr, PyStrInterned, PyStrRef, PyTypeRef, PyUtf8Str, PyUtf8StrInterned,
PyWeak,
code::PyCode,
dict::{PyDictItems, PyDictKeys, PyDictValues},
pystr::AsPyStr,
@@ -3288,6 +3289,12 @@ impl VirtualMachine {
i += 1;
}
return Ok(results);
} else if cls.is(self.ctx.types.set_type) {
let keys = value.downcast_ref::<PySet>().unwrap().elements();
return map_known_len(keys.into_iter(), func);
} else if cls.is(self.ctx.types.frozenset_type) {
let keys = value.downcast_ref::<PyFrozenSet>().unwrap().elements();
return map_known_len(keys.into_iter(), func);
} else if cls.is(self.ctx.types.dict_type) {
let keys = value.downcast_ref::<PyDict>().unwrap().keys_vec();
return map_known_len(keys.into_iter(), func);
+10 -3
View File
@@ -113,6 +113,14 @@ fn shell_exec(
}
}
fn flush_stdio(vm: &VirtualMachine) {
for name in ["stdout", "stderr"] {
if let Ok(stream) = vm.sys_module.get_attr(name, vm) {
let _ = vm.call_method(&stream, "flush", ());
}
}
}
/// Enter a repl loop
pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> {
let mut repl = Readline::new(helper::ShellHelper::new(vm, scope.globals.clone()));
@@ -128,9 +136,7 @@ pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> {
None => ".repl_history.txt".into(),
};
if repl.load_history(&repl_history_path).is_err() {
println!("No previous history.");
}
let _ = repl.load_history(&repl_history_path);
// We might either be waiting to know if a block is complete, or waiting to know if a multiline
// statement is complete. In the former case, we need to ensure that we read one extra new line
@@ -245,6 +251,7 @@ pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> {
}
vm.print_exception(exc);
}
flush_stdio(vm);
}
repl.save_history(&repl_history_path).unwrap();