diff --git a/majit/majit-gc/src/gcreftracer.rs b/majit/majit-gc/src/gcreftracer.rs index b2dca586a77..e8b5a76229d 100644 --- a/majit/majit-gc/src/gcreftracer.rs +++ b/majit/majit-gc/src/gcreftracer.rs @@ -81,6 +81,19 @@ unsafe impl Sync for GcTable {} /// drop). static LIVE_GC_TABLES: RwLock>> = RwLock::new(Vec::new()); +/// Test-only lock modeling the stop-the-world invariant that no table is +/// dropped while a walk is in flight. The harness runs tests in parallel, so +/// a collector test's collection — which walks this registry through the +/// globally-registered [`gc_table_extra_root_walker`] once any table has +/// existed — can call [`walk_all_gc_tables`] concurrently with a registry +/// test's table drop, transiently upgrading a `Weak` the dropping test +/// expects to be dead. A registry test takes the write side to exclude every +/// walk across its drop/observe window; each walk takes the read side. +/// Compiled out in production, where the STW collector already guarantees no +/// concurrent drop. +#[cfg(test)] +static GC_TABLE_WALK_LOCK: RwLock<()> = RwLock::new(()); + impl GcTable { /// Build a per-loop table from the rewrite's gcref output list and /// register it for GC forwarding. @@ -149,6 +162,18 @@ fn register_table(table: &Arc) { /// (`collector.rs:668`) and major (`collector.rs:1185`) collection /// phases. fn walk_all_gc_tables(visitor: &mut dyn FnMut(&mut GcRef)) { + // In test builds, hold the read side of the walk lock so a registry + // test's drop/observe window (which takes the write side) is never + // interleaved with a walk. Compiles out in production. + #[cfg(test)] + let _walk = GC_TABLE_WALK_LOCK.read().unwrap_or_else(|e| e.into_inner()); + walk_all_gc_tables_inner(visitor); +} + +/// The walk itself, without the test-only lock, so a registry test that +/// already holds the write side observes the registry without re-entering +/// the lock. +fn walk_all_gc_tables_inner(visitor: &mut dyn FnMut(&mut GcRef)) { // Snapshot the live tables under a read guard, then release the lock // before tracing (same snapshot-then-iterate discipline as // `walk_extra_roots`, `shadow_stack.rs:622`). Dead `Weak`s are @@ -182,15 +207,17 @@ mod tests { // `LIVE_GC_TABLES` is a process-global registry; in production it is // only mutated outside a collection (table build at compile time) and // only read inside a stop-the-world collection, so there is never a - // concurrent build-vs-walk. The test harness runs tests in parallel, - // which would let one table-building test's registry mutation race - // another's global `walk_all_gc_tables` assertion. Serialize the - // table-touching tests against each other to model the STW invariant. - static TEST_REGISTRY_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + // concurrent build-vs-walk. The test harness runs tests in parallel, so + // besides serializing the table-touching tests against each other, the + // write side of [`GC_TABLE_WALK_LOCK`] also excludes any concurrent + // collector-test collection whose walk would otherwise transiently + // resurrect a table this test is dropping — modeling the STW invariant. #[test] fn trace_forwards_slots_in_place() { - let _serialize = TEST_REGISTRY_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let _serialize = GC_TABLE_WALK_LOCK + .write() + .unwrap_or_else(|e| e.into_inner()); let table = GcTable::from_gcrefs(&[GcRef(0x1000), GcRef(0x2000)]); // A moving collection relocates 0x1000 -> 0x9000. table.trace(&mut |r| { @@ -215,7 +242,9 @@ mod tests { // is the shared dynasm/cranelift `LoadFromGcTable` contract; wasm never // runs the GC rewrite (loud-panic), so it has no moving-GC ref-const // path to cover. - let _serialize = TEST_REGISTRY_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let _serialize = GC_TABLE_WALK_LOCK + .write() + .unwrap_or_else(|e| e.into_inner()); let table = GcTable::from_gcrefs(&[GcRef(0x1000), GcRef(0x2000)]); // `base` is the value baked into the trace at compile time. let base = table.base_addr(); @@ -253,12 +282,16 @@ mod tests { #[test] fn dropping_table_deregisters_from_walk() { - let _serialize = TEST_REGISTRY_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let _serialize = GC_TABLE_WALK_LOCK + .write() + .unwrap_or_else(|e| e.into_inner()); // A sentinel unlikely to collide with any other test's table. const SENTINEL: GcRef = GcRef(0x0DEAD_BEEF); + // The write side is already held, so count through the unlocked + // walk to avoid re-entering the lock. let count_sentinels = || { let mut n = 0usize; - walk_all_gc_tables(&mut |r| { + walk_all_gc_tables_inner(&mut |r| { if *r == SENTINEL { n += 1; } diff --git a/pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py b/pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py index cfac974f09d..4f68ea9cccd 100644 --- a/pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py +++ b/pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py @@ -40,10 +40,11 @@ def raises(call, exc): # above it, so -1ns is the last nanosecond of 1969 rather than a value with no # representation. # -# Windows is left out: a FILETIME counts 100ns ticks, so a nanosecond that is -# not a multiple of 100 is not a time that filesystem can hold — CPython reads -# -1 back as -100 — and this build's Windows path carries the timestamp as an -# unsigned duration and refuses the whole range. See the follow-up task. +# Windows is left out of this block: a FILETIME counts 100ns ticks, so +# ns=(-1, -1) is not a time that filesystem can hold (it reads -1 back as +# -100), and the descriptor form below is not one Windows advertises. The +# keyword `times` case after the block, which lands on whole seconds, is +# written through SetFileTime everywhere. if sys.platform != "win32": os.utime(p, ns=(-1, -1)) check(os.stat(p).st_mtime_ns == -1, f"utime(ns=(-1,-1)) -> {os.stat(p).st_mtime_ns}") @@ -88,21 +89,20 @@ def raises(call, exc): # question — the terminal-only limits are not ones a regular file has. What no # answer may be is None: a host with no determinate value says so with -1. # -# `pathconf` and the `pathconf_names` table it resolves through are a POSIX -# surface; neither runtime carries them on Windows, so there is nothing to -# compare there. -def limits(target): - for name in sorted(os.pathconf_names): - try: - limit = os.pathconf(target, name) - except OSError: - continue - check(isinstance(limit, int), f"pathconf({name!r}) answered {limit!r}") - check(limit >= -1, f"pathconf({name!r}) answered {limit}") - yield name, limit +# pathconf and pathconf_names are POSIX-only; Windows has neither, so the +# section is skipped there rather than asked of a name that cannot answer. +if sys.platform != "win32": + def limits(target): + for name in sorted(os.pathconf_names): + try: + limit = os.pathconf(target, name) + except OSError: + continue + check(isinstance(limit, int), f"pathconf({name!r}) answered {limit!r}") + check(limit >= -1, f"pathconf({name!r}) answered {limit}") + yield name, limit -if sys.platform != "win32": answered = dict(limits(p)) check(answered, "pathconf answered no name at all") check("PC_NAME_MAX" in answered, "pathconf refused PC_NAME_MAX on a regular file") diff --git a/pyre/pyre-interpreter/Cargo.toml b/pyre/pyre-interpreter/Cargo.toml index e7456a72a60..7ebef503728 100644 --- a/pyre/pyre-interpreter/Cargo.toml +++ b/pyre/pyre-interpreter/Cargo.toml @@ -79,6 +79,7 @@ lz4_flex = { version = "0.13", default-features = false, features = [ windows-sys = { version = "0.61", features = [ "Win32_Foundation", "Win32_Storage_FileSystem", + "Win32_Security", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_UI_Shell", diff --git a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs index 005014e011b..e56f56212cc 100644 --- a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs +++ b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs @@ -24,14 +24,22 @@ struct ApplevelForkCallbacks { } /// `posix.DirEntry` — native layout `[PyObject | w_name | w_path | w_stat | -/// w_lstat | dir_fd]`, matching `interp_scandir.py W_DirEntry`: the name and -/// full path plus the cached `stat` (`follow_symlinks=True`) and `lstat` -/// (`follow_symlinks=False`) results. `w_stat`/`w_lstat` are `PY_NULL` until -/// first requested, so `entry.stat()` re-fetches once and then returns the -/// same object, and `is_dir`/`is_file`/`inode` share the same on-demand stat. -/// `dir_fd` is the descriptor a `scandir(fd)` handed the entry (`-1` for a -/// name), which its own stat resolves the bare `name` against — the native -/// counterpart of `self.scandir_iterator.orig_fd`. +/// w_lstat | dir_fd | enum_ino | enum_type]`, matching `interp_scandir.py +/// W_DirEntry`: the name and full path plus the cached `stat` +/// (`follow_symlinks=True`) and `lstat` (`follow_symlinks=False`) results. +/// `w_stat`/`w_lstat` are `PY_NULL` until first requested, so `entry.stat()` +/// re-fetches once and then returns the same object, and `is_dir`/`is_file` +/// share the same on-demand stat. `dir_fd` is the descriptor a `scandir(fd)` +/// handed the entry (`-1` for a name), which its own stat resolves the bare +/// `name` against — the native counterpart of +/// `self.scandir_iterator.orig_fd`. `enum_ino` is the inode `readdir` reported +/// at enumeration (`descr_inode`'s `self.inode`), so `inode()` answers from it +/// without a stat; it is `-1` when unavailable (non-unix hosts), which falls +/// back to a stat. `enum_type` is the `d_type` `readdir` reported (the +/// `known_type` half of `self.flags`), so `is_dir`/`is_file`/`is_symlink` +/// answer from it without a stat when it is not `DT_UNKNOWN`; it defaults to +/// `DT_UNKNOWN` (`0`) — the value for a host or filesystem that reports no +/// type — which falls through to the stat. /// The layout carries no instance dict; `name`/`path` are read-only getset /// descriptors, so the type is not instantiable and not acceptable as a base. #[crate::pyre_class("posix.DirEntry")] @@ -42,6 +50,8 @@ pub struct W_DirEntry { pub w_stat: PyObjectRef, pub w_lstat: PyObjectRef, pub dir_fd: i32, + pub enum_ino: i64, + pub enum_type: i32, } static APPLEVEL_FORK_CALLBACKS: LazyLock> = @@ -2577,26 +2587,65 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { #[cfg(all(windows, feature = "host_env"))] { + use windows_sys::Win32::Foundation::{CloseHandle, FILETIME, INVALID_HANDLE_VALUE}; + use windows_sys::Win32::Storage::FileSystem::{ + CreateFileW, FILE_FLAG_BACKUP_SEMANTICS, FILE_WRITE_ATTRIBUTES, OPEN_EXISTING, + SetFileTime, + }; if dir_fd.is_some() || !follow_symlinks { return Err(crate::PyError::not_implemented( "utime: dir_fd and follow_symlinks=False are unavailable on this platform", )); } - // The host call here counts from the epoch upwards and has no - // second below it, so a pre-epoch time is turned away rather than - // written as the wrong one. The descriptor form above and every - // POSIX host write it. - let since_epoch = |t: UTime| { - u64::try_from(t.sec) - .map(|sec| std::time::Duration::new(sec, t.nsec as u32)) - .map_err(|_| crate::PyError::value_error("utime: timestamp out of range")) + // A FILETIME counts 100-ns intervals from 1601-01-01, so a + // pre-epoch time is an ordinary positive count rather than one with + // no representation; utimensat and every POSIX host write it, and + // 3.14 writes it through `SetFileTime` (rposix.win32_utime). The + // sub-100ns of a nanosecond is not a tick the filesystem holds and + // is floored, as the call floors it. + const EPOCH_DIFF: i64 = 11_644_473_600; + let to_filetime = |t: UTime| -> Result { + let ticks = t + .sec + .checked_add(EPOCH_DIFF) + .filter(|s| *s >= 0) + .and_then(|s| s.checked_mul(10_000_000)) + .and_then(|s| s.checked_add(t.nsec / 100)) + .ok_or_else(|| crate::PyError::value_error("utime: timestamp out of range"))? + as u64; + Ok(FILETIME { + dwLowDateTime: ticks as u32, + dwHighDateTime: (ticks >> 32) as u32, + }) }; - host_os::set_file_times( - path_from_bytes(&path.as_bytes).as_ref(), - since_epoch(access)?, - since_epoch(modified)?, - ) - .map_err(|e| fs_err_with_filename(e, path.w_path()))?; + let atime = to_filetime(access)?; + let mtime = to_filetime(modified)?; + let wide = wide_path(&path.as_bytes)?; + // FILE_WRITE_ATTRIBUTES is the access `SetFileTime` takes; + // FILE_FLAG_BACKUP_SEMANTICS lets the name open a directory too. + let handle = unsafe { + CreateFileW( + wide.as_ptr(), + FILE_WRITE_ATTRIBUTES, + 0, + std::ptr::null(), + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + std::ptr::null_mut(), + ) + }; + if handle == INVALID_HANDLE_VALUE { + return Err(fs_err_with_filename( + std::io::Error::last_os_error(), + path.w_path(), + )); + } + let wrote = unsafe { SetFileTime(handle, std::ptr::null(), &atime, &mtime) }; + let error = (wrote == 0).then(std::io::Error::last_os_error); + unsafe { CloseHandle(handle) }; + if let Some(error) = error { + return Err(fs_err_with_filename(error, path.w_path())); + } return Ok(pyre_object::w_none()); } #[cfg(all(unix, not(feature = "sandbox")))] @@ -2708,14 +2757,44 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { ); } - /// The names a directory descriptor holds, `.` and `..` left out + /// Drive an open `DIR*` to its end, handing each real entry (`.` and `..` + /// left out) to `f` as `(name, d_ino, d_type)` — the `get_name_bytes`, + /// `get_inode`, and `get_known_type` a `nextentry` yields + /// (`interp_scandir.py:148-153`). Returns the errno at the end: `0` for a + /// clean end, or the failure `readdir` reported. Does not close `dirp`. + #[cfg(all(unix, feature = "host_env", not(feature = "sandbox")))] + fn readdir_collect(dirp: *mut libc::DIR, mut f: impl FnMut(&[u8], i64, u8)) -> i32 { + loop { + // `readdir` reports the end of the directory and a failure the + // same way — a null return — so errno is cleared before the call + // and read back after it (`rposix.py:797` RFFI_FULL_ERRNO_ZERO). + rustpython_host_env::os::set_errno(0); + let entry = unsafe { libc::readdir(dirp) }; + if entry.is_null() { + return crate::builtins::crt_errno(); + } + let name = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }; + let name = name.to_bytes(); + if name != b"." && name != b".." { + let ino = unsafe { (*entry).d_ino } as i64; + let d_type = unsafe { (*entry).d_type }; + f(name, ino, d_type); + } + } + } + + /// Read a directory descriptor's entries through `f` /// (`rposix.py:810-845` `_listdir`/`fdlistdir`). /// /// `fdopendir` takes the descriptor over and `closedir` closes it, so the /// caller's own is duplicated first — `interp_posix.py:1118` spells that - /// `rposix.dup(fd, inheritable=False)`, which is `F_DUPFD_CLOEXEC`. + /// `rposix.dup(fd, inheritable=False)`, which is `F_DUPFD_CLOEXEC`. The + /// duplicate shares its file description — and so its directory offset — + /// with the caller's descriptor, which would be left at the end of the + /// directory and read as empty next time; `_listdir`'s `rewind=True` + /// (`rposix.py:844`) puts it back before the close. #[cfg(all(unix, feature = "host_env", not(feature = "sandbox")))] - fn fdlistdir(fd: i32) -> Result>, i32> { + fn fd_readdir(fd: i32, f: impl FnMut(&[u8], i64, u8)) -> Result<(), i32> { let dup = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) }; if dup < 0 { return Err(crate::builtins::crt_errno()); @@ -2726,32 +2805,21 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { unsafe { libc::close(dup) }; return Err(errno); } - let mut names = Vec::new(); - let errno = loop { - // `readdir` reports the end of the directory and a failure the - // same way — a null return — so errno is cleared before the call - // and read back after it (`rposix.py:797` RFFI_FULL_ERRNO_ZERO). - rustpython_host_env::os::set_errno(0); - let entry = unsafe { libc::readdir(dirp) }; - if entry.is_null() { - break crate::builtins::crt_errno(); - } - let name = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }; - let name = name.to_bytes(); - if name != b"." && name != b".." { - names.push(name.to_vec()); - } - }; - // The duplicate shares its file description — and so its directory - // offset — with the caller's descriptor, which would be left at the end - // of the directory and read as empty next time. `_listdir`'s - // `rewind=True` (`rposix.py:844`) puts it back before the close. + let errno = readdir_collect(dirp, f); unsafe { libc::rewinddir(dirp) }; // `closedir` closes the duplicate, so nothing here outlives the call. unsafe { libc::closedir(dirp) }; if errno != 0 { return Err(errno); } + Ok(()) + } + + /// The names a directory descriptor holds, `.` and `..` left out. + #[cfg(all(unix, feature = "host_env", not(feature = "sandbox")))] + fn fdlistdir(fd: i32) -> Result>, i32> { + let mut names = Vec::new(); + fd_readdir(fd, |name, _ino, _d_type| names.push(name.to_vec()))?; Ok(names) } @@ -3790,6 +3858,38 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { const S_IFREG: u32 = 0o100_000; const S_IFLNK: u32 = 0o120_000; + /// The `d_type` byte `readdir` reports — the `known_type` half of + /// `interp_scandir.py`'s `flags`. `DT_UNKNOWN` (`0`) is the `W_DirEntry` + /// default, so an entry whose type the host did not report (a non-unix + /// host, or a filesystem that answers `DT_UNKNOWN`) falls through to the + /// stat `dir_entry_kind` runs. Only the three types the tests read need a + /// name. + const DT_UNKNOWN: u8 = 0; + const DT_DIR: u8 = 4; + const DT_REG: u8 = 8; + const DT_LNK: u8 = 10; + + fn dir_entry_known_type(self_obj: PyObjectRef) -> u8 { + W_DirEntry::from_obj(self_obj).map_or(DT_UNKNOWN, |de| de.enum_type as u8) + } + + /// Answer `is_dir`/`is_file`/`is_symlink` from the enumeration `d_type` + /// when it decides the question, else `None` to fall through to a stat + /// (`interp_scandir.py:399-426`). `target` is the `DT_*` the query wants. + /// An unknown type never decides. A symlink decides every query but a + /// followed `is_dir`/`is_file`, which need the target's type instead. + fn dir_entry_kind_from_type(known: u8, target: u8, follow: bool) -> Option { + if known == DT_UNKNOWN { + None + } else if known == target { + Some(true) + } else if follow && known == DT_LNK { + None + } else { + Some(false) + } + } + /// The file type an entry's name resolves to, or `None` for a name that has /// gone away — `check_mode` (`interp_scandir.py:319-330`) answers "no, not /// this type" for `ENOENT` alone, on the reasoning that a vanished entry is @@ -3832,26 +3932,43 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { } fn dir_entry_is_dir(args: &[PyObjectRef]) -> Result { let follow = dir_entry_follow(args)?; - Ok(pyre_object::w_bool_from( - dir_entry_kind(args, follow)? == Some(S_IFDIR), - )) + let ans = match dir_entry_kind_from_type(dir_entry_known_type(args[0]), DT_DIR, follow) { + Some(b) => b, + None => dir_entry_kind(args, follow)? == Some(S_IFDIR), + }; + Ok(pyre_object::w_bool_from(ans)) } fn dir_entry_is_file(args: &[PyObjectRef]) -> Result { let follow = dir_entry_follow(args)?; - Ok(pyre_object::w_bool_from( - dir_entry_kind(args, follow)? == Some(S_IFREG), - )) + let ans = match dir_entry_kind_from_type(dir_entry_known_type(args[0]), DT_REG, follow) { + Some(b) => b, + None => dir_entry_kind(args, follow)? == Some(S_IFREG), + }; + Ok(pyre_object::w_bool_from(ans)) } fn dir_entry_is_symlink(args: &[PyObjectRef]) -> Result { - Ok(pyre_object::w_bool_from( - dir_entry_kind(args, false)? == Some(S_IFLNK), - )) + // `is_symlink` never follows, so a known non-`DT_LNK` type answers `false` + // and `DT_LNK` answers `true`; only `DT_UNKNOWN` needs the lstat. + let ans = match dir_entry_kind_from_type(dir_entry_known_type(args[0]), DT_LNK, false) { + Some(b) => b, + None => dir_entry_kind(args, false)? == Some(S_IFLNK), + }; + Ok(pyre_object::w_bool_from(ans)) } fn dir_entry_is_junction(_args: &[PyObjectRef]) -> Result { // POSIX has no junction points. Ok(pyre_object::w_bool_from(false)) } fn dir_entry_inode(args: &[PyObjectRef]) -> Result { + // `descr_inode` returns the inode `readdir` reported at enumeration when + // the entry carries one (every unix `scandir` path, name or descriptor), + // with no stat; `-1` means it has none and the stat paths below answer + // instead. + if let Some(de) = W_DirEntry::from_obj(args[0]) { + if de.enum_ino != -1 { + return Ok(pyre_object::w_int_new(de.enum_ino)); + } + } let (w_path, path) = dir_entry_path(args[0])?; #[cfg(all(unix, not(feature = "sandbox")))] { @@ -3941,9 +4058,13 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { } } } - /// `interp_scandir.py get_stat` — `follow_symlinks=True` caches into - /// `w_stat`, `False` into `w_lstat`, so a repeated call returns the same - /// object. Only a successful fetch is cached; an error re-raises on each + /// `posixmodule.c DirEntry_get_stat` caches the built result and hands back + /// the same object — `follow_symlinks=True` into `w_stat`, `False` into + /// `w_lstat` — so `entry.stat() is entry.stat()`. (`interp_scandir.py + /// descr_stat` caches only the raw stat data and rebuilds a fresh + /// `build_stat_result` on every call, so under it the result identity + /// differs; the 3.14 behavior is to cache the object, which is what this + /// does.) Only a successful fetch is cached; an error re-raises on each /// call. The entry never moves (`allocate_stable`), so the raw receiver /// stays valid across the fetch's allocation. fn dir_entry_stat(args: &[PyObjectRef]) -> Result { @@ -4157,13 +4278,29 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { /// on the shadow stack). Each string is pinned before the next allocation /// so a moving collection during `allocate_stable` forwards it; the entry /// is stable but its young strings join the remembered set. `dir_fd` is the - /// descriptor the entry resolves its own `name` against, or `-1`. + /// descriptor the entry resolves its own `name` against, or `-1`. `enum_ino` + /// is the `readdir` inode (or `-1` when the enumeration did not carry one). + /// Join a `scandir` path prefix to an entry name the way + /// `interp_scandir.py:65-67` builds `w_path_prefix`: a separator goes + /// between them unless the prefix is empty or already ends in one. + #[cfg(all(unix, feature = "host_env", not(feature = "sandbox")))] + fn join_dir_name(prefix: &[u8], name: &[u8]) -> Vec { + let mut full = Vec::with_capacity(prefix.len() + 1 + name.len()); + full.extend_from_slice(prefix); + if !prefix.is_empty() && full.last() != Some(&b'/') { + full.push(b'/'); + } + full.extend_from_slice(name); + full + } fn scandir_push_entry( list_slot: usize, bytes_mode: bool, name: &[u8], full: &[u8], dir_fd: i32, + enum_ino: i64, + enum_type: u8, ) { let _entry_scope = pyre_object::gc_roots::push_roots(); let base = pyre_object::gc_roots::shadow_stack_len(); @@ -4175,6 +4312,8 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { de.w_name = pyre_object::gc_roots::shadow_stack_get(base); de.w_path = pyre_object::gc_roots::shadow_stack_get(base + 1); de.dir_fd = dir_fd; + de.enum_ino = enum_ino; + de.enum_type = enum_type as i32; unsafe { pyre_object::gc_hook::try_gc_write_barrier(obj as *mut u8) }; let list = pyre_object::gc_roots::shadow_stack_get(list_slot); unsafe { pyre_object::w_list_append(list, obj) }; @@ -4212,13 +4351,39 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { // `interp_scandir.py:50` leaves the path prefix empty for a // descriptor — there is no directory to join — so an entry's // `path` is its bare `name`, and a descriptor is not `bytes`, - // so both come back as `str`. - for name in - fdlistdir(fd).map_err(|errno| errno_err_with_filename(errno, w_path()))? - { - scandir_push_entry(list_slot, false, &name, &name, fd); + // so both come back as `str`. Every entry records the + // descriptor so its own stat resolves the bare name against it. + fd_readdir(fd, |name, ino, d_type| { + scandir_push_entry(list_slot, false, name, name, fd, ino, d_type); + }) + .map_err(|errno| errno_err_with_filename(errno, w_path()))?; + } + // A name is enumerated through `opendir`/`readdir` so each entry + // carries the `d_ino` and `d_type` the dirent reports + // (`interp_scandir.py:148-153`): `inode()` answers from `d_ino` + // and `is_dir`/`is_file`/`is_symlink` from `d_type`, both without + // a stat. + #[cfg(all(unix, feature = "host_env", not(feature = "sandbox")))] + _ => { + let c_path = std::ffi::CString::new(path) + .map_err(|_| crate::PyError::value_error("embedded null byte"))?; + let dirp = unsafe { libc::opendir(c_path.as_ptr()) }; + if dirp.is_null() { + return Err(errno_err_with_filename(crate::builtins::crt_errno(), w_path())); + } + let errno = readdir_collect(dirp, |name, ino, d_type| { + let full = join_dir_name(path, name); + scandir_push_entry(list_slot, bytes_mode, name, &full, -1, ino, d_type); + }); + unsafe { libc::closedir(dirp) }; + if errno != 0 { + return Err(errno_err_with_filename(errno, w_path())); } } + // No raw `readdir` to read the dirent from (wasm, the sandbox seam, + // or a build without `host_env`), so `d_type` is unknown and + // `is_dir` stats; the inode is still free from the dirent on unix. + #[cfg(not(all(unix, feature = "host_env", not(feature = "sandbox"))))] _ => { let entries = host_fs::read_dir(path_from_bytes(path).as_ref()) .map_err(|e| fs_err_with_filename(e, w_path()))?; @@ -4226,12 +4391,21 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { let entry = entry.map_err(|e| fs_err_with_filename(e, w_path()))?; let name = entry.file_name(); let full = entry.path().into_os_string(); + #[cfg(unix)] + let enum_ino = { + use std::os::unix::fs::DirEntryExt; + entry.ino() as i64 + }; + #[cfg(not(unix))] + let enum_ino = -1i64; scandir_push_entry( list_slot, bytes_mode, name.as_encoded_bytes(), full.as_encoded_bytes(), -1, + enum_ino, + DT_UNKNOWN, ); } }