diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index 8999eef80f1..71c5ad18555 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -7591,7 +7591,7 @@ fn make_exc_type_with_doc( /// `UnicodeDecodeError.__new__(cls, *args)` call would inherit the /// typechecking that PyPy keeps confined to `descr_init` — see /// `_new` at `:274-284` (no per-arg validation). -fn make_exc_type_with_init( +pub(crate) fn make_exc_type_with_init( name: &'static str, doc: Option<&'static str>, new_fn: crate::gateway::BuiltinCodeFn, diff --git a/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs b/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs index dbc5d24ac72..e6bc8fdfe59 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs @@ -116,7 +116,7 @@ pub(super) fn cdata_in_dll(args: &[PyObjectRef]) -> Result Result { b"PyOS_snprintf" => return Ok(INTERNAL_PYOS_SNPRINTF), _ => {} } - host_ctypes::lookup_function_symbol_addr(handle, &name_bytes).map_err(|e| { + super::interp_ctypes::lookup_symbol(handle, &name_bytes).map_err(|e| { use host_ctypes::LookupSymbolError as L; if matches!(e, L::LibraryNotFound) { return crate::PyError::value_error("library not found"); diff --git a/pyre/pyre-interpreter/src/module/_ctypes/interp_ctypes.rs b/pyre/pyre-interpreter/src/module/_ctypes/interp_ctypes.rs index 4151b5422a1..3379df528e1 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/interp_ctypes.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/interp_ctypes.rs @@ -1,8 +1,9 @@ //! `_ctypes` — the native surface the CPython `ctypes` package sits on. //! -//! On unix with the `host_env` feature this provides a working end-to-end -//! slice: the dynamic-linker primitives (`dlopen`/`dlsym`/`dlclose`), the -//! scalar data type (`_SimpleCData`, see [`super::cdata`]), the foreign +//! With the `host_env` feature this provides a working end-to-end slice: the +//! dynamic-linker primitives (`dlopen`/`dlsym`/`dlclose` on posix, +//! `LoadLibrary`/`FreeLibrary` on Windows), the scalar data type +//! (`_SimpleCData`, see [`super::cdata`]), the foreign //! function object (`CFuncPtr`, see [`super::funcptr`]), `sizeof`/`addressof`/ //! `byref`/`alignment`/`resize`, and the import-time constants the package //! requires. `Structure`/`Union`/`Array`/`_Pointer`/`CField` are real @@ -10,51 +11,82 @@ //! the buffer-view machinery aliases nested/pointed-to memory. //! //! All host/FFI work is delegated to `rustpython_host_env::ctypes`; the module -//! contains no direct `libc::` FFI. +//! contains no direct `libc::` FFI. The one exception is the Windows loader, +//! which calls `windows-sys` directly: `LoadLibrary` has to reach +//! `LoadLibraryExW`'s flags argument, and that layer's Windows door is +//! `libloading::Library::new`, which has none. The handle is then the +//! `HMODULE` rather than a key into its library cache, so `FreeLibrary` and +//! [`lookup_symbol`] are the plain Win32 calls that go with one. pub fn register_module(ns: pyre_object::PyObjectRef) { - #[cfg(all(unix, feature = "host_env"))] + #[cfg(all(any(unix, windows), feature = "host_env"))] register_host_ctypes(ns); - #[cfg(not(all(unix, feature = "host_env")))] + #[cfg(not(all(any(unix, windows), feature = "host_env")))] register_stub_ctypes(ns); } // ────────────────────────────────────────────────────────────────────── -// Functional surface (unix + host_env) +// Functional surface (host_env) // ────────────────────────────────────────────────────────────────────── -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] fn register_host_ctypes(ns: pyre_object::PyObjectRef) { use rustpython_host_env::ctypes as host_ctypes; - // ── dlopen flags (POSIX) ── - crate::module_ns_store( - ns, - "RTLD_LOCAL", - pyre_object::w_int_new(libc::RTLD_LOCAL as i64), - ); - crate::module_ns_store( - ns, - "RTLD_GLOBAL", - pyre_object::w_int_new(libc::RTLD_GLOBAL as i64), - ); - crate::module_ns_store( - ns, - "RTLD_LAZY", - pyre_object::w_int_new(libc::RTLD_LAZY as i64), - ); - crate::module_ns_store( - ns, - "RTLD_NOW", - pyre_object::w_int_new(libc::RTLD_NOW as i64), - ); + // ── dlopen flags ── + // + // `ctypes/__init__.py:14` imports `RTLD_LOCAL`/`RTLD_GLOBAL` before it + // branches on `os.name`, so both names have to exist wherever the module + // is real; where there is no `dlfcn.h` they are 0, which is the value + // `host_env`'s own pair carries. `RTLD_LAZY`/`RTLD_NOW` have no such + // caller and stay with the platform that defines them. + #[cfg(unix)] + { + crate::module_ns_store( + ns, + "RTLD_LOCAL", + pyre_object::w_int_new(libc::RTLD_LOCAL as i64), + ); + crate::module_ns_store( + ns, + "RTLD_GLOBAL", + pyre_object::w_int_new(libc::RTLD_GLOBAL as i64), + ); + crate::module_ns_store( + ns, + "RTLD_LAZY", + pyre_object::w_int_new(libc::RTLD_LAZY as i64), + ); + crate::module_ns_store( + ns, + "RTLD_NOW", + pyre_object::w_int_new(libc::RTLD_NOW as i64), + ); + } + #[cfg(not(unix))] + { + crate::module_ns_store( + ns, + "RTLD_LOCAL", + pyre_object::w_int_new(host_ctypes::RTLD_LOCAL as i64), + ); + crate::module_ns_store( + ns, + "RTLD_GLOBAL", + pyre_object::w_int_new(host_ctypes::RTLD_GLOBAL as i64), + ); + } crate::module_ns_store( ns, "DEFAULT_MODE", pyre_object::w_int_new(host_ctypes::dlopen_mode(None) as i64), ); + #[cfg(windows)] + register_windows_loader(ns); + // ── dlopen(name, mode=DEFAULT_MODE) → integer handle into host libcache ── + #[cfg(unix)] crate::module_ns_store( ns, "dlopen", @@ -108,6 +140,7 @@ fn register_host_ctypes(ns: pyre_object::PyObjectRef) { ); // ── dlsym(handle, name) → address (int) ── + #[cfg(unix)] crate::module_ns_store( ns, "dlsym", @@ -142,6 +175,7 @@ fn register_host_ctypes(ns: pyre_object::PyObjectRef) { ); // ── dlclose(handle) → None ── + #[cfg(unix)] crate::module_ns_store( ns, "dlclose", @@ -384,9 +418,364 @@ fn register_host_ctypes(ns: pyre_object::PyObjectRef) { ); } +/// Resolve `symbol` in the library a `_handle` names, for `CFuncPtr((name, +/// dll))` and `in_dll`. +/// +/// The two platforms disagree on what a `_handle` is. On posix it is a key +/// into `host_env`'s library cache, which owns the `dlopen` handle and does +/// the `dlsym`. On Windows it is the `HMODULE` `LoadLibrary` returned — that +/// cache cannot take one, and `GetProcAddress` on the module is what +/// `_ctypes.c` does with it anyway. +#[cfg(all(unix, feature = "host_env"))] +pub(super) fn lookup_symbol( + handle: usize, + symbol: &[u8], +) -> Result { + rustpython_host_env::ctypes::lookup_function_symbol_addr(handle, symbol) +} + +#[cfg(all(windows, feature = "host_env"))] +pub(super) fn lookup_symbol( + handle: usize, + symbol: &[u8], +) -> Result { + use rustpython_host_env::ctypes::LookupSymbolError as Error; + if handle == 0 { + return Err(Error::LibraryNotFound); + } + // `GetProcAddress` names the export in the module's own narrow spelling, + // and an embedded NUL would silently truncate the name it looks for. + let Ok(name) = std::ffi::CString::new(symbol) else { + return Err(Error::Load("symbol name contains a null byte".to_string())); + }; + let address = unsafe { + windows_sys::Win32::System::LibraryLoader::GetProcAddress( + handle as *mut core::ffi::c_void, + name.as_ptr().cast(), + ) + }; + match address { + Some(address) => Ok(address as usize), + None => Err(Error::Load( + rustpython_host_env::ctypes::format_error_message(None) + .unwrap_or_else(|| "symbol not found".to_string()), + )), + } +} + +// ────────────────────────────────────────────────────────────────────── +// Windows surface — `_ctypes.c`'s `#ifdef MS_WIN32` module methods +// ────────────────────────────────────────────────────────────────────── + +/// `PyErr_SetFromWindowsErr(GetLastError())` — the code lands in `.winerror` +/// and the errmap picks the `.errno` its subclass comes from. +#[cfg(all(windows, feature = "host_env"))] +fn last_win32_error() -> crate::PyError { + let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); + crate::PyError::os_error_win32_syscall2(code, pyre_object::PY_NULL, pyre_object::PY_NULL) +} + +/// The names `ctypes/__init__.py` reaches for once `os.name == "nt"`. +/// +/// The dynamic loader is the only part of the module that is written twice: +/// `libloading` already spells `LoadLibrary`/`FreeLibrary` for the same +/// `host_env` library cache the posix `dlopen` uses, so what differs is the +/// module surface, not the machinery below it. +#[cfg(all(windows, feature = "host_env"))] +fn register_windows_loader(ns: pyre_object::PyObjectRef) { + use rustpython_host_env::ctypes as host_ctypes; + + // `_ctypes.h`: `STDCALL` is the absence of the `CDECL` bit, and `HRESULT` + // marks a return value `_check_HRESULT` inspects. Both sit inside the + // same `#ifdef MS_WIN32` as the functions below, so neither is defined on + // the posix side. + crate::module_ns_store(ns, "FUNCFLAG_STDCALL", pyre_object::w_int_new(0x0)); + crate::module_ns_store(ns, "FUNCFLAG_HRESULT", pyre_object::w_int_new(0x2)); + + // ── LoadLibrary(name, load_flags=0) → HMODULE ── + // + // `LoadLibraryExW(name, NULL, load_flags)`. The flags are not optional + // decoration: `CDLL._load_library` (`ctypes/__init__.py:435-451`) defaults + // `winmode` to `nt._LOAD_LIBRARY_SEARCH_DEFAULT_DIRS` and adds + // `_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR` for a name carrying a separator, so + // *every* `CDLL` on this platform arrives with a search policy to apply. + // Dropping it would silently widen the search back to `LoadLibraryW`'s + // default order, which is what those flags exist to narrow. + // + // The handle is the `HMODULE` itself rather than a key into `host_env`'s + // library cache: that cache's Windows door is `Library::new`, which has no + // flags parameter, and its raw-handle door is unix-only. Keeping the + // module handle is also what `_ctypes.c` stores, so `FreeLibrary` and the + // symbol lookup in [`lookup_symbol`] are the plain Win32 calls on it. + crate::module_ns_store( + ns, + "LoadLibrary", + crate::make_builtin_function("LoadLibrary", |args| { + let Some(&name) = args.first() else { + return Err(crate::PyError::type_error( + "LoadLibrary() missing library name", + )); + }; + // `PyArg_ParseTuple(args, "U|i:LoadLibrary")` — a str, and the + // path reaches the loader in the filesystem's own units so a + // surrogate-bearing name round-trips instead of folding to U+FFFD. + if !unsafe { pyre_object::is_str(name) } { + return Err(crate::PyError::type_error( + "LoadLibrary() argument 1 must be str", + )); + } + let name = crate::gateway::os_string_from_fs_bytes(&crate::gateway::fsencode(name)?); + let load_flags = match args.get(1) { + Some(&flags) => crate::baseobjspace::int_w(flags)? as u32, + None => 0, + }; + let module = { + use std::os::windows::ffi::OsStrExt; + let wide: Vec = name.encode_wide().chain(std::iter::once(0)).collect(); + unsafe { + windows_sys::Win32::System::LibraryLoader::LoadLibraryExW( + wide.as_ptr(), + std::ptr::null_mut(), + load_flags, + ) + } + }; + if module.is_null() { + // ERROR_MOD_NOT_FOUND is answered with a plain + // FileNotFoundError naming the module rather than the winerror + // OSError every other failure gets, because the DLL that is + // missing is as often a dependency as the name asked for. + const ERROR_MOD_NOT_FOUND: i32 = 126; + let err = std::io::Error::last_os_error().raw_os_error(); + if err != Some(ERROR_MOD_NOT_FOUND) { + return Err(last_win32_error()); + } + let mut msg = + rustpython_wtf8::Wtf8Buf::from_string("Could not find module '".to_string()); + msg.push_wtf8(&crate::gateway::fsdecode_os_str_wtf8(&name)); + msg.push_str( + "' (or one of its dependencies). Try using the full path with \ + constructor syntax.", + ); + return Err(crate::PyError::new( + crate::error::PyErrorKind::FileNotFoundError, + msg, + )); + } + Ok(pyre_object::w_int_new(module as isize as i64)) + }), + ); + + // ── FreeLibrary(handle) → None ── + crate::module_ns_store( + ns, + "FreeLibrary", + crate::make_builtin_function_with_arity( + "FreeLibrary", + |args| { + let Some(&handle) = args.first() else { + return Err(crate::PyError::type_error("FreeLibrary() needs handle")); + }; + let module = crate::baseobjspace::int_w(handle)? as isize as *mut core::ffi::c_void; + let freed = unsafe { + windows_sys::Win32::Foundation::FreeLibrary(module) != 0 + }; + if !freed { + return Err(last_win32_error()); + } + Ok(pyre_object::w_none()) + }, + 1, + ), + ); + + // ── FormatError(code=GetLastError()) → str ── + crate::module_ns_store( + ns, + "FormatError", + crate::make_builtin_function("FormatError", |args| { + // `if (code == 0) code = GetLastError();` — an explicit zero is + // the same request as no argument, not a request to describe + // ERROR_SUCCESS. + let code = match args.first() { + Some(&code) => match crate::baseobjspace::int_w(code)? as u32 { + 0 => None, + code => Some(code), + }, + None => None, + }; + // The code the system cannot describe still has to produce a + // string: `FormatMessageW` failing is not an error here. + let message = host_ctypes::format_error_message(code) + .unwrap_or_else(|| "".to_string()); + Ok(pyre_object::w_str_new(&message)) + }), + ); + + // ── get_last_error / set_last_error — the ctypes-local copy, which is + // separate from the thread's own Win32 last error. The setter answers + // with the value it replaced, the same contract `set_errno` above + // carries and the one the documented signature promises. ── + crate::module_ns_store( + ns, + "get_last_error", + crate::make_builtin_function_with_arity( + "get_last_error", + |_| Ok(pyre_object::w_int_new(host_ctypes::get_last_error() as i64)), + 0, + ), + ); + crate::module_ns_store( + ns, + "set_last_error", + crate::make_builtin_function_with_arity( + "set_last_error", + |args| { + let Some(&value) = args.first() else { + return Err(crate::PyError::type_error("set_last_error() needs value")); + }; + let previous = + host_ctypes::set_last_error(crate::baseobjspace::int_w(value)? as u32); + Ok(pyre_object::w_int_new(previous as i64)) + }, + 1, + ), + ); + + // ── _check_HRESULT(hr) → hr, or the Win32 error it names ── + // + // `HRESULT`'s `_check_retval_`. `FAILED(hr)` is the sign bit, and the + // raise is `PyErr_SetFromWindowsErr(hr)` — the code lands in `.winerror` + // and the errmap picks `.errno`. + crate::module_ns_store( + ns, + "_check_HRESULT", + crate::make_builtin_function_with_arity( + "_check_HRESULT", + |args| { + let Some(&hr) = args.first() else { + return Err(crate::PyError::type_error("_check_HRESULT() needs hresult")); + }; + let hr = crate::baseobjspace::int_w(hr)? as i32; + if hr < 0 { + return Err(crate::PyError::os_error_win32_syscall2( + hr, + pyre_object::PY_NULL, + pyre_object::PY_NULL, + )); + } + Ok(pyre_object::w_int_new(hr as i64)) + }, + 1, + ), + ); + + // ── CopyComPointer(src, dst) → HRESULT ── + // + // `dst` is a `byref()` carrier, so the destination is the address that + // carrier already resolved; `src` is a COM interface pointer held in a + // cdata buffer. The `AddRef` before the store is what makes this a copy + // rather than a move. + crate::module_ns_store( + ns, + "CopyComPointer", + crate::make_builtin_function_with_arity( + "CopyComPointer", + |args| { + use super::cdata; + if args.len() < 2 { + return Err(crate::PyError::type_error( + "CopyComPointer() needs (src, dst)", + )); + } + let (src, dst) = (args[0], args[1]); + let destination = if is_carg(dst) { carg_ptr(dst) } else { 0 }; + if destination == 0 { + return Ok(pyre_object::w_int_new( + host_ctypes::HRESULT_E_POINTER as i64, + )); + } + let source = if unsafe { pyre_object::is_none(src) } { + 0 + } else if cdata::is_cdata_instance(src) { + let (Some(addr), Some(len)) = (cdata::cdata_addr(src), cdata::cdata_len(src)) + else { + return Ok(pyre_object::w_int_new( + host_ctypes::HRESULT_E_POINTER as i64, + )); + }; + let buffer = unsafe { host_ctypes::borrow_memory(addr as *const u8, len) }; + host_ctypes::read_pointer_from_buffer(buffer) + } else { + return Ok(pyre_object::w_int_new( + host_ctypes::HRESULT_E_POINTER as i64, + )); + }; + Ok(pyre_object::w_int_new( + host_ctypes::copy_com_pointer(source, destination) as i64, + )) + }, + 2, + ), + ); + + // ── COMError — `args` is the (text, details) tail, not the whole tuple ── + let w_exception = crate::builtins::lookup_exc_class("Exception") + .expect("Exception must be installed before _ctypes init"); + crate::module_ns_store( + ns, + "COMError", + crate::builtins::make_exc_type_with_init( + "COMError", + Some("Raised when a COM method call failed."), + crate::builtins::exc_exception_new, + Some(comerror_init), + w_exception, + ), + ); +} + +/// `comerror_init` — stamps the three slots and re-points `args` at the tail. +#[cfg(all(windows, feature = "host_env"))] +fn comerror_init( + args: &[pyre_object::PyObjectRef], +) -> Result { + let Some(&w_self) = args.first() else { + return Err(crate::PyError::type_error( + "__init__() missing 1 required positional argument: 'self'", + )); + }; + let (positional, kwargs) = crate::builtins::split_builtin_kwargs(&args[1..]); + if kwargs.is_some_and(|dict| { + unsafe { pyre_object::w_dict_str_entries(dict) } + .iter() + .any(|(key, _)| key != "__pyre_kw__") + }) { + return Err(crate::PyError::type_error( + "COMError() takes no keyword arguments", + )); + } + let [hresult, text, details] = positional else { + return Err(crate::PyError::type_error(format!( + "COMError expected 3 arguments, got {}", + positional.len() + ))); + }; + crate::baseobjspace::setattr_str(w_self, "hresult", *hresult)?; + crate::baseobjspace::setattr_str(w_self, "text", *text)?; + crate::baseobjspace::setattr_str(w_self, "details", *details)?; + // `args = args[1:]`, so the hresult is reachable only through its slot. + crate::baseobjspace::setattr_str( + w_self, + "args", + pyre_object::tupleobject::w_tuple_new(vec![*text, *details]), + )?; + Ok(pyre_object::w_none()) +} + // ── sizeof / alignment (types and instances) ────────────────────────── -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] fn ctypes_sizeof( args: &[pyre_object::PyObjectRef], ) -> Result { @@ -412,7 +801,7 @@ fn ctypes_sizeof( Err(crate::PyError::type_error("this type has no size")) } -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] fn ctypes_alignment( args: &[pyre_object::PyObjectRef], ) -> Result { @@ -442,7 +831,7 @@ fn ctypes_alignment( // ── addressof / byref / resize (instances) ──────────────────────────── -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] fn ctypes_addressof( args: &[pyre_object::PyObjectRef], ) -> Result { @@ -458,7 +847,7 @@ fn ctypes_addressof( Ok(pyre_object::w_int_new(addr as i64)) } -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] fn ctypes_byref( args: &[pyre_object::PyObjectRef], ) -> Result { @@ -483,7 +872,7 @@ fn ctypes_byref( Ok(make_carg(addr, obj)) } -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] fn ctypes_resize( args: &[pyre_object::PyObjectRef], ) -> Result { @@ -528,13 +917,13 @@ fn ctypes_resize( // ── byref carrier ────────────────────────────────────────────────────── -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] static CARG_TYPE_OBJ: std::sync::OnceLock = std::sync::OnceLock::new(); /// The minimal `byref` carrier type — holds `_ptr` (address) and `_obj` /// (the referenced instance, kept alive). Foreign-call consumption of the /// carrier (the CArgObject P-tag path) is a later slice. -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] fn carg_type() -> pyre_object::PyObjectRef { let raw = *CARG_TYPE_OBJ.get_or_init(|| { let tp = crate::typedef::make_builtin_type("CArgObject", |ns| { @@ -556,7 +945,7 @@ fn carg_type() -> pyre_object::PyObjectRef { raw as pyre_object::PyObjectRef } -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] pub(super) fn make_carg(addr: usize, obj: pyre_object::PyObjectRef) -> pyre_object::PyObjectRef { let carg = pyre_object::w_instance_new(carg_type()); let d = crate::baseobjspace::getdict_native(carg); @@ -570,13 +959,13 @@ pub(super) fn make_carg(addr: usize, obj: pyre_object::PyObjectRef) -> pyre_obje } /// Whether `obj` is a `byref()` carrier (consumed by [`super::funcptr`]). -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] pub(super) fn is_carg(obj: pyre_object::PyObjectRef) -> bool { !obj.is_null() && unsafe { pyre_object::w_instance_get_type(obj) } == carg_type() } /// The address a `byref()` carrier points at. -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] pub(super) fn carg_ptr(carg: pyre_object::PyObjectRef) -> usize { let d = crate::baseobjspace::getdict_native(carg); if d.is_null() { @@ -594,7 +983,7 @@ pub(super) fn carg_ptr(carg: pyre_object::PyObjectRef) -> usize { // Stub surface (non-unix or no host_env) — keeps names importable. // ────────────────────────────────────────────────────────────────────── -#[cfg(not(all(unix, feature = "host_env")))] +#[cfg(not(all(any(unix, windows), feature = "host_env")))] fn register_stub_ctypes(ns: pyre_object::PyObjectRef) { crate::module_ns_store(ns, "ArgumentError", crate::typedef::w_object()); crate::module_ns_store(ns, "_Pointer", crate::typedef::w_object()); diff --git a/pyre/pyre-interpreter/src/module/_ctypes/mod.rs b/pyre/pyre-interpreter/src/module/_ctypes/mod.rs index 7340d2b4c6e..3c23cdfb70b 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/mod.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/mod.rs @@ -5,23 +5,23 @@ //! machinery is split across the submodules — `stginfo` carries a ctypes type's //! layout, `metaclass` builds the types and their fields, `cdata` holds the //! scalar instance buffer, and `funcptr` marshals and performs the foreign -//! call. The submodules are unix-only; elsewhere the module is limited to what -//! `interp_ctypes` can offer without `host_env`. +//! call. The submodules need `host_env`; without it the module is limited to +//! the placeholder surface at the foot of `interp_ctypes`. crate::pyre_module_init!(interp_ctypes); /// Store into a builtin type's namespace — the dict `make_builtin_type` hands /// its init closure. The type-namespace sibling of `module_ns_store`. -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] fn type_ns_store(ns: pyre_object::PyObjectRef, name: &str, value: pyre_object::PyObjectRef) { unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy(ns, name, value) } } -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] pub mod cdata; -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] pub mod funcptr; -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] pub mod metaclass; -#[cfg(all(unix, feature = "host_env"))] +#[cfg(all(any(unix, windows), feature = "host_env"))] pub mod stginfo; diff --git a/pyre/pyre-interpreter/src/module/_winapi/mod.rs b/pyre/pyre-interpreter/src/module/_winapi/mod.rs index a466b4ada51..2e02fd14d6d 100644 --- a/pyre/pyre-interpreter/src/module/_winapi/mod.rs +++ b/pyre/pyre-interpreter/src/module/_winapi/mod.rs @@ -100,6 +100,39 @@ mod process { Ok(w_int_new(host_winapi::get_last_error() as i64)) } + /// `_winapi.GetModuleFileName(module_handle)` — the path the module was + /// loaded from, or the executable's own path for handle 0. + /// + /// `sysconfig._init_non_posix` calls it on `sys.dllhandle` to locate the + /// install prefix, so it is on the path of every `import sysconfig` here. + /// + /// The `MAX_PATH` buffer is the interface, not a shortcut: this call is + /// specified to hand back one fixed-size buffer, so a longer path comes + /// back truncated rather than retried in a growing loop. `initpath.py:308-315 + /// _get_module_file_name` allocates exactly `_MAX_PATH` and gives up when + /// the result does not fit, and the module this shadows declares + /// `WCHAR filename[MAX_PATH]` and forces `filename[MAX_PATH - 1] = '\0'` + /// before returning it. Growing the buffer here would be the deviation. + /// + /// The length is then the NUL scan rather than the count the call reported, + /// because on a truncation the call reports the whole buffer while the + /// loader has already terminated the string inside it — taking the reported + /// count would append that terminator to the path. + pub fn get_module_file_name(args: &[PyObjectRef]) -> Result { + const MAX_PATH: usize = 260; + let module = handle_w(arg(args, 0, "GetModuleFileName")?)? as *mut core::ffi::c_void; + let mut buffer = [0u16; MAX_PATH]; + let length = host_winapi::get_module_file_name(module, &mut buffer); + if length == 0 { + return Err(super::last_os_error()); + } + buffer[MAX_PATH - 1] = 0; + let filename = &buffer[..buffer.iter().position(|&u| u == 0).unwrap_or(MAX_PATH)]; + Ok(pyre_object::w_str_from_wtf8( + rustpython_wtf8::Wtf8Buf::from_wide(filename), + )) + } + /// `_winapi.TerminateProcess(handle, exit_code)` pub fn terminate_process(args: &[PyObjectRef]) -> Result { let handle = handle_w(arg(args, 0, "TerminateProcess")?)?; @@ -408,6 +441,7 @@ crate::py_module! { ("GetCurrentProcess", 0, process::get_current_process), ("GetFileType", 1, process::get_file_type), ("GetLastError", 0, process::get_last_error), + ("GetModuleFileName", 1, process::get_module_file_name), ("TerminateProcess", 2, process::terminate_process), ("CreatePipe", 2, process::create_pipe), ("CreateProcess", 9, process::create_process), diff --git a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs index eb840503bed..0165b6408b2 100644 --- a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs +++ b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs @@ -3334,14 +3334,15 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { // `($module, fd=, /)` — the descriptor is // positional-only, so `fd=1` is a keyword this entry point does // not take rather than a binding. - let (bound, _kwargs) = bind_posonly_args( - args, - "get_terminal_size", - "posix.get_terminal_size", - 1, - 0, - &[], - )?; + // A keyword is refused against the module-qualified name, and this + // module answers to `nt` on Windows. + let qualname = if cfg!(windows) { + "nt.get_terminal_size" + } else { + "posix.get_terminal_size" + }; + let (bound, _kwargs) = + bind_posonly_args(args, "get_terminal_size", qualname, 1, 0, &[])?; let fd = match bound[0] { Some(w) => crate::baseobjspace::c_int_w(w)?, None => 1, @@ -4073,7 +4074,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { /// `argument_unavailable` (`interp_posix.py:298-301`) — a modifier this /// platform has no call to apply, named together with the entry point that /// was asked to apply it. - #[cfg(all(unix, feature = "host_env"))] + #[cfg(feature = "host_env")] fn argument_unavailable(funcname: &str, arg: &str) -> crate::PyError { crate::PyError::not_implemented(format!("{funcname}: {arg} unavailable on this platform")) } @@ -8996,20 +8997,51 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { ), ); - // os.access(path, mode) -> bool. Windows has no permission bits to - // consult beyond the read-only attribute, so `W_OK` is the only mode - // that can answer False for a name that exists (`os_access_impl`). + // os.access(path, mode, *, dir_fd=None, effective_ids=False, + // follow_symlinks=True) -> bool + // + // Windows has no permission bits to consult beyond the read-only + // attribute, so `W_OK` is the only mode that can answer False for a + // name that exists (`os_access_impl`). None of the three modifiers + // has a call to reach: `dir_fd` types as `dir_fd(requires='faccessat')` + // and the other two are the pair `os_access_impl` turns away without + // `faccessat`, so each is refused rather than answered as though it + // had been applied. crate::module_ns_store( ns, "access", crate::make_builtin_function("access", |args| { - if args.len() < 2 { - return Err(crate::PyError::type_error("access() requires 2 arguments")); - } - let path = crate::gateway::fsencode_path_named_w(args[0], "access", "path")?; + // The three modifiers are keyword-only, so a third positional + // is an error rather than a `dir_fd`. + let (bound, kwargs) = bind_path_args( + args, + "access", + &["path", "mode"], + 2, + &["dir_fd", "effective_ids", "follow_symlinks"], + )?; + // The parameters convert in declaration order and each of them + // can raise, so the order is observable: `path` reports before + // `mode`, and both before either flag's `__bool__` is called. + let path = crate::gateway::fsencode_path_named_w( + bound[0].expect("path is required"), + "access", + "path", + )?; // Only `W_OK` is read, so the byte holding it is the whole of // the mode as far as the answer goes. - let mode = crate::baseobjspace::c_int_w(args[1])? as u8; + let mode = crate::baseobjspace::c_int_w(bound[1].expect("mode is required"))? as u8; + dir_fd_kwarg(kwargs, false)?; + if let Some(v) = crate::builtins::kwarg_get(kwargs, "effective_ids") + && crate::baseobjspace::is_true(v)? + { + return Err(argument_unavailable("access", "effective_ids")); + } + if let Some(v) = crate::builtins::kwarg_get(kwargs, "follow_symlinks") + && !crate::baseobjspace::is_true(v)? + { + return Err(argument_unavailable("access", "follow_symlinks")); + } Ok(pyre_object::w_bool_from(host_nt::access( path_from_bytes(&path.as_bytes).as_ref(), mode, @@ -9017,6 +9049,130 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { }), ); + // os.execv(path, argv) / os.execve(path, argv, env) + // + // `_wexecv` / `_wexecve` are the wide forms `os_execv_impl` reaches + // for; they return only on failure, because on success the calling + // process is gone by the time they would. + fn exec_argv_wide( + w_argv: PyObjectRef, + function: &str, + ) -> Result, crate::PyError> { + let items = crate::baseobjspace::unpackiterable(w_argv, -1).map_err(|error| { + if error.kind == crate::PyErrorKind::TypeError { + crate::PyError::type_error(format!( + "{function}() arg 2 must be an iterable of strings" + )) + } else { + error + } + })?; + if items.is_empty() { + return Err(crate::PyError::value_error(format!( + "{function}() arg 2 must not be empty" + ))); + } + let mut argv = Vec::with_capacity(items.len()); + for item in items { + // An element is converted on the sequence's behalf, not as an + // argument of the call, so the caller-less message is the one + // it reports — the same for the environment below. + let value = extract_path(item)?; + argv.push(widestring::WideCString::from_os_str(&*os_str_from_bytes(&value)) + .map_err(|_| { + crate::PyError::value_error(format!( + "{function}() arg 2 contains an embedded null byte" + )) + })?); + } + if argv[0].is_empty() { + return Err(crate::PyError::value_error(format!( + "{function}() arg 2 first element cannot be empty" + ))); + } + Ok(argv) + } + + fn exec_pointer_array_wide(values: &[widestring::WideCString]) -> Vec<*const u16> { + let mut pointers: Vec<_> = values.iter().map(|value| value.as_ptr()).collect(); + pointers.push(std::ptr::null()); + pointers + } + + crate::module_ns_store( + ns, + "execv", + crate::make_builtin_function_with_arity( + "execv", + |args| { + // The path names itself; the argv entries do not, because + // each of those is converted on the sequence's behalf + // rather than as an argument of its own. + let command = + crate::gateway::fsencode_path_named_w(args[0], "execv", "path")?.as_bytes; + let command_w = wide_path(&command)?; + let argv = exec_argv_wide(args[1], "execv")?; + let argv_ptrs = exec_pointer_array_wide(&argv); + unsafe { libc::wexecv(command_w.as_ptr(), argv_ptrs.as_ptr()) }; + // `wrap_oserror` names no file, so the path stays out of + // the error. + Err(io_err(std::io::Error::last_os_error(), "")) + }, + 2, + ), + ); + + crate::module_ns_store( + ns, + "execve", + crate::make_builtin_function_with_arity( + "execve", + |args| { + let command = + crate::gateway::fsencode_path_named_w(args[0], "execve", "path")?.as_bytes; + let command_w = wide_path(&command)?; + let argv = exec_argv_wide(args[1], "execve")?; + let argv_ptrs = exec_pointer_array_wide(&argv); + + let keys_obj = crate::baseobjspace::call_method(args[2], "keys", &[]); + if keys_obj.is_null() { + return Err(crate::call::take_call_error().unwrap_or_else(|| { + crate::PyError::type_error("execve: env must be a mapping") + })); + } + let keys = crate::baseobjspace::unpackiterable(keys_obj, -1)?; + let mut env = Vec::with_capacity(keys.len()); + for key_obj in keys { + let value_obj = crate::baseobjspace::getitem(args[2], key_obj)?; + let key = extract_path(key_obj)?; + let value = extract_path(value_obj)?; + if key.is_empty() || key.get(1..).is_some_and(|tail| tail.contains(&b'=')) { + return Err(crate::PyError::value_error( + "illegal environment variable name", + )); + } + let mut entry = key; + entry.push(b'='); + entry.extend_from_slice(&value); + env.push( + widestring::WideCString::from_os_str(&*os_str_from_bytes(&entry)) + .map_err(|_| { + crate::PyError::value_error( + "execve() environment contains an embedded null byte", + ) + })?, + ); + } + let env_ptrs = exec_pointer_array_wide(&env); + unsafe { + libc::wexecve(command_w.as_ptr(), argv_ptrs.as_ptr(), env_ptrs.as_ptr()) + }; + Err(io_err(std::io::Error::last_os_error(), "")) + }, + 3, + ), + ); + /// The mode bit Windows keeps: with the owner's write bit the /// read-only attribute comes off, without it goes on. const S_IWRITE: u32 = 0o200; @@ -9185,27 +9341,28 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { CreateSymbolicLinkW, SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE, SYMBOLIC_LINK_FLAG_DIRECTORY, }; - let (args, kwargs) = crate::builtins::split_builtin_kwargs(args); - crate::builtins::kwarg_reject_unknown( - kwargs, - &["target_is_directory", "dir_fd"], + let (bound, kwargs) = bind_path_args( + args, "symlink", + &["src", "dst", "target_is_directory"], + 2, + &["dir_fd"], )?; - if crate::builtins::kwarg_get(kwargs, "dir_fd") - .is_some_and(|w| !unsafe { pyre_object::is_none(w) }) - { - return Err(dir_fd_unavailable()); - } - if args.len() < 2 { - return Err(crate::PyError::type_error("symlink() requires 2 arguments")); - } - let src = crate::gateway::fsencode_path_named_w(args[0], "symlink", "src")?; - let dst = crate::gateway::fsencode_path_named_w(args[1], "symlink", "dst")?; - let target_is_directory = match args - .get(2) - .copied() - .or_else(|| crate::builtins::kwarg_get(kwargs, "target_is_directory")) - { + // `symlink` types `dir_fd` as `DirFD(rposix.HAVE_SYMLINKAT)`, + // and `CreateSymbolicLinkW` resolves a relative name against + // the working directory alone. + dir_fd_kwarg(kwargs, false)?; + let src = crate::gateway::fsencode_path_named_w( + bound[0].expect("src is required"), + "symlink", + "src", + )?; + let dst = crate::gateway::fsencode_path_named_w( + bound[1].expect("dst is required"), + "symlink", + "dst", + )?; + let target_is_directory = match bound[2] { Some(w) => crate::baseobjspace::is_true(w)?, None => false, }; diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index 352d18879da..c6a6e5b652d 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -1190,6 +1190,16 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { // reads it to build USER_SITE. #[cfg(windows)] module_ns_store(ns, "winver", w_str_new("3.14")); + // sys.dllhandle — the handle of the DLL exporting the Python C API, + // published beside `winver` because both come from the same `MS_COREDLL` + // block. `ctypes/__init__.py:562` builds `pythonapi` out of it with no + // import guard, so a missing attribute is an AttributeError out of `import + // ctypes`. `vm.py:301 get_dllhandle` answers 0 for a build without + // `cpyext`, and there is no cpyext here, so 0 is the whole answer: the + // handle names an API this interpreter does not export, and reporting the + // executable's own module would only make `pythonapi` fail one call later. + #[cfg(windows)] + module_ns_store(ns, "dllhandle", w_int_new(0)); // sys._vpath — the build's relative path from the executable's directory to // the prefix. `sysconfig._init_config_vars` subscripts it under `os.name == // 'nt'`, so it is an AttributeError out of the first `get_config_var` call