Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion pyre/pyre-interpreter/src/baseobjspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down
66 changes: 65 additions & 1 deletion pyre/pyre-interpreter/src/importing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down Expand Up @@ -2601,6 +2621,49 @@ fn gcd_import_fast(name: &str) -> Result<Option<PyObjectRef>, crate::PyError> {
Ok(Some(shadow_stack_get(mod_slot)))
}

/// `interp_import.py:98` — `e.remove_traceback_module_frames('<frozen
/// importlib._bootstrap>', '<frozen importlib._bootstrap_external>', ...)`:
/// 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 == "<frozen importlib._bootstrap>"
|| norm == "<frozen importlib._bootstrap_external>"
}

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`
Expand Down Expand Up @@ -2725,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(
Expand Down
9 changes: 7 additions & 2 deletions pyre/pyre-interpreter/src/module/_io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}),
);
Expand Down Expand Up @@ -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())
}),
);
Expand Down
29 changes: 27 additions & 2 deletions pyre/pyre-interpreter/src/module/imp/interp_imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -408,7 +412,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,
),
);
Expand Down
15 changes: 10 additions & 5 deletions pyre/pyre-interpreter/src/module/marshal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,17 @@ fn call_method(obj: PyObjectRef, name: &str, args: &[PyObjectRef]) -> PyResult {

fn bytes_like(obj: PyObjectRef, function: &str) -> Result<Vec<u8>, 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
Expand Down
25 changes: 25 additions & 0 deletions pyre/pyre-interpreter/src/module/posix/interp_posix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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 Validate chmod's mode before calling libc

In non-sandbox builds this new os.chmod path reads args[1] with w_int_get_value even though the wrapper only checks arity, so calls such as os.chmod(path, "0644") or any non-int mode reinterpret that object's layout as a W_IntObject and still invoke libc::chmod with an arbitrary mode instead of raising TypeError. Convert the mode through the object-space integer/index helper before the syscall so bad input cannot change permissions unpredictably.

Useful? React with 👍 / 👎.

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,
Expand Down
8 changes: 5 additions & 3 deletions pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1153,9 +1153,9 @@ pub(crate) fn try_execute_residual_call_via_executor<Sym: WalkSym>(
// 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
Expand Down Expand Up @@ -1188,6 +1188,8 @@ pub(crate) fn try_execute_residual_call_via_executor<Sym: WalkSym>(
|| 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) {
Expand Down
27 changes: 20 additions & 7 deletions pyre/pyre-object/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Module>();

/// 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 {
Expand Down
5 changes: 5 additions & 0 deletions pyre/pyrex/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ fn parse_args(binary_name: &str) -> Result<(RunMode, LaunchFlags, Vec<String>),
}
}
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,
Expand Down
Loading