From d76c9a9e387dd467ab79a61a1416520cfee051ce Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 24 Jul 2026 01:22:22 +0900 Subject: [PATCH 01/11] io: dispatch close dynamically from the context-manager exit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IOBase.__exit__ and the buffered __exit__ called the native close directly, so a Python subclass close() override never ran inside a with-block — zipfile's _ZipWriteFile.close() was skipped and the archive kept its open-writing-handle state. Dispatch self.close() dynamically, the shape iobase_del already uses. Assisted-by: Claude --- pyre/pyre-interpreter/src/module/_io/mod.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyre/pyre-interpreter/src/module/_io/mod.rs b/pyre/pyre-interpreter/src/module/_io/mod.rs index 58183c9a5e8..a7808eff468 100644 --- a/pyre/pyre-interpreter/src/module/_io/mod.rs +++ b/pyre/pyre-interpreter/src/module/_io/mod.rs @@ -496,7 +496,11 @@ fn init_iobase_type(ns: PyObjectRef) { ns, "__exit__", crate::make_builtin_function("__exit__", |args| { - iobase_close(&args[..1])?; + // Dispatch `close` dynamically (`W_IOBase._exit` calls + // `space.call_method(self, "close")`) so a Python subclass + // override runs; a static `iobase_close` would mark the object + // closed without ever running the override. + call_method_result(args[0], "close", &[])?; Ok(w_none()) }), ); @@ -842,7 +846,8 @@ fn init_buffered_reader_type(ns: PyObjectRef) { ns, "__exit__", crate::make_builtin_function("__exit__", |args| { - buffered_reader_close(&args[..1])?; + // Dynamic dispatch, as on the IOBase `__exit__` above. + call_method_result(args[0], "close", &[])?; Ok(w_none()) }), ); From 63b48dd355402a0493366b1512c43ca7bb29cf9e Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 24 Jul 2026 01:22:22 +0900 Subject: [PATCH 02/11] import: install the zipimporter path hook after the bootstrap install_importlib_bootstrap now inserts zipimport.zipimporter at the front of the live sys.path_hooks list once the external importers are installed, so zip archives on sys.path are importable. A failed zipimport import leaves the hook out instead of failing the bootstrap. Assisted-by: Claude --- pyre/pyre-interpreter/src/importing.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyre/pyre-interpreter/src/importing.rs b/pyre/pyre-interpreter/src/importing.rs index 7b24cadb5db..3205ff05431 100644 --- a/pyre/pyre-interpreter/src/importing.rs +++ b/pyre/pyre-interpreter/src/importing.rs @@ -2225,6 +2225,26 @@ fn install_importlib_bootstrap( "_frozen_importlib", shadow_stack_get(module_slot), )?; + + // `sys.path_hooks.insert(0, zipimporter)` (zipimport moduledef startup / + // pylifecycle.c init after the external importers) so zip archives on + // `sys.path` are importable. `zipimport` is served from the frozen table + // and its body imports `_frozen_importlib`, hence after the alias above. + // A failed import leaves the hook out — the tolerant `# can't import + // zipimport` path — rather than failing the whole bootstrap. + if let Ok(w_zipimport) = absolute_import("zipimport", pyre_object::PY_NULL, execution_context) { + let zipimport_slot = shadow_stack_len(); + pin_root(w_zipimport); + let w_zipimporter = + crate::baseobjspace::getattr_str(shadow_stack_get(zipimport_slot), "zipimporter")?; + let zipimporter_slot = shadow_stack_len(); + pin_root(w_zipimporter); + let w_path_hooks = + crate::baseobjspace::getattr_str(shadow_stack_get(sys_slot), "path_hooks")?; + unsafe { + pyre_object::w_list_insert(w_path_hooks, 0, shadow_stack_get(zipimporter_slot)); + } + } Ok(()) } From f0d55b7a2245c1ce7719cc28a868e06afd231c66 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 24 Jul 2026 01:22:22 +0900 Subject: [PATCH 03/11] marshal: accept any readable buffer in loads and load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loads/load only took bytes and bytearray; a sliced memoryview — what SourcelessFileLoader.get_code passes for the pyc payload — raised TypeError. Route non-bytes inputs through buffer_as_bytes_like. Assisted-by: Claude --- pyre/pyre-interpreter/src/module/marshal/mod.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyre/pyre-interpreter/src/module/marshal/mod.rs b/pyre/pyre-interpreter/src/module/marshal/mod.rs index 7a3852e963c..941b0492f41 100644 --- a/pyre/pyre-interpreter/src/module/marshal/mod.rs +++ b/pyre/pyre-interpreter/src/module/marshal/mod.rs @@ -47,12 +47,17 @@ fn call_method(obj: PyObjectRef, name: &str, args: &[PyObjectRef]) -> PyResult { fn bytes_like(obj: PyObjectRef, function: &str) -> Result, PyError> { if unsafe { bytesobject::is_bytes_like(obj) } { - Ok(unsafe { bytesobject::bytes_like_data(obj) }.to_vec()) - } else { - Err(PyError::type_error(format!( - "{function}() argument must be a bytes-like object" - ))) + return Ok(unsafe { bytesobject::bytes_like_data(obj) }.to_vec()); + } + // Any readable buffer is accepted (`interp_marshal` unwraps via + // `space.readbuf_w`): `SourcelessFileLoader.get_code` hands `loads` a + // sliced memoryview of the pyc payload. + if let Some(src) = crate::typedef::buffer_as_bytes_like(obj)? { + return Ok(unsafe { bytesobject::bytes_like_data(src) }.to_vec()); } + Err(PyError::type_error(format!( + "{function}() argument must be a bytes-like object" + ))) } /// Transient equivalent of PyPy's `Marshaller.all_refs` dict. A VecMap is From 43786915d67ecad3de28a5c338491c6d8fd308b1 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 24 Jul 2026 01:22:22 +0900 Subject: [PATCH 04/11] imp: implement source_hash with siphash-2-4 The stub returned int 0, so _code_to_hash_pyc's len(source_hash) == 8 assert failed when writing hash-based pycs. Port interp_imp.py source_hash: siphash-2-4 keyed by the pyc magic, 8 bytes little-endian. Assisted-by: Claude --- .../src/module/imp/interp_imp.rs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/pyre/pyre-interpreter/src/module/imp/interp_imp.rs b/pyre/pyre-interpreter/src/module/imp/interp_imp.rs index 4f18a401a4c..9a0fa7d5499 100644 --- a/pyre/pyre-interpreter/src/module/imp/interp_imp.rs +++ b/pyre/pyre-interpreter/src/module/imp/interp_imp.rs @@ -408,7 +408,28 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { "source_hash", crate::make_builtin_function_with_arity( "source_hash", - |_| Ok(pyre_object::w_int_new(0)), + |args| { + // `interp_imp.py source_hash`: siphash-2-4 of the source + // bytes keyed by the pyc magic (k0=magic, k1=0), serialized + // low-byte-first — the 8-byte hash field of hash-based pycs + // (`_code_to_hash_pyc` asserts `len(source_hash) == 8`). + use std::hash::Hasher; + let magic = crate::baseobjspace::int_w(args[0])? as u64; + let content = if unsafe { pyre_object::bytesobject::is_bytes_like(args[1]) } { + unsafe { pyre_object::bytesobject::bytes_like_data(args[1]) }.to_vec() + } else if let Some(src) = crate::typedef::buffer_as_bytes_like(args[1])? { + unsafe { pyre_object::bytesobject::bytes_like_data(src) }.to_vec() + } else { + return Err(crate::PyError::type_error( + "source_hash() argument 2 must be a bytes-like object", + )); + }; + let mut hasher = siphasher::sip::SipHasher24::new_with_keys(magic, 0); + hasher.write(&content); + Ok(pyre_object::bytesobject::w_bytes_from_bytes( + &hasher.finish().to_le_bytes(), + )) + }, 2, ), ); From 3f6b353b4c983d7e7cfbd9e4417e301636a4a8dd Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 24 Jul 2026 01:22:22 +0900 Subject: [PATCH 05/11] jit: treat str and bytes as immutable inplace receivers in the walk gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inplace-BinaryOp journal gate declined exact str/bytes receivers into InplaceContainerMutationUnsupported, a permanent abort whose resume path drops the in-flight FOR_ITER item — posixpath.join lost one path component once per process when its loop hit the compile threshold mid-call. str/bytes += yields a fresh object and rebinds the journaled local, the same argument as the existing int/bool/float/tuple arm. Assisted-by: Claude --- pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs index e047fb68d21..8f6b642392f 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs @@ -1153,9 +1153,9 @@ pub(crate) fn try_execute_residual_call_via_executor( // Integer-strategy, so `w_list_int_set_len` can rewind it. Capture the // pre-extend length; the success arm journals it so the abort rollback // undoes the one extend and the deliver re-applies it exactly once. - // * an immutable receiver (`int`/`bool`/`float`/`tuple`) — `+=` yields a - // FRESH object and rebinds the journaled local, so a plain deliver re-run - // is exact with no journaling. + // * an immutable receiver (`int`/`bool`/`float`/`tuple`/`str`/`bytes`) — + // `+=` yields a FRESH object and rebinds the journaled local, so a plain + // deliver re-run is exact with no journaling. // // Any OTHER *exact builtin* receiver — an object-/float-strategy list, // `bytearray`, `set`, `dict`, `array`, a mixed `int-list += non-ints` that @@ -1188,6 +1188,8 @@ pub(crate) fn try_execute_residual_call_via_executor( || pyre_object::pyobject::is_bool(lhs) || pyre_object::pyobject::is_float(lhs) || pyre_object::pyobject::is_tuple(lhs) + || pyre_object::unicodeobject::is_str(lhs) + || pyre_object::bytesobject::is_bytes(lhs) { None } else if pyre_object::pyobject::is_exact_builtin_instance(lhs) { From eeb31c72c4318d507ab9f291a6fce2ed1598477b Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 24 Jul 2026 01:22:22 +0900 Subject: [PATCH 06/11] pyrex: accept the -u flag script_helper spawns children with sys.executable -E -u; the parser rejected -u with exit 2. pyre's stdio wrappers already write through to the fd on every call, so the flag is accepted as a no-op. Assisted-by: Claude --- pyre/pyrex/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyre/pyrex/src/lib.rs b/pyre/pyrex/src/lib.rs index 5dcb52336ea..a30aa8e88a5 100644 --- a/pyre/pyrex/src/lib.rs +++ b/pyre/pyrex/src/lib.rs @@ -194,6 +194,11 @@ fn parse_args(binary_name: &str) -> Result<(RunMode, LaunchFlags, Vec), } } Short('O') => {} // no-op + // Unbuffered stdio: pyre's stdout/stderr wrappers already write + // through to the fd on every call, so the flag has nothing left + // to disable; accepting it keeps `script_helper`-style spawns + // (`sys.executable -E -u script`) working. + Short('u') => {} Short('q') => flags.quiet = true, Short('s') => flags.no_user_site = true, Short('S') => flags.no_site = true, From 2e5c0057f169eddd1dbcf89615dab6e435d80cdd Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 24 Jul 2026 01:22:22 +0900 Subject: [PATCH 07/11] posix: implement chmod os.chmod was a silent no-op stub, so os_helper.can_chmod() observed an unchanging st_mode and skipped chmod-dependent tests. Register a real path-based chmod next to fchmod; the sandbox name set already neutralises it. Assisted-by: Claude --- .../src/module/posix/interp_posix.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs index 947c4bf8f70..29547f1bc1f 100644 --- a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs +++ b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs @@ -2625,6 +2625,31 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { }), ); + // os.chmod(path, mode) -> None + #[cfg(not(feature = "sandbox"))] + crate::module_ns_store( + ns, + "chmod", + crate::make_builtin_function_with_arity( + "chmod", + |args| { + if args.len() < 2 { + return Err(crate::PyError::type_error("chmod() requires 2 arguments")); + } + let path = extract_path(args[0])?; + let mode = (unsafe { pyre_object::w_int_get_value(args[1]) }) as u32; + let c_path = std::ffi::CString::new(path.as_bytes()) + .map_err(|_| crate::PyError::value_error("embedded null in path"))?; + let ret = unsafe { libc::chmod(c_path.as_ptr(), mode as libc::mode_t) }; + if ret < 0 { + return Err(io_err(std::io::Error::last_os_error(), &path)); + } + Ok(pyre_object::w_none()) + }, + 2, + ), + ); + // os.fchmod(fd, mode) -> None crate::module_ns_store( ns, From 310d1a0db9f160eb941ef705a0df64cf151dfc3d Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 24 Jul 2026 03:40:51 +0900 Subject: [PATCH 08/11] module: trace w_class in the module GC shape Add the `w_class` slot to `W_MODULE_GC_PTR_OFFSETS` so a Module keeps its class reachable. For a `types.ModuleType` subclass instance the class is a collectible heap `W_TypeObject`; when the module is its only referent, an untraced slot let a major collection sweep the class and left `type(m)` and slot dispatch pointing at freed memory. Assisted-by: Claude --- pyre/pyre-object/src/module.rs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/pyre/pyre-object/src/module.rs b/pyre/pyre-object/src/module.rs index 5b94ea2de08..f2a5a655a99 100644 --- a/pyre/pyre-object/src/module.rs +++ b/pyre/pyre-object/src/module.rs @@ -33,13 +33,26 @@ pub const W_MODULE_GC_TYPE_ID: u32 = 36; /// Fixed payload size (`framework.py:811`). pub const W_MODULE_OBJECT_SIZE: usize = std::mem::size_of::(); -/// Byte offset of the inline `w_dict: PyObjectRef` slot — the GC must -/// trace the aliased `W_DictObject` (`pypy/interpreter/module.py:22 -/// self.w_dict = w_dict`) so a Module surviving a minor collection -/// keeps the user-supplied dict alive. `name`/`dict` are non-PyObject -/// raw heap pointers and are intentionally absent; they are owned via -/// `lltype::malloc_raw` and traced through their own type ids. -pub const W_MODULE_GC_PTR_OFFSETS: [usize; 1] = [std::mem::offset_of!(Module, w_dict)]; +/// Byte offsets of the inline `PyObjectRef` slots the GC must trace. +/// +/// `w_dict` — the aliased `W_DictObject` (`pypy/interpreter/module.py:22 +/// self.w_dict = w_dict`) so a Module surviving a collection keeps its +/// dict alive. +/// +/// `w_class` — the module's class. For a `types.ModuleType` subclass +/// instance this is a heap-allocated (GC-managed, collectible) +/// `W_TypeObject`; if the module were its only reference, an untraced +/// slot would let a major collection sweep the class and leave +/// `type(m)` / slot dispatch pointing at freed memory. `W_ObjectObject` +/// traces its `w_class` for the same reason (`object_object_custom_trace`). +/// +/// `name`/`dict` are non-PyObject raw heap pointers and are intentionally +/// absent; they are owned via `lltype::malloc_raw` and traced through +/// their own type ids. +pub const W_MODULE_GC_PTR_OFFSETS: [usize; 2] = [ + std::mem::offset_of!(Module, ob_header.w_class), + std::mem::offset_of!(Module, w_dict), +]; impl crate::lltype::GcType for Module { fn type_id() -> u32 { From 675f71c2ea2300ff8a7fcdb6205bbff2b0805fb8 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 24 Jul 2026 03:40:59 +0900 Subject: [PATCH 09/11] imp: serve the default frozen table at override mode 0 `frozen_module_served` treated mode 0 the same as a negative override, so at the default `_override_frozen_modules_for_tests` setting only the essential bootstrap set was served. Serve the whole frozen table when mode is non-negative; a negative mode still keeps only the essential bootstrap set. Assisted-by: Claude --- pyre/pyre-interpreter/src/module/imp/interp_imp.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyre/pyre-interpreter/src/module/imp/interp_imp.rs b/pyre/pyre-interpreter/src/module/imp/interp_imp.rs index 9a0fa7d5499..d83da69dcc4 100644 --- a/pyre/pyre-interpreter/src/module/imp/interp_imp.rs +++ b/pyre/pyre-interpreter/src/module/imp/interp_imp.rs @@ -119,7 +119,11 @@ fn is_bootstrap_frozen(name: &str) -> bool { fn frozen_module_served(entry: &FrozenModule) -> bool { let mode = FROZEN_OVERRIDE.load(Ordering::Relaxed); - mode > 0 || (mode <= 0 && is_bootstrap_frozen(entry.name)) + // `_override_frozen_modules_for_tests`: 0 is the default (the normal + // frozen table is enabled), a positive value forces frozen modules on, + // and a negative value disables the non-essential ones, keeping only the + // essential bootstrap set frozen. + mode >= 0 || is_bootstrap_frozen(entry.name) } fn served_frozen_module(name: &str) -> Option<&'static FrozenModule> { From effb5cdf6a15442804e6389b86b122c1a44f1e4d Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 24 Jul 2026 03:40:59 +0900 Subject: [PATCH 10/11] module: fall back to type __getattr__ when a replaced __getattribute__ raises When a module's `__getattribute__` slot is replaced (for example importlib.util._LazyModule) and the replacement raises AttributeError, route the miss to the receiver type's `__getattr__` per descroperation.py:242-245 instead of the module-dict PEP 562 `__getattr__`; re-raise when the type has no `__getattr__`. Assisted-by: Claude --- pyre/pyre-interpreter/src/baseobjspace.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 753b2ec966c..842a7ce53bd 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -4672,8 +4672,13 @@ fn getattr_str_impl(obj: PyObjectRef, name: &str, call_getattr: bool) -> PyResul let name_obj = w_str_new(name); match get_and_call_function(slot, obj, w_type, &[name_obj]) { Ok(v) => return Ok(v), + // A replacement `__getattribute__` has taken over the + // whole lookup, so the PEP 562 module-dict tail no + // longer applies; `descroperation.py:242-245` falls + // back to the receiver type's `__getattr__` (and + // re-raises when there is none). Err(e) if e.kind == PyErrorKind::AttributeError => { - return module_getattr_hook_or_err(obj, name, e, call_getattr); + return instance_getattr_hook_or_err(w_type, obj, name, e); } Err(e) => return Err(e), } From e5dcb4b3d197cc5166b7c828f1eb3a903f2992ef Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 24 Jul 2026 03:41:04 +0900 Subject: [PATCH 11/11] import: strip importlib bootstrap frames from import-error tracebacks `dunder_import` now runs `strip_bootstrap_traceback_frames` on the slow-path `__import__` result, dropping the leading traceback entries whose code filename belongs to `importlib/_bootstrap{,_external}.py` (or the frozen pseudo-names). The walk stops at the first non-bootstrap frame, so an erroring imported module keeps its own frames. Mirrors `remove_traceback_module_frames` at interp_import.py:98. Assisted-by: Claude --- pyre/pyre-interpreter/src/importing.rs | 46 +++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/pyre/pyre-interpreter/src/importing.rs b/pyre/pyre-interpreter/src/importing.rs index 3205ff05431..77780a8fa31 100644 --- a/pyre/pyre-interpreter/src/importing.rs +++ b/pyre/pyre-interpreter/src/importing.rs @@ -2621,6 +2621,49 @@ fn gcd_import_fast(name: &str) -> Result, crate::PyError> { Ok(Some(shadow_stack_get(mod_slot))) } +/// `interp_import.py:98` — `e.remove_traceback_module_frames('', '', ...)`: +/// drop the leading traceback entries that belong to the importlib bootstrap +/// so an import error does not expose its internal `__import__` / +/// `_find_and_load` machinery. pyre runs the bootstrap from the on-disk +/// `importlib/_bootstrap{,_external}.py` sources, so match those filenames as +/// well as the frozen pseudo-names. Only leading (outermost, contiguous) +/// bootstrap frames are removed; a user frame stops the walk, keeping real +/// application frames intact. +fn strip_bootstrap_traceback_frames(mut err: crate::PyError) -> crate::PyError { + use pyre_object::interp_exceptions::{w_exception_get_traceback, w_exception_set_traceback}; + + fn is_bootstrap_filename(path: &str) -> bool { + let norm = path.replace('\\', "/"); + norm.ends_with("importlib/_bootstrap.py") + || norm.ends_with("importlib/_bootstrap_external.py") + || norm == "" + || norm == "" + } + + let exc = err.to_exc_object(); + if exc.is_null() { + return err; + } + unsafe { + let mut tb = w_exception_get_traceback(exc); + while !tb.is_null() && !is_none(tb) { + let w_code = crate::pytraceback::w_pytraceback_get_w_code(tb); + let is_bootstrap = !w_code.is_null() + && crate::pycode::code_get_field(w_code, "co_filename") + .ok() + .filter(|f| pyre_object::is_str(*f)) + .is_some_and(|f| is_bootstrap_filename(&pyre_object::w_str_get_value(f))); + if !is_bootstrap { + break; + } + tb = crate::pytraceback::w_pytraceback_get_w_next(tb); + } + w_exception_set_traceback(exc, tb); + } + err +} + /// `builtins.__import__` — `interp___import__`: a fast path answering /// absolute imports from initialised `sys.modules` entries, the app-level /// `_bootstrap.__import__` (the full `sys.meta_path` / `sys.path_hooks` @@ -2745,7 +2788,8 @@ pub fn dunder_import( shadow_stack_get(call_fromlist_slot), shadow_stack_get(level_slot), ], - ); + ) + .map_err(strip_bootstrap_traceback_frames); } } importhook(