diff --git a/issues/a-pipe-console-socket-or-shared-memory-has-no-identity.md b/issues/a-pipe-console-socket-or-shared-memory-has-no-identity.md new file mode 100644 index 0000000000..e81140a2c6 --- /dev/null +++ b/issues/a-pipe-console-socket-or-shared-memory-has-no-identity.md @@ -0,0 +1,24 @@ +--- +status: open +kind: defect +opened: 2026-10-11 +--- + +# A pipe, console, socket or shared memory has no identity + +`ops::fstat` (`kernel/src/object/ops.rs`) answers `volume: 0, node: 0` for +every object but a file: a pipe or tty end, a connection, an acceptor, a +console, shared memory, a device and the capability objects. libc answers the +pair as `st_dev` and `st_ino` and std as `MetadataExt::dev` and `ino`, so +every such descriptor is one file to a program that asks: two pipes, a pipe's +two ends and a console beside a pipe all compare equal, and `0` is the pair a +caller reads as "nothing". POSIX's `` says `st_ino` and `st_dev` +uniquely identify a file, a pipe included. + +**Read off the code; no program that depends on it has been seen to fail.** + +**Exit condition.** `fstat` of two pipes answers two nonzero pairs, both ends +of one pipe answer one, and a console answers one apart from them; a guest C +case reads them. + +**Owner.** `kernel/src/object/ops.rs`. Nobody holds it. diff --git a/issues/no-physical-memory-fairness.md b/issues/no-physical-memory-fairness.md index 0454f4d516..37f20e6d2a 100644 --- a/issues/no-physical-memory-fairness.md +++ b/issues/no-physical-memory-fairness.md @@ -27,7 +27,12 @@ costs and refuse past it, and a pipe allocates its ring page on first use rather than at `create`. **What a bound alone still does not answer is whose window to refuse.** The memory is charged to nobody, so a cap is the only thing between one client and the machine — which is this entry, and is why both of those functions' -doc comments name a kernel memory limit as what deletes them. +doc comments name a kernel memory limit as what deletes them. A third such cap +is the kernel's directory numbering, `MAX_DIRECTORIES` in `kernel/pure/dirnodes.rs`, +and its refusal crosses processes: once one process's `/tmp` names fill it, every +process's `stat`, `access`, `opendir` or listing that reaches a directory not yet +numbered is refused `ResourceExhausted`. Charging each number to whoever made it +deletes the cap. **Promoted to `defect` 2026-08-25** (finding-lifecycle ruling: isolation findings are security-adjacent and are promoted, never folded). One process diff --git a/issues/the-kernel-numbers-no-directory-symlink-or-listing-entry.md b/issues/the-kernel-numbers-no-directory-symlink-or-listing-entry.md deleted file mode 100644 index e71abfaf36..0000000000 --- a/issues/the-kernel-numbers-no-directory-symlink-or-listing-entry.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-01 ---- - -# The kernel numbers no directory, symlink or listing entry - -A file's identity is `toyos_abi::syscall::Stat`'s `volume` and `node`, which -libc answers as `st_dev` and `st_ino` and std as `MetadataExt::dev` and `ino`. -Every file the kernel opens, and every name a file server stats, has one. What -the kernel opens not has none: `SYS_OPEN` refuses a directory and a symlink, so -on `/system` and `/tmp` libc's `stat` of a directory and `lstat` of a symlink -answer `st_dev = st_ino = 0` (`userland/libc/src/posix_io.rs`), as std's do -(`sdk/std/sys/fs.rs`), and every such name is one file to a program that -asks. clang's `FileManager` keys directories on the pair too -(`getDirectoryRef`'s `UniqueRealDirs`), so every kernel directory is one -`DirectoryEntry` to it. `SYS_READDIR` answers no identity, so `readdir`'s -`d_ino` is the entry's offset in the listing, which no `st_ino` equals. Pipes, -consoles and the other kernel objects answer 0 as well. - -**Exit**: `stat` of two kernel directories, and `lstat` of two kernel symlinks, -answer two nonzero pairs, `d_ino` is its entry's `st_ino`, and a guest C case -reads them for two directories and a link. diff --git a/issues/toyos-builds-itself.md b/issues/toyos-builds-itself.md index 522edd739b..e18419ff40 100644 --- a/issues/toyos-builds-itself.md +++ b/issues/toyos-builds-itself.md @@ -61,10 +61,8 @@ libc's state it is `userland/libc` at `15625e0cb`. `socketpair`, which it does not define; a file `mmap` and record locks (`issues/libc-refuses-what-toyos-cannot-yet-answer.md`); paths resolved by POSIX's rules, which `realpath` answers by the kernel's - (`issues/the-kernel-resolves-a-path-by-rules-of-its-own.md`); file - identities - (`issues/the-kernel-numbers-no-directory-symlink-or-listing-entry.md`), - served like the locks by fsd; and socket descriptors that are pollable and + (`issues/the-kernel-resolves-a-path-by-rules-of-its-own.md`); and socket + descriptors that are pollable and can be non-blocking (`issues/libc-close-of-a-socket-ends-the-process.md`). Read from each library's source and not run: SQLite's default VFS takes `fcntl` record @@ -130,9 +128,8 @@ commit that lands M2's compile-and-run test, which links inside the guest. --hosted-clang` makes a clang and an `ld.lld` for `x86_64-unknown-toyos`, static PIEs that name no library (`src/hostedclang.rs`): 147.5 MB and 85.6 MB, 121.9 MB and 70.9 MB stripped. No guest has run either. -- Run: what `issues/libc-refuses-what-toyos-cannot-yet-answer.md` lists, - `issues/libc-has-no-pread-or-pwrite.md`, and - `issues/the-kernel-numbers-no-directory-symlink-or-listing-entry.md`. +- Run: what `issues/libc-refuses-what-toyos-cannot-yet-answer.md` lists, and + `issues/libc-has-no-pread-or-pwrite.md`. **What M3 adds: a rustc that carries that LLVM**, after all of M2's. - Build: `issues/rustc-llvm-cannot-build-for-a-toyos-host.md`, and diff --git a/kernel/pure/dirnodes.rs b/kernel/pure/dirnodes.rs new file mode 100644 index 0000000000..94ab07d768 --- /dev/null +++ b/kernel/pure/dirnodes.rs @@ -0,0 +1,203 @@ +//! The numbers a kernel volume gives its directories +//! (`toyos_abi::syscall::Stat::node`). +//! +//! ROOT and `/tmp` keep no record of a directory: one is the prefix its names +//! share, or a name `mkdir` left in the VFS. So the VFS numbers each by its +//! absolute path, drawing the number on the first ask, and forgets it once a +//! removal beneath the path leaves no directory there: a number is never +//! another directory's while both exist. The paths are userland's choice, so +//! what this holds is bounded: [`MAX_DIRECTORIES`] keys of at most +//! [`MAX_PATH`] bytes, a directory past either refused by name. + +#![forbid(unsafe_code)] + +use alloc::collections::BTreeMap; +use alloc::string::String; +use toyos_abi::syscall::{SyscallError, MAX_PATH}; + +/// The most directories numbered at once: past it a directory not yet +/// numbered is refused `ResourceExhausted` until one that is ends. +pub const MAX_DIRECTORIES: usize = 16_384; + +#[derive(Default)] +pub struct Directories { + of: BTreeMap, +} + +impl Directories { + pub const fn new() -> Self { + Self { of: BTreeMap::new() } + } + + /// The number of the directory at `path`, drawn by `draw` when this does + /// not hold one; `draw` answers a number nothing else on its volume holds. + /// A path longer than [`MAX_PATH`] is refused `InvalidArgument`, as `cd` + /// and `mkdir` refuse it, and a new one past [`MAX_DIRECTORIES`] + /// `ResourceExhausted`. + pub fn of(&mut self, path: &str, draw: impl FnOnce() -> u64) -> Result { + if let Some(&number) = self.of.get(path) { + return Ok(number); + } + if path.len() > MAX_PATH { + return Err(SyscallError::InvalidArgument); + } + if self.of.len() >= MAX_DIRECTORIES { + return Err(SyscallError::ResourceExhausted); + } + let number = draw(); + self.of.insert(String::from(path), number); + Ok(number) + } + + /// A name at `path` was removed or moved away: `path` and every directory + /// above it whose number this holds and that `exists` no longer answers for + /// is forgotten, so a directory made there later is drawn a new one. The + /// walk begins at the longest of them within [`MAX_PATH`], since nothing + /// longer is held, so its length is bounded whatever `path`'s is. + pub fn removed(&mut self, path: &str, mut exists: impl FnMut(&str) -> bool) { + let mut at = path; + if at.len() > MAX_PATH { + match at.as_bytes()[..=MAX_PATH].iter().rposition(|&b| b == b'/') { + Some(0) | None => return, + Some(slash) => at = &at[..slash], + } + } + loop { + if self.of.contains_key(at) && !exists(at) { + self.of.remove(at); + } + match at.rfind('/') { + Some(0) | None => return, + Some(slash) => at = &at[..slash], + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::format; + use std::cell::Cell; + use std::vec::Vec; + + /// A counter that never rewinds, as the file cache's is. + fn counter() -> impl FnMut() -> u64 { + let mut next = 0; + move || { + next += 1; + next + } + } + + fn never() -> u64 { + panic!("a held number is never drawn again, and a refused one never drawn") + } + + /// Asked twice, a directory answers one number; two directories answer two. + #[test] + fn a_directory_is_one_number_and_two_are_two() { + let mut draw = counter(); + let mut dirs = Directories::new(); + let a = dirs.of("/tmp/a", &mut draw).unwrap(); + let b = dirs.of("/tmp/b", &mut draw).unwrap(); + assert_ne!(a, b); + assert_eq!(dirs.of("/tmp/a", never), Ok(a)); + } + + /// A removal that leaves the directory standing keeps its number; one that + /// ends it forgets it, and the directory made there next is a new one. + #[test] + fn a_number_lasts_exactly_as_long_as_its_directory() { + let mut draw = counter(); + let mut dirs = Directories::new(); + let a = dirs.of("/tmp/a", &mut draw).unwrap(); + + dirs.removed("/tmp/a/f", |_| true); + assert_eq!(dirs.of("/tmp/a", never), Ok(a)); + + dirs.removed("/tmp/a/f", |dir| dir != "/tmp/a"); + let again = dirs.of("/tmp/a", &mut draw).unwrap(); + assert_ne!(again, a, "a directory made again where one was removed is another directory"); + } + + /// Every directory above the removal is judged, not only its parent, and + /// one that only shares a prefix with it is not. + #[test] + fn a_removal_forgets_every_directory_it_ended_and_no_other() { + let mut draw = counter(); + let mut dirs = Directories::new(); + let tmp = dirs.of("/tmp", &mut draw).unwrap(); + let a = dirs.of("/tmp/a", &mut draw).unwrap(); + let b = dirs.of("/tmp/a/b", &mut draw).unwrap(); + let sibling = dirs.of("/tmp/ab", &mut draw).unwrap(); + + dirs.removed("/tmp/a/b/f", |dir| dir == "/tmp" || dir == "/tmp/ab"); + assert_eq!(dirs.of("/tmp", never), Ok(tmp)); + assert_eq!(dirs.of("/tmp/ab", never), Ok(sibling)); + let (a2, b2) = (dirs.of("/tmp/a", &mut draw).unwrap(), dirs.of("/tmp/a/b", &mut draw).unwrap()); + assert!(![tmp, a, b, sibling].contains(&a2) && ![tmp, a, b, sibling, a2].contains(&b2)); + } + + /// The mount's directories are asked about only where a number is held: + /// a removal costs nothing for what nobody numbered. + #[test] + fn only_a_held_number_is_judged() { + let mut dirs = Directories::new(); + dirs.of("/tmp/a", || 7).unwrap(); + let asked = Cell::new(Vec::new()); + dirs.removed("/tmp/a/b/c/f", |dir| { + let mut seen = asked.take(); + seen.push(String::from(dir)); + asked.set(seen); + true + }); + assert_eq!(asked.take(), [String::from("/tmp/a")]); + } + + /// A path of `MAX_PATH` bytes is numbered; one byte more is refused by + /// name, and nothing is drawn or held for it. + #[test] + fn a_path_past_max_path_is_refused_invalid() { + let mut draw = counter(); + let mut dirs = Directories::new(); + let longest = format!("/{}", "a".repeat(MAX_PATH - 1)); + assert!(dirs.of(&longest, &mut draw).is_ok()); + let past = format!("{longest}a"); + assert_eq!(dirs.of(&past, never), Err(SyscallError::InvalidArgument)); + assert_eq!(dirs.of.len(), 1); + } + + /// Full, a new directory is refused by name while a numbered one still + /// answers; once one ends, a new one is numbered again. + #[test] + fn past_max_directories_a_new_directory_is_refused_exhausted() { + let mut draw = counter(); + let mut dirs = Directories::new(); + for i in 0..MAX_DIRECTORIES { + dirs.of(&format!("/tmp/{i}"), &mut draw).unwrap(); + } + assert_eq!(dirs.of("/tmp/new", never), Err(SyscallError::ResourceExhausted)); + assert_eq!(dirs.of("/tmp/0", never), Ok(1)); + + dirs.removed("/tmp/0/f", |dir| dir != "/tmp/0"); + assert!(dirs.of("/tmp/new", &mut draw).is_ok()); + assert_eq!(dirs.of("/tmp/0", never), Err(SyscallError::ResourceExhausted)); + } + + /// A removal beneath a path longer than `MAX_PATH` still judges every + /// numbered directory above it. + #[test] + fn a_removal_past_max_path_judges_the_numbered_above_it() { + let mut draw = counter(); + let mut dirs = Directories::new(); + let near = format!("/tmp/{}", "a".repeat(MAX_PATH - 5)); + assert_eq!(near.len(), MAX_PATH); + let top = dirs.of("/tmp", &mut draw).unwrap(); + dirs.of(&near, &mut draw).unwrap(); + let deep = format!("{near}{}/f", "/b".repeat(MAX_PATH)); + dirs.removed(&deep, |dir| dir == "/tmp"); + assert_eq!(dirs.of("/tmp", never), Ok(top)); + assert_ne!(dirs.of(&near, &mut draw), Ok(2), "the directory MAX_PATH long ended and kept its number"); + } +} diff --git a/kernel/pure/lib.rs b/kernel/pure/lib.rs index deda3b590e..0a55604924 100644 --- a/kernel/pure/lib.rs +++ b/kernel/pure/lib.rs @@ -3,7 +3,8 @@ //! address space is handed ([`pcid`]), what type the range registers give a //! range ([`mtrr`]), what a pipe's ends are told of each other ([`pipe`]), what //! a CPU waiting on the boot processor's write to `SMI_CMD` decides -//! ([`bootwrite`]) and what the clock computes from counter ticks ([`clock`]). +//! ([`bootwrite`]), what the clock computes from counter ticks ([`clock`]) and +//! which number a kernel volume's directory answers ([`dirnodes`]). //! The kernel binary links it; the host runs //! its tests, because none of it reads a register, a clock or a kernel lock. @@ -16,6 +17,7 @@ extern crate std; pub mod bootwrite; pub mod clock; +pub mod dirnodes; pub mod mtrr; pub mod pcid; pub mod pipe; diff --git a/kernel/src/bcachefs_adapter.rs b/kernel/src/bcachefs_adapter.rs index 94ff0fc2be..48d1ce3017 100644 --- a/kernel/src/bcachefs_adapter.rs +++ b/kernel/src/bcachefs_adapter.rs @@ -60,24 +60,71 @@ fn present(op: &str, name: &str, result: Result, FsError>) -> Resul } +/// What a name on ROOT is numbered by, once asked. +#[derive(Clone, Copy)] +enum Node { + /// A file: the `FileId` every open of it answers. + File(FileId), + /// A symlink: a number `file_cache::draw_node` drew, which no file holds. + Symlink(u64), +} + /// VFS adapter for ROOT, a read-only bcachefs volume in memory. /// /// Every backing it hands out reads the image the mount was opened over, so a /// file cannot be served off bytes its filesystem does not occupy. pub struct ReadOnlyBcacheFsAdapter { fs: Mounted, - name_to_id: BTreeMap, + /// Every name's node once asked. The volume never changes, so a name is + /// one entry for the boot. + nodes: BTreeMap, } impl ReadOnlyBcacheFsAdapter { pub fn new(fs: Mounted) -> Self { - Self { fs, name_to_id: BTreeMap::new() } + Self { fs, nodes: BTreeMap::new() } + } + + /// The `FileId` of the file `name`, `size` bytes long, made with no + /// reference held when this is its first ask; `Io` for a name the volume + /// answered as a symlink before and as a file now, which only a corrupt + /// image can. + fn file_id(&mut self, name: &str, size: u64) -> Result { + match self.nodes.get(name) { + Some(&Node::File(file_id)) => return Ok(file_id), + Some(&Node::Symlink(_)) => { + log!("bcachefs: '{}' has extents and was a symlink", name); + return Err(SyscallError::Io); + } + None => {} + } + // Its mtime is the volume's (`file_mtime`), never the cache's. + let file_id = file_cache::create_file(true, 0); + file_cache::set_size(file_id, size); + file_cache::release(file_id); + self.nodes.insert(String::from(name), Node::File(file_id)); + Ok(file_id) } } impl FileSystem for ReadOnlyBcacheFsAdapter { - fn list(&mut self, dir: &str, limit: usize) -> Result, SyscallError> { - mapped("list", dir, self.fs.list(limit, &|name| crate::vfs::under_directory(name, dir))) + fn list(&mut self, dir: &str, limit: usize) -> Result, SyscallError> { + let listed = mapped("list", dir, self.fs.list(limit, &|name| crate::vfs::under_directory(name, dir)))?; + Ok(listed.into_iter().map(|(name, _)| name).collect()) + } + + fn node(&mut self, name: &str) -> Result { + match self.nodes.get(name) { + Some(&(Node::File(node) | Node::Symlink(node))) => return Ok(node), + None => {} + } + if mapped("is_symlink", name, self.fs.is_symlink(name))? { + let node = file_cache::draw_node(); + self.nodes.insert(String::from(name), Node::Symlink(node)); + return Ok(node); + } + let (_, size) = present("node", name, self.fs.file_extents(name))?; + self.file_id(name, size) } fn is_dir(&mut self, dir: &str) -> Result { @@ -94,18 +141,8 @@ impl FileSystem for ReadOnlyBcacheFsAdapter { fn open_file(&mut self, name: &str) -> Result<(FileId, Option>), SyscallError> { let (extents, size) = present("open", name, self.fs.file_extents(name))?; - if let Some(&file_id) = self.name_to_id.get(name) { - file_cache::open(file_id).commit(); - let backing = Arc::new(ReadOnlyBacking::new(*self.fs.io(), extents, size)); - return Ok((file_id, Some(backing))); - } - - // Its mtime is the volume's (`file_mtime`), never the cache's. - let file_id = file_cache::create_file(true, 0); - file_cache::set_size(file_id, size); - - self.name_to_id.insert(String::from(name), file_id); - + let file_id = self.file_id(name, size)?; + file_cache::open(file_id).commit(); let backing = Arc::new(ReadOnlyBacking::new(*self.fs.io(), extents, size)); Ok((file_id, Some(backing))) } diff --git a/kernel/src/file_cache.rs b/kernel/src/file_cache.rs index 3e30efea39..0d29ebfc4d 100644 --- a/kernel/src/file_cache.rs +++ b/kernel/src/file_cache.rs @@ -87,6 +87,16 @@ pub fn create_file(evictable: bool, mtime: u64) -> FileId { id } +/// A number from the count `FileId`s are drawn from, which no file will ever +/// hold: a kernel volume's node for a name the cache keeps no file for, so no +/// node on a kernel volume is another's (`vfs::FileSystem::node`). +pub fn draw_node() -> u64 { + let mut cache = FILE_CACHE.lock(); + let node = cache.next_id; + cache.next_id += 1; + node +} + /// Point a file at the store its evicted pages come back from; idempotent across opens of the same file. pub fn set_backing(file_id: FileId, backing: Arc) { let mut cache = FILE_CACHE.lock(); diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs index ecaad068c3..959490458f 100644 --- a/kernel/src/object/ops.rs +++ b/kernel/src/object/ops.rs @@ -116,8 +116,8 @@ pub fn open(table: &mut HandleTable, path: &str, flags: OpenFlags) -> u64 { let built = if truncate && create { let mtime = crate::clock::mtime_now(); // `NotFound` is not a failure: truncating past a name that was not there is fine. - // Any `vfs.delete` error other than `NotFound` is propagated, not swallowed: truncating past it could silently create a file over one the mount could not confirm was missing. - match vfs.delete(target.as_str()) { + // Any `vfs.delete_file` error other than `NotFound` is propagated, not swallowed: truncating past it could silently create a file over one the mount could not confirm was missing. + match vfs.delete_file(target.as_str()) { Ok(()) | Err(SyscallError::NotFound) => {} Err(e) => return e.to_u64(), } diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index f8beb9ade0..e61b604b56 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -29,7 +29,7 @@ use super::device::{ }; use super::fs::{ spawn_cwd, sys_chdir, sys_delete, sys_getcwd, sys_mkdir, sys_open, sys_readdir, sys_readlink, - sys_rename, sys_rmdir, sys_symlink, + sys_rename, sys_rmdir, sys_stat, sys_symlink, }; use super::handles::{sys_close, sys_dup2, sys_handle_dup, with_object, with_object_ref}; use super::io::{ @@ -138,6 +138,22 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> Err(e) => e.refuse(), } } + SYS_STAT => { + let path = match ctx.user_str(UserAddr::new(a1), a2) { Ok(s) => s, Err(e) => return e.to_u64() }; + let follow = match a4 { + 0 => false, + 1 => true, + _ => return SyscallError::InvalidArgument.to_u64(), + }; + let stat = match sys_stat(&path, follow) { + Ok(stat) => stat, + Err(e) => return e.to_u64(), + }; + match ctx.copy_out(UserAddr::new(a3), &stat) { + Ok(()) => 0, + Err(e) => e.to_u64(), + } + } SYS_READDIR => { let path = match ctx.user_str(UserAddr::new(a1), a2) { Ok(s) => s, Err(e) => return e.to_u64() }; let Some(mut buf) = ctx.user_bytes_mut(UserAddr::new(a3), a4) else { return bad_addr }; diff --git a/kernel/src/syscall/fs.rs b/kernel/src/syscall/fs.rs index 2685487808..856ba4b055 100644 --- a/kernel/src/syscall/fs.rs +++ b/kernel/src/syscall/fs.rs @@ -38,6 +38,14 @@ pub(super) fn sys_open(path: &str, flags: OpenFlags) -> u64 { process::with_process_data(|data| ops::open(&mut data.handles, &resolved, flags)) } +/// What `path` names, a final symlink followed when `follow` says so. +pub(super) fn sys_stat(path: &str, follow: bool) -> Result { + let cwd = process::with_process_data(|d| d.cwd.clone()); + let mut vfs = vfs::lock(); + let resolved = vfs.resolve_absolute(&cwd, path); + vfs.stat(&resolved, follow) +} + /// Returns the length needed; writes the listing only when it fits (`n <= out.len()`). pub(super) fn sys_readdir(path: &str, out: &mut UserBytesMut) -> u64 { let cwd = process::with_process_data(|d| d.cwd.clone()); @@ -54,7 +62,7 @@ pub(super) fn sys_readdir(path: &str, out: &mut UserBytesMut) -> u64 { } let mut pos = 0; - for (name, size) in &entries { + for (name, node) in &entries { let is_dir = name.ends_with('/'); let clean_name = if is_dir { &name[..name.len() - 1] } else { name.as_str() }; out.write_at(pos, &[if is_dir { DIRENT_DIR } else { DIRENT_FILE }]); @@ -63,7 +71,7 @@ pub(super) fn sys_readdir(path: &str, out: &mut UserBytesMut) -> u64 { pos += clean_name.len(); out.write_at(pos, &[0]); pos += 1; - out.write_at(pos, &size.to_le_bytes()); + out.write_at(pos, &node.to_le_bytes()); pos += 8; } debug_assert_eq!(pos, needed); diff --git a/kernel/src/tmpfs.rs b/kernel/src/tmpfs.rs index 1a6fc879c8..d16084900a 100644 --- a/kernel/src/tmpfs.rs +++ b/kernel/src/tmpfs.rs @@ -52,7 +52,8 @@ fn retire(id: FileId, alive: &AtomicBool) { /// each see a different namespace. pub(crate) enum Entry { File { id: FileId, alive: Arc }, - Symlink { target: String }, + /// `node` is drawn at the link's making and moves with its entry. + Symlink { target: String, node: u64 }, } /// In-memory filesystem: file data lives in the file cache. @@ -100,22 +101,30 @@ impl ReplaceRename for TmpFs { impl FileSystem for TmpFs { // Nothing else caps file count here. - fn list(&mut self, dir: &str, limit: usize) -> Result, SyscallError> { + fn list(&mut self, dir: &str, limit: usize) -> Result, SyscallError> { let mut out = Vec::new(); for (name, entry) in &self.entries { - if let Entry::File { id, .. } = entry { + if let Entry::File { .. } = entry { if !crate::vfs::under_directory(name, dir) { continue; } if out.len() == limit { return Err(SyscallError::ResourceExhausted); } - out.push((name.clone(), file_cache::size(*id))); + out.push(name.clone()); } } Ok(out) } + fn node(&mut self, name: &str) -> Result { + match self.entries.get(name) { + Some(Entry::File { id, .. }) => Ok(*id), + Some(Entry::Symlink { node, .. }) => Ok(*node), + None => Err(SyscallError::NotFound), + } + } + /// The first name at or after `dir/` in key order is the only one that can /// lie beneath it. fn is_dir(&mut self, dir: &str) -> Result { @@ -137,7 +146,7 @@ impl FileSystem for TmpFs { fn read_link(&mut self, name: &str) -> Result, SyscallError> { Ok(match self.entries.get(name) { - Some(Entry::Symlink { target }) => Some(target.clone()), + Some(Entry::Symlink { target, .. }) => Some(target.clone()), _ => None, }) } @@ -189,8 +198,8 @@ impl FileSystem for TmpFs { fn create_symlink(&mut self, name: &str, target: &str) -> Result<(), SyscallError> { // Displaces whatever answered to this name: one name, one entry. - if let Some(Entry::File { id, alive, .. }) = - self.entries.insert(String::from(name), Entry::Symlink { target: String::from(target) }) + let link = Entry::Symlink { target: String::from(target), node: file_cache::draw_node() }; + if let Some(Entry::File { id, alive, .. }) = self.entries.insert(String::from(name), link) { retire(id, &alive); } diff --git a/kernel/src/vfs.rs b/kernel/src/vfs.rs index 4b3cdf1ced..b8a4d0a4fb 100644 --- a/kernel/src/vfs.rs +++ b/kernel/src/vfs.rs @@ -5,9 +5,11 @@ use alloc::collections::BTreeSet; use alloc::vec::Vec; use core::ops::{Deref, DerefMut}; -use toyos_abi::syscall::{SyscallError, MAX_LINKS, MAX_PATH}; -use crate::file_cache::FileId; +use toyos_abi::syscall::{FileType, SyscallError, MAX_LINKS, MAX_PATH}; +use crate::file_cache::{self, FileId}; +use crate::object::ops::Stat; use crate::sync::{Lock, LockGuard}; +use kernel::dirnodes::Directories; static VFS: Lock> = Lock::new(None); @@ -34,7 +36,13 @@ pub fn lock() -> VfsGuard { pub trait FileSystem: Send { /// Every name [`under_directory`] puts at or under `dir` (`""` is the mount /// root), or `ResourceExhausted` above `limit`, which counts only those. - fn list(&mut self, dir: &str, limit: usize) -> Result, SyscallError>; + fn list(&mut self, dir: &str, limit: usize) -> Result, SyscallError>; + + /// The number of the file or symlink `name`, not followed, as + /// `toyos_abi::syscall::Stat::node` says: a file's is the `FileId` + /// [`open_file`](Self::open_file) answers, a symlink's one drawn by + /// `file_cache::draw_node`; `NotFound` for anything else. + fn node(&mut self, name: &str) -> Result; /// Whether `dir` (`""` is the mount root) is a directory: a working /// directory is judged by this, on every spawn and `SYS_CHDIR`, under the @@ -116,6 +124,19 @@ impl MountPoint { } } +/// The volume `/` and every [`ROOT_ENTRIES`] name nothing is mounted at are +/// on: the directories the VFS makes itself, below a file server's bit 63 and +/// above every [`MountPoint::volume`]. +const NAMESPACE_VOLUME: u64 = (1 << 63) - 1; + +/// The node of `/` on [`NAMESPACE_VOLUME`]. +const ROOT_NODE: u64 = 1; + +/// The node on [`NAMESPACE_VOLUME`] of the [`ROOT_ENTRIES`] name at `slot`. +fn namespace_node(slot: usize) -> u64 { + ROOT_NODE + 1 + slot as u64 +} + /// A path with every symlink followed, minted only by [`Vfs::resolve_for_open`]: /// the mount an `OpenTarget` names is the mount opened, never one a link aimed away from. pub struct OpenTarget(String); @@ -138,6 +159,8 @@ pub struct Vfs { /// Each mounted filesystem once, however many names reach it. mounts: Vec, created_dirs: BTreeSet, + /// Every mounted directory's node, by absolute path. + dirs: Directories, } /// The most entries one `FileSystem::list` may materialise for one directory. @@ -188,6 +211,7 @@ impl Vfs { at: [const { None }; ROOT_ENTRIES.len()], mounts: Vec::new(), created_dirs: BTreeSet::new(), + dirs: Directories::new(), } } @@ -271,21 +295,47 @@ impl Vfs { return Err(SyscallError::InvalidArgument); } - if self.created_dirs.contains(&abs) { - return Ok(abs); - } - // A mount point exists whether or not anything is mounted on it; and // under one this kernel mounts nothing at, the directory is a file // server's, which the caller's client asked and this kernel cannot. - if Self::entry(&mount).is_some() && (subdir.is_empty() || self.point(&mount).is_none()) { + if Self::entry(&mount).is_some() && self.point(&mount).is_none() { return Ok(abs); } - let (fs, fs_path) = self.resolve_fs(&mount, &subdir).ok_or(SyscallError::NotFound)?; - if fs.is_dir(&fs_path)? { Ok(abs) } else { Err(SyscallError::NotFound) } + if self.is_mounted_dir(&mount, &subdir)? { Ok(abs) } else { Err(SyscallError::NotFound) } } + /// Whether `/mount/subdir` is a directory of what is mounted at `mount`: + /// its root, one `mkdir` left in `created_dirs`, or one the filesystem holds. + fn is_mounted_dir(&mut self, mount: &str, subdir: &str) -> Result { + let Some(point) = self.point(mount) else { return Ok(false) }; + Ok(subdir.is_empty() + || self.created_dirs.contains(&directory(mount, subdir)) + || self.mounts[point.fs].fs.is_dir(&point.path(subdir))?) + } + + /// The node of the mounted directory at the absolute `path` + /// ([`Directories`]), which the caller found is one; refused past + /// [`Directories`]'s bounds. + fn dir_node(&mut self, path: &str) -> Result { + self.dirs.of(path, file_cache::draw_node) + } + + /// A name at the absolute `path` was removed or moved away: every + /// directory that ended with it loses its node. A mount that cannot answer + /// keeps the node, since forgetting a directory that stands renumbers it. + fn forget_ended_dirs(&mut self, path: &str) { + let mut dirs = core::mem::take(&mut self.dirs); + dirs.removed(path, |dir| { + let (mount, subdir) = self.resolve_path("/", dir); + self.is_mounted_dir(&mount, &subdir).unwrap_or(true) + }); + self.dirs = dirs; + } + + /// Each entry of the directory, a directory's name ending in `/`, and its + /// node, as [`Self::stat`] answers it. + /// /// `list` refuses above [`MAX_LIST_ENTRIES`] rather than truncating because a short listing is a confidently wrong answer to a caller checking existence or deleting a tree. pub fn list(&mut self, cwd: &str, path: &str) -> Result, SyscallError> { let (mount, subdir) = if path.is_empty() { @@ -295,10 +345,18 @@ impl Vfs { }; if mount.is_empty() { - return Ok(ROOT_ENTRIES.iter().map(|name| (format!("{name}/"), 0)).collect()); + let mut entries = Vec::with_capacity(ROOT_ENTRIES.len()); + for (slot, name) in ROOT_ENTRIES.iter().enumerate() { + let node = match self.point(name) { + Some(_) => self.dir_node(&format!("/{name}"))?, + None => namespace_node(slot), + }; + entries.push((format!("{name}/"), node)); + } + return Ok(entries); } - let Some((fs, fs_path)) = self.resolve_fs(&mount, &subdir) else { + let Some(point) = self.point(&mount) else { // A mount point with nothing mounted on it is an empty directory; // anything under it, and any other name, is not there at all. return match Self::entry(&mount) { @@ -306,7 +364,9 @@ impl Vfs { _ => Err(SyscallError::NotFound), }; }; - let all_files = fs.list(&fs_path, MAX_LIST_ENTRIES)?; + let fs_path = point.path(&subdir); + let all_files = self.mounts[point.fs].fs.list(&fs_path, MAX_LIST_ENTRIES)?; + let listed = directory(&mount, &subdir); let prefix = if fs_path.is_empty() { String::new() @@ -319,12 +379,12 @@ impl Vfs { } // Dedup only removes entries, so `matching` is a true upper bound on the final result count. - let matching = all_files.iter().filter(|(n, _)| under_prefix(n, &prefix).is_some()).count(); + let matching = all_files.iter().filter(|n| under_prefix(n, &prefix).is_some()).count(); let mut result = Vec::with_capacity(matching); let mut seen_dirs = BTreeSet::new(); let mut saw_self = false; - for (name, size) in &all_files { + for name in &all_files { let Some(rest) = under_prefix(name, &prefix) else { continue }; // The listed directory's own entry: proof it exists, not a child. @@ -333,18 +393,19 @@ impl Vfs { continue; } if let Some(slash_pos) = rest.find('/') { - let dir_name = format!("{}/", &rest[..slash_pos]); - if seen_dirs.insert(dir_name.clone()) { - result.push((dir_name, 0)); + let child = &rest[..slash_pos]; + if seen_dirs.insert(format!("{child}/")) { + let node = self.dir_node(&format!("{listed}/{child}"))?; + result.push((format!("{child}/"), node)); } } else { - result.push((String::from(rest), *size)); + result.push((String::from(rest), self.mounts[point.fs].fs.node(name)?)); } } // A directory the VFS carries is its parent's entry whether or not a // file is under it yet. - let parent = format!("{}/", directory(&mount, &subdir)); + let parent = format!("{listed}/"); for dir in self.created_dirs.range(parent.clone()..) { let Some(rest) = dir.strip_prefix(parent.as_str()) else { break }; let child = rest.split('/').next().unwrap_or(rest); @@ -354,7 +415,7 @@ impl Vfs { if result.len() == MAX_LIST_ENTRIES { return Err(SyscallError::ResourceExhausted); } - result.push((dir_name, 0)); + result.push((dir_name, self.dirs.of(&format!("{parent}{child}"), file_cache::draw_node)?)); } } } @@ -377,30 +438,75 @@ impl Vfs { if intent == ResolveIntent::UserModify && !self.user_may_modify(path) { return Err(SyscallError::PermissionDenied); } - let target = self.resolve_for_open_depth(path, 0)?; + let (mount, file) = self.followed(path)?; + // A mount point is a directory, never a file to open. + if file.is_empty() || self.point(&mount).is_none() { + return Err(SyscallError::NotFound); + } + let target = OpenTarget(format!("/{mount}/{file}")); if intent == ResolveIntent::UserModify && !self.user_may_modify(target.as_str()) { return Err(SyscallError::PermissionDenied); } Ok(target) } - fn resolve_for_open_depth(&mut self, path: &str, depth: u32) -> Result { - if depth > MAX_LINKS { return Err(SyscallError::InvalidArgument); } - let (mount, file) = self.resolve_path("/", path); - // A mount point is a directory, never a file to open. - if file.is_empty() { return Err(SyscallError::NotFound); } - let (fs, fs_path) = self.resolve_fs(&mount, &file).ok_or(SyscallError::NotFound)?; - if let Some(target) = fs.read_link(&fs_path)? { + /// The mount and the path under it that the absolute `path` names once + /// every symlink it ends in is followed, [`MAX_LINKS`] at most. + fn followed(&mut self, path: &str) -> Result<(String, String), SyscallError> { + let (mut mount, mut file) = self.resolve_path("/", path); + for _ in 0..=MAX_LINKS { + let link = match self.resolve_fs(&mount, &file) { + Some((fs, fs_path)) if !file.is_empty() => fs.read_link(&fs_path)?, + _ => None, + }; + let Some(target) = link else { return Ok((mount, file)) }; // An absolute target names the whole hierarchy; a relative one is // read against the mount the link is on. - let next = if target.starts_with('/') { - target - } else { - format!("/{}/{}", mount, target) + let next = if target.starts_with('/') { target } else { format!("/{mount}/{target}") }; + (mount, file) = self.resolve_path("/", &next); + } + Err(SyscallError::InvalidArgument) + } + + /// What the absolute `path` names, a final symlink followed when `follow` + /// says so, as `toyos_abi::syscall::Stat` describes it. + pub fn stat(&mut self, path: &str, follow: bool) -> Result { + let (mount, file) = if follow { self.followed(path)? } else { self.resolve_path("/", path) }; + let named = |file_type: FileType, size, mtime, volume, node| { + Stat { file_type: file_type as u64, size, mtime, volume, node } + }; + if mount.is_empty() { + return Ok(named(FileType::Directory, 0, 0, NAMESPACE_VOLUME, ROOT_NODE)); + } + let Some(point) = self.point(&mount) else { + return match Self::entry(&mount) { + Some(slot) if file.is_empty() => { + Ok(named(FileType::Directory, 0, 0, NAMESPACE_VOLUME, namespace_node(slot))) + } + _ => Err(SyscallError::NotFound), }; - return self.resolve_for_open_depth(&next, depth + 1); + }; + let volume = point.volume(); + if !file.is_empty() { + let fs_path = point.path(&file); + let fs = &mut self.mounts[point.fs].fs; + if let Some(target) = fs.read_link(&fs_path)? { + return Ok(named(FileType::Symlink, target.len() as u64, 0, volume, fs.node(&fs_path)?)); + } + match fs.file_mtime(&fs_path) { + Ok(mtime) => { + // A file's node is its `FileId`. + let node = fs.node(&fs_path)?; + return Ok(named(FileType::File, file_cache::size(node), mtime, volume, node)); + } + Err(SyscallError::NotFound) => {} + Err(e) => return Err(e), + } + } + if !self.is_mounted_dir(&mount, &file)? { + return Err(SyscallError::NotFound); } - Ok(OpenTarget(format!("/{mount}/{file}"))) + Ok(named(FileType::Directory, 0, 0, volume, self.dir_node(&directory(&mount, &file))?)) } /// `target`'s filesystem, its path there, and its volume ([`MountPoint::volume`]). @@ -435,7 +541,8 @@ impl Vfs { Ok((file_id, point.volume())) } - /// Unlink `path` on its mount. + /// Unlink `path` on its mount, every directory keeping its node: for a + /// caller that puts a file back at once (`ops::open`'s truncate). pub fn delete_file(&mut self, path: &str) -> Result<(), SyscallError> { let (mount, file) = self.resolve_path("/", path); if file.is_empty() { return Err(SyscallError::InvalidArgument); } @@ -450,7 +557,9 @@ impl Vfs { if old_mount != new_mount { return Err(SyscallError::NotSupported); } let point = self.point(&old_mount).ok_or(SyscallError::NotFound)?; let (old_fs_path, new_fs_path) = (point.path(&old_file), point.path(&new_file)); - self.mounts[point.fs].fs.rename(&old_fs_path, &new_fs_path) + self.mounts[point.fs].fs.rename(&old_fs_path, &new_fs_path)?; + self.forget_ended_dirs(&directory(&old_mount, &old_file)); + Ok(()) } /// The `Result` is as much the point as the bound: a caller that discards it and reports success anyway turns the bound into a silent failure. @@ -482,6 +591,12 @@ impl Vfs { /// the point, as in [`Self::create_dir`]. A mount root, a missing name, a /// file, and a non-empty directory are each refused, not reported as removed. pub fn remove_dir(&mut self, path: &str) -> Result<(), SyscallError> { + self.remove_empty_dir(path)?; + self.forget_ended_dirs(&normalize(path)); + Ok(()) + } + + fn remove_empty_dir(&mut self, path: &str) -> Result<(), SyscallError> { let (mount, subdir) = self.resolve_path("/", path); // A mount point (and `/`) is not a directory a caller may remove. if subdir.is_empty() { @@ -507,10 +622,10 @@ impl Vfs { let (fs, fs_path) = self.resolve_fs(&mount, &subdir).ok_or(SyscallError::NotFound)?; let names = fs.list(&fs_path, MAX_LIST_ENTRIES)?; let child_prefix = format!("{fs_path}/"); - let is_file = names.iter().any(|(n, _)| *n == fs_path); + let is_file = names.contains(&fs_path); // A listing mount's own `name/` self-entry is not a child, or every empty directory reads non-empty. let has_file_child = - names.iter().any(|(n, _)| n.starts_with(&child_prefix) && *n != child_prefix); + names.iter().any(|n| n.starts_with(&child_prefix) && *n != child_prefix); // The name resolves to a file, not a directory. if is_file { @@ -547,8 +662,11 @@ impl Vfs { fs.read_link(&fs_path) } + /// Unlink `path`, and forget the node of every directory that ended with it. pub fn delete(&mut self, path: &str) -> Result<(), SyscallError> { - self.delete_file(path) + self.delete_file(path)?; + self.forget_ended_dirs(&normalize(path)); + Ok(()) } pub fn open_backing(&mut self, path: &str) -> Result, SyscallError> { diff --git a/sdk/std/os/fs.rs b/sdk/std/os/fs.rs index ca2ee04247..744be69744 100644 --- a/sdk/std/os/fs.rs +++ b/sdk/std/os/fs.rs @@ -7,8 +7,7 @@ use crate::sys::AsInner; #[stable(feature = "toyos_ext", since = "1.0.0")] pub trait MetadataExt { /// The volume the file is on (`toyos_abi::syscall::Stat::volume`), as - /// Unix's `dev`: 0 for what has none, a directory or symlink the kernel - /// serves among them. + /// Unix's `dev`: 0 for what has none. #[stable(feature = "toyos_ext", since = "1.0.0")] fn dev(&self) -> u64; /// The file on that volume (`toyos_abi::syscall::Stat::node`), as Unix's diff --git a/sdk/std/sys/fs.rs b/sdk/std/sys/fs.rs index d95dcb8a7e..b4967e35a1 100644 --- a/sdk/std/sys/fs.rs +++ b/sdk/std/sys/fs.rs @@ -122,8 +122,7 @@ pub struct FileAttr { size: u64, file_type: FileType, mtime: u64, - /// `toyos_abi::syscall::Stat`'s identity; 0 for what the kernel numbers - /// not — its directories and symlinks. + /// `toyos_abi::syscall::Stat`'s identity. volume: u64, node: u64, } @@ -203,18 +202,13 @@ impl FileAttr { fn of_kernel(stat: syscall::Stat) -> FileAttr { FileAttr { size: stat.size, - file_type: opened_file_type(stat.file_type), + file_type: kernel_file_type(stat.file_type), mtime: stat.mtime, volume: stat.volume, node: stat.node, } } - /// What the kernel answers of a name it opens not: its kind and size alone. - fn unopened(size: u64, file_type: FileType) -> FileAttr { - FileAttr { size, file_type, mtime: 0, volume: 0, node: 0 } - } - pub fn volume(&self) -> u64 { self.volume } @@ -291,7 +285,7 @@ impl DirEntry { self.name.clone() } - /// Asked of the entry, as Unix's is: a listing carries no identity. + /// Asked of the entry, as Unix's is. pub fn metadata(&self) -> io::Result { lstat(&self.path()) } @@ -764,23 +758,15 @@ impl fmt::Debug for File { } } -/// Whether the kernel's `path` names a directory, asked through `readdir`: a -/// directory too large to list is still one, which is why -/// `ResourceExhausted` is a yes. -fn kernel_is_dir(path: &[u8]) -> bool { - let mut buf = [0u8; 1]; - match syscall::readdir(path, &mut buf) { - Ok(_) | Err(SyscallError::ResourceExhausted) => true, - Err(_) => false, +/// The type of something the kernel answered a `stat` or `fstat` of. +fn kernel_file_type(ty: syscall::FileType) -> FileType { + FileType { + is_file: ty == syscall::FileType::File, + is_dir: ty == syscall::FileType::Directory, + is_symlink: ty == syscall::FileType::Symlink, } } -/// The type of something the kernel's `open` accepted, which is never a -/// directory. -fn opened_file_type(ty: syscall::FileType) -> FileType { - FileType { is_file: ty == syscall::FileType::File, is_dir: false, is_symlink: false } -} - /// A listing too large for a first buffer is asked again at the size it /// named; a directory that outgrows its own listing this many times is /// refused rather than waited on. @@ -948,37 +934,16 @@ pub fn link(_src: &Path, _dst: &Path) -> io::Result<()> { Err(io::Error::new(io::ErrorKind::Unsupported, "no hard links on ToyOS")) } -fn kernel_stat(abs: &str) -> io::Result { - if let Ok(fd) = syscall::open(abs.as_bytes(), OpenFlags::READ) { - let result = syscall::fstat(fd); - syscall::close(fd); - return result.map(FileAttr::of_kernel).map_err(to_io_error); - } - if kernel_is_dir(abs.as_bytes()) { - return Ok(FileAttr::unopened(0, FileType { is_file: false, is_dir: true, is_symlink: false })); - } - Err(io::Error::new(io::ErrorKind::NotFound, "file not found")) +fn kernel_stat(abs: &str, follow: bool) -> io::Result { + syscall::stat(abs.as_bytes(), follow).map(FileAttr::of_kernel).map_err(to_io_error) } pub fn stat(path: &Path) -> io::Result { - on_path(path, kernel_stat, |_, dir, rel| dir.stat(rel, true).map(FileAttr::of)) + on_path(path, |abs| kernel_stat(abs, true), |_, dir, rel| dir.stat(rel, true).map(FileAttr::of)) } pub fn lstat(path: &Path) -> io::Result { - on_path( - path, - |abs| { - let mut link_buf = [0u8; 4096]; - if let Ok(n) = syscall::readlink(abs.as_bytes(), &mut link_buf) { - return Ok(FileAttr::unopened( - n as u64, - FileType { is_file: false, is_dir: false, is_symlink: true }, - )); - } - kernel_stat(abs) - }, - |_, dir, rel| dir.stat(rel, false).map(FileAttr::of), - ) + on_path(path, |abs| kernel_stat(abs, false), |_, dir, rel| dir.stat(rel, false).map(FileAttr::of)) } pub fn canonicalize(p: &Path) -> io::Result { diff --git a/tests/testcases/tinycc/210_stat_identity.c b/tests/testcases/tinycc/210_stat_identity.c index ca3c000aac..9899e9e364 100644 --- a/tests/testcases/tinycc/210_stat_identity.c +++ b/tests/testcases/tinycc/210_stat_identity.c @@ -1,8 +1,12 @@ /* `st_dev` and `st_ino` as the C library fills them: one file is one pair through every path, a link and its open descriptor, and across a rename; - two files are two; and the two kernel mounts are two volumes. */ + two files are two; and the two kernel mounts are two volumes. A kernel + directory and symlink are each one as well, and `d_ino` is its entry's + `st_ino`. */ +#include #include #include +#include #include #include @@ -10,8 +14,8 @@ static int same(const struct stat *a, const struct stat *b) { return a->st_dev == b->st_dev && a->st_ino == b->st_ino; } -static int stat_of(const char *path, struct stat *st) { - if (stat(path, st) != 0) { +static int identity(const char *path, struct stat *st, int follow) { + if ((follow ? stat(path, st) : lstat(path, st)) != 0) { printf("stat %s failed\n", path); return 1; } @@ -22,6 +26,27 @@ static int stat_of(const char *path, struct stat *st) { return 0; } +static int stat_of(const char *path, struct stat *st) { + return identity(path, st, 1); +} + +static int lstat_of(const char *path, struct stat *st) { + return identity(path, st, 0); +} + +/* The `d_ino` `dir`'s listing answers for `name`, 0 when it lists none. */ +static ino_t listed(const char *dir, const char *name) { + DIR *d = opendir(dir); + struct dirent *e; + ino_t ino = 0; + while (d && (e = readdir(d))) + if (strcmp(e->d_name, name) == 0) + ino = e->d_ino; + if (d) + closedir(d); + return ino; +} + static int write_file(const char *path) { int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0 || write(fd, "x", 1) != 1) { @@ -38,6 +63,7 @@ static void judge(const char *what, int is, int want) { int main(void) { struct stat a, b, a2, opened, moved, toybox, cat, supervisor; + struct stat bin, bin2, etc, sys, tmpdir, catlink, cplink; int fd; if (write_file("/tmp/210_a") || write_file("/tmp/210_b")) @@ -70,6 +96,27 @@ int main(void) { judge("two files on /system are two", same(&toybox, &supervisor), 0); judge("/system and /tmp are two volumes", toybox.st_dev == a.st_dev, 0); + if (mkdir("/tmp/210_d", 0755) != 0 || stat_of("/system/bin", &bin) || stat_of("/system/etc/../bin", &bin2) || + stat_of("/system/etc", &etc) || stat_of("/system", &sys) || stat_of("/tmp/210_d", &tmpdir)) + return 1; + judge("two directories on /system are two", same(&bin, &etc), 0); + judge("a directory and its mount's root are two", same(&bin, &sys), 0); + judge("one directory through two paths is one", same(&bin, &bin2), 1); + judge("a directory is on its mount's volume", bin.st_dev == toybox.st_dev && tmpdir.st_dev == a.st_dev, 1); + judge("a directory is no file", same(&tmpdir, &b), 0); + + if (lstat_of("/system/bin/cat", &catlink) || lstat_of("/system/bin/cp", &cplink)) + return 1; + judge("two links on /system are two", same(&catlink, &cplink), 0); + judge("a link is not the file it names", same(&catlink, &toybox), 0); + + judge("d_ino is a directory's st_ino", listed("/system", "bin") == bin.st_ino, 1); + judge("d_ino is a file's st_ino", listed("/system/bin", "toybox") == toybox.st_ino, 1); + judge("d_ino is a link's st_ino", listed("/system/bin", "cat") == catlink.st_ino, 1); + judge("d_ino is a mount's st_ino", listed("/", "system") == sys.st_ino, 1); + judge("d_ino on /tmp is st_ino", listed("/tmp", "210_b") == b.st_ino && listed("/tmp", "210_d") == tmpdir.st_ino, 1); + + rmdir("/tmp/210_d"); unlink("/tmp/210_moved"); unlink("/tmp/210_b"); return 0; diff --git a/tests/testcases/tinycc/210_stat_identity.expect b/tests/testcases/tinycc/210_stat_identity.expect index a75f1604ad..fc312afc02 100644 --- a/tests/testcases/tinycc/210_stat_identity.expect +++ b/tests/testcases/tinycc/210_stat_identity.expect @@ -6,3 +6,15 @@ its descriptor after the rename is the file: yes a link is the file it names: yes two files on /system are two: yes /system and /tmp are two volumes: yes +two directories on /system are two: yes +a directory and its mount's root are two: yes +one directory through two paths is one: yes +a directory is on its mount's volume: yes +a directory is no file: yes +two links on /system are two: yes +a link is not the file it names: yes +d_ino is a directory's st_ino: yes +d_ino is a file's st_ino: yes +d_ino is a link's st_ino: yes +d_ino is a mount's st_ino: yes +d_ino on /tmp is st_ino: yes diff --git a/tests/toyos-rust-tests/src/bin/empty_dir_stat.rs b/tests/toyos-rust-tests/src/bin/empty_dir_stat.rs index 289c160173..8a7c62ee4d 100644 --- a/tests/toyos-rust-tests/src/bin/empty_dir_stat.rs +++ b/tests/toyos-rust-tests/src/bin/empty_dir_stat.rs @@ -29,7 +29,7 @@ const WITH_FILE: &str = "/tmp/empty_dir_stat_full"; fn readdir(path: &str) -> Result { // One byte on purpose: the kernel reports the size the listing *needs* // whether or not it fits, so the answer is the return value and the bytes - // are not wanted. It is also what `std`'s `is_dir` passes. + // are not wanted. let mut buf = [0u8; 1]; syscall::readdir(path.as_bytes(), &mut buf) } diff --git a/tests/toyos-rust-tests/src/bin/stat_identity.rs b/tests/toyos-rust-tests/src/bin/stat_identity.rs index 544ebbde8e..072c7e6662 100644 --- a/tests/toyos-rust-tests/src/bin/stat_identity.rs +++ b/tests/toyos-rust-tests/src/bin/stat_identity.rs @@ -1,8 +1,11 @@ //! A file's identity through std (`std::os::toyos::fs::MetadataExt`): on a //! kernel mount (`/tmp`), on DATA (`/home`) and on FAT (`/log`), one file is //! one `(dev, ino)` through every path, a symlink, its open handle, its -//! directory's listing and a rename; two files are two; and the three are -//! three volumes, each served one with `toyos::fs::SERVED_VOLUME` set. +//! directory's listing and a rename; two files are two, as are two +//! directories; a symlink is one of its own, across a rename; a kernel +//! directory that ends gives its number up, and one that stands keeps it; and +//! the three are three volumes, each served one with +//! `toyos::fs::SERVED_VOLUME` set. use std::fs::{self, File, Metadata}; use std::os::toyos::fs::{symlink, MetadataExt}; @@ -44,7 +47,12 @@ fn judge(dir: &str, spelling: &str, links: bool) -> u64 { let link = format!("{dir}/stat-identity-link"); symlink(&a, &link).unwrap_or_else(|e| panic!("symlink {link}: {e}")); assert_eq!(of(&link), ida, "on {dir} a symlink to {a} is another file"); - fs::remove_file(&link).unwrap(); + let own = id(&fs::symlink_metadata(&link).unwrap()); + assert!(own.0 == ida.0 && own.1 != 0 && own.1 != ida.1, "on {dir} the link itself is {own:?}"); + let renamed = format!("{dir}/stat-identity-link2"); + fs::rename(&link, &renamed).unwrap_or_else(|e| panic!("rename {link}: {e}")); + assert_eq!(id(&fs::symlink_metadata(&renamed).unwrap()), own, "on {dir} a rename made another link"); + fs::remove_file(&renamed).unwrap(); } let moved = format!("{dir}/stat-identity-moved"); @@ -55,9 +63,7 @@ fn judge(dir: &str, spelling: &str, links: bool) -> u64 { let (d1, d2) = (format!("{dir}/stat-identity-d1"), format!("{dir}/stat-identity-d2")); fs::create_dir(&d1).unwrap(); fs::create_dir(&d2).unwrap(); - if ida.0 & toyos::fs::SERVED_VOLUME != 0 { - assert_ne!(of(&d1), of(&d2), "on {dir} two directories are one"); - } + assert_ne!(of(&d1), of(&d2), "on {dir} two directories are one"); for d in [&d1, &d2] { fs::remove_dir(d).unwrap(); } @@ -68,7 +74,66 @@ fn judge(dir: &str, spelling: &str, links: bool) -> u64 { ida.0 } +/// On a kernel mount a directory that ends gives its number up, so the one +/// made there next is another: by `rmdir`, and by the unlink or the rename of +/// the file that alone made it one, but not by that file's truncating open. +fn an_ended_directory_is_forgotten() { + const DIR: &str = "/tmp/stat-identity-ended"; + const FILE: &str = "/tmp/stat-identity-ended/f"; + const AWAY: &str = "/tmp/stat-identity-away"; + let again = |end: &str, make: &dyn Fn(), ends: &dyn Fn()| { + make(); + let before = of(DIR); + ends(); + make(); + assert_ne!(of(DIR), before, "{end} left {DIR}'s number to the directory made there next"); + ends(); + }; + let mkdir = || fs::create_dir(DIR).unwrap(); + let file = || write(FILE); + again("rmdir", &mkdir, &|| fs::remove_dir(DIR).unwrap()); + again("an unlink", &file, &|| fs::remove_file(FILE).unwrap()); + again("a rename", &file, &|| { + fs::rename(FILE, AWAY).unwrap(); + fs::remove_file(AWAY).unwrap(); + }); + + // A truncating open puts its file back at once: the directory never ended. + file(); + let kept = of(DIR); + file(); + assert_eq!(of(DIR), kept, "a truncating open of its one file renumbered {DIR}"); + fs::remove_file(FILE).unwrap(); +} + +/// On a kernel mount a directory that still stands after a removal beneath it +/// keeps its number, as `/tmp` above it does: one a second file still makes a +/// directory, and one `mkdir` made whose only file goes. +fn a_standing_directory_keeps_its_number() { + const DIR: &str = "/tmp/stat-identity-stands"; + const ONE: &str = "/tmp/stat-identity-stands/one"; + const TWO: &str = "/tmp/stat-identity-stands/two"; + let tmp = of("/tmp"); + + write(ONE); + write(TWO); + let before = of(DIR); + fs::remove_file(ONE).unwrap(); + assert_eq!(of(DIR), before, "an unlink beside a second file renumbered {DIR}"); + assert_eq!(of("/tmp"), tmp, "an unlink beneath it renumbered /tmp"); + fs::remove_file(TWO).unwrap(); + + fs::create_dir(DIR).unwrap(); + write(ONE); + let before = of(DIR); + fs::remove_file(ONE).unwrap(); + assert_eq!(of(DIR), before, "an unlink of its only file renumbered the made {DIR}"); + fs::remove_dir(DIR).unwrap(); +} + fn main() { + an_ended_directory_is_forgotten(); + a_standing_directory_keeps_its_number(); let tmp = judge("/tmp", "/tmp/../tmp/./stat-identity-a", true); let home = judge("/home", "/home/./stat-identity-a", true); // FAT names one entry by either case, and has no symlinks. diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index 7c356c3185..0815666718 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -8,6 +8,8 @@ pub const SYS_CLOSE: u64 = 10; pub const SYS_SEEK: u64 = 13; pub const SYS_FSTAT: u64 = 14; pub const SYS_FSYNC: u64 = 15; +/// What a path names, as a [`Stat`]: [`stat`]. +pub const SYS_STAT: u64 = 16; pub const SYS_READDIR: u64 = 17; pub const SYS_DELETE: u64 = 18; pub const SYS_SHUTDOWN: u64 = 19; @@ -555,6 +557,10 @@ pub enum FileType { /// A shared-memory region; `size` is its length. A program's stdout and /// stderr are one when they are its log ring (`toyos::log`). SharedMemory = 10, + /// A directory, which only [`stat`] answers. + Directory = 11, + /// A symbolic link, which only [`stat`] answers; `size` is its target's length. + Symlink = 12, } impl FileType { @@ -570,6 +576,8 @@ impl FileType { 7 => Some(Self::Mouse), 8 => Some(Self::Socket), 10 => Some(Self::SharedMemory), + 11 => Some(Self::Directory), + 12 => Some(Self::Symlink), _ => None, } } @@ -660,7 +668,7 @@ pub struct RealTime { pub seconds: u8, } -/// File metadata returned by [`fstat`]. +/// File metadata returned by [`fstat`] and [`stat`]. #[repr(C)] #[derive(Clone, Copy, Debug, Default)] pub struct Stat { @@ -1069,6 +1077,15 @@ pub fn fstat(handle: RawHandle) -> Result { Ok(stat) } +/// What the kernel's `path` names: a file, a directory, or, unless `follow` +/// follows it, a symlink, each with the identity [`fstat`] answers a file. +pub fn stat(path: &[u8], follow: bool) -> Result { + let mut stat = Stat::default(); + let out = &mut stat as *mut Stat as u64; + check_unit(syscall(SYS_STAT, path.as_ptr() as u64, path.len() as u64, out, follow as u64))?; + Ok(stat) +} + /// Flush a file handle to disk, or a partition claim's writes out of its /// device's cache ([`crate::part`]): `Ok` is the device's own word that every /// write the claim returned from before this call is durable. @@ -1099,11 +1116,11 @@ pub const DIRENT_FILE: u8 = 1; pub const DIRENT_DIR: u8 = 2; /// One entry of a [`readdir`] listing: its kind byte, its name, a NUL, and its -/// size as eight bytes, little-endian. +/// node as eight bytes, little-endian: the [`Stat::node`] [`stat`] answers it. pub struct Dirent<'a> { pub is_dir: bool, pub name: &'a [u8], - pub size: u64, + pub node: u64, } /// The entry at `*at` in a whole [`readdir`] listing, with `*at` moved past @@ -1117,11 +1134,11 @@ pub fn dirent<'a>(listing: &'a [u8], at: &mut usize) -> Option> { kind => panic!("SYS_READDIR answered an entry of kind {kind}"), }; let len = rest[1..].iter().position(|&b| b == 0).expect("SYS_READDIR answered a name with no NUL"); - let size = rest + let node = rest .get(len + 2..len + 10) - .expect("SYS_READDIR answered an entry with no size"); + .expect("SYS_READDIR answered an entry with no node"); *at += len + 10; - Some(Dirent { is_dir, name: &rest[1..1 + len], size: u64::from_le_bytes(size.try_into().unwrap()) }) + Some(Dirent { is_dir, name: &rest[1..1 + len], node: u64::from_le_bytes(node.try_into().unwrap()) }) } /// Delete a file or directory. @@ -2873,14 +2890,14 @@ mod tests { } /// A listing of `entries`, each a name, whether it is a directory and a - /// size, encoded as `sys_readdir` (`kernel/src/syscall/fs.rs`) encodes it. + /// node, encoded as `sys_readdir` (`kernel/src/syscall/fs.rs`) encodes it. fn listing(entries: &[(&str, bool, u64)]) -> std::vec::Vec { let mut out = std::vec::Vec::new(); - for (name, is_dir, size) in entries { + for (name, is_dir, node) in entries { out.push(if *is_dir { 2 } else { 1 }); out.extend_from_slice(name.as_bytes()); out.push(0); - out.extend_from_slice(&size.to_le_bytes()); + out.extend_from_slice(&node.to_le_bytes()); } out } @@ -2889,15 +2906,15 @@ mod tests { let mut at = 0; let mut out = std::vec::Vec::new(); while let Some(entry) = dirent(bytes, &mut at) { - out.push((std::string::String::from_utf8(entry.name.to_vec()).unwrap(), entry.is_dir, entry.size)); + out.push((std::string::String::from_utf8(entry.name.to_vec()).unwrap(), entry.is_dir, entry.node)); } assert_eq!(at, bytes.len(), "the reader stopped short of the listing's end"); out } #[test] - fn every_dirent_is_read_back_whatever_its_size_holds() { - // Sizes whose bytes are NULs, kind bytes and letters: a reader that + fn every_dirent_is_read_back_whatever_its_node_holds() { + // Nodes whose bytes are NULs, kind bytes and letters: a reader that // does not step over all eight takes them for names. let entries = [ ("a", false, 0), @@ -2918,7 +2935,7 @@ mod tests { } #[test] - #[should_panic(expected = "no size")] + #[should_panic(expected = "no node")] fn a_short_dirent_is_a_broken_kernel() { read_listing(&[1, b'x', 0, 0, 0]); } diff --git a/userland/libc/src/posix_io.rs b/userland/libc/src/posix_io.rs index 764cad305b..a8099f2091 100644 --- a/userland/libc/src/posix_io.rs +++ b/userland/libc/src/posix_io.rs @@ -133,31 +133,38 @@ pub unsafe extern "C" fn lseek(raw_fd: i32, offset: i64, whence: i32) -> i64 { #[no_mangle] pub unsafe extern "C" fn fstat(raw_fd: i32, buf: *mut Stat) -> i32 { - match syscall::fstat(fd(raw_fd)) { - Ok(st) => { - if !buf.is_null() { - ptr::write_bytes(buf, 0, 1); - let s = &mut *buf; - s.st_dev = st.volume; - s.st_ino = st.node; - s.st_size = st.size as i64; - s.st_mtim = Timespec { - tv_sec: (st.mtime / NANOS_PER_SEC) as i64, - tv_nsec: (st.mtime % NANOS_PER_SEC) as i64, - }; - s.st_mode = match st.file_type { - syscall::FileType::File => S_IFREG | 0o644, - syscall::FileType::Pipe => S_IFIFO | 0o644, - syscall::FileType::Tty => S_IFCHR | 0o644, - syscall::FileType::Keyboard => S_IFCHR | 0o444, - syscall::FileType::Serial => S_IFCHR | 0o644, - _ => 0o644, - }; - } - 0 - } - Err(e) => set_errno(e), + unsafe { answered(syscall::fstat(fd(raw_fd)), buf) } +} + +/// `buf` filled from the kernel's `answer`, or `errno` from its refusal. +unsafe fn answered(answer: Result, buf: *mut Stat) -> i32 { + let st = match answer { + Ok(st) => st, + Err(e) => return set_errno(e), + }; + if !buf.is_null() { + // SAFETY: C's caller hands one `struct stat` at `buf`. + unsafe { ptr::write_bytes(buf, 0, 1) }; + let s = unsafe { &mut *buf }; + s.st_dev = st.volume; + s.st_ino = st.node; + s.st_size = st.size as i64; + s.st_mtim = Timespec { + tv_sec: (st.mtime / NANOS_PER_SEC) as i64, + tv_nsec: (st.mtime % NANOS_PER_SEC) as i64, + }; + s.st_mode = match st.file_type { + syscall::FileType::File => S_IFREG | 0o644, + syscall::FileType::Directory => S_IFDIR | 0o755, + syscall::FileType::Symlink => S_IFLNK | 0o777, + syscall::FileType::Pipe => S_IFIFO | 0o644, + syscall::FileType::Tty => S_IFCHR | 0o644, + syscall::FileType::Keyboard => S_IFCHR | 0o444, + syscall::FileType::Serial => S_IFCHR | 0o644, + _ => 0o644, + }; } + 0 } #[no_mangle] @@ -288,56 +295,15 @@ pub unsafe extern "C" fn chdir(path: *const u8) -> i32 { } } -/// Whether `path` names a directory, asked through `readdir`. -/// -/// `open` refuses every directory, so this is the only way to tell. The buffer -/// is deliberately tiny — only whether the kernel accepted the path matters, -/// and a listing that does not fit is reported rather than written, so a -/// directory too large to list is still a yes. -unsafe fn is_dir(path_bytes: &[u8]) -> bool { - let mut probe = [0u8; 1]; - matches!( - syscall::readdir(path_bytes, &mut probe), - Ok(_) | Err(syscall::SyscallError::ResourceExhausted) - ) -} - -/// Fill `buf` with what a directory looks like to `stat`: a mode and nothing -/// else, because `readdir` answers existence and the kernel keeps no size or -/// mtime for one. -unsafe fn stat_a_directory(buf: *mut Stat) -> i32 { - if !buf.is_null() { - ptr::write_bytes(buf, 0, 1); - (*buf).st_mode = S_IFDIR | 0o755; - } - 0 -} - -// stat by path: open + fstat + close, or readdir for what open refuses #[no_mangle] pub unsafe extern "C" fn stat(path: *const u8, buf: *mut Stat) -> i32 { - stat_impl(path, buf) + unsafe { answered(syscall::stat(c_str_to_bytes(path), true), buf) } } /// `stat` that does not follow a final symbolic link. -/// -/// `SYS_READLINK` is what distinguishes one, and it is asked first: it succeeds -/// on a link and on nothing else, so a success is both the answer and the -/// target length `st_size` reports. Everything else is [`stat`]. #[no_mangle] pub unsafe extern "C" fn lstat(path: *const u8, buf: *mut Stat) -> i32 { - let path_bytes = c_str_to_bytes(path); - let mut target = [0u8; 4096]; - if let Ok(n) = syscall::readlink(path_bytes, &mut target) { - if !buf.is_null() { - ptr::write_bytes(buf, 0, 1); - let s = &mut *buf; - s.st_mode = S_IFLNK | 0o777; - s.st_size = n as i64; - } - return 0; - } - stat_impl(path, buf) + unsafe { answered(syscall::stat(c_str_to_bytes(path), false), buf) } } /// `SYS_READLINK` answers `NotFound` for a path that is no link as for one @@ -353,8 +319,8 @@ pub unsafe extern "C" fn readlink(path: *const u8, buf: *mut u8, size: usize) -> match syscall::readlink(c_str_to_bytes(path), target) { Ok(n) => n as isize, Err(syscall::SyscallError::NotFound) => { - // `stat_impl` sets `errno` for a path that names nothing. - if unsafe { stat_impl(path, ptr::null_mut()) } == 0 { + // `stat` sets `errno` for a path that names nothing. + if unsafe { stat(path, ptr::null_mut()) } == 0 { crate::errno::set(EINVAL); } -1 @@ -375,8 +341,7 @@ impl crate::resolve::Kernel for TheKernel { } fn is_dir(&mut self, at: &[u8]) -> bool { - // SAFETY: `is_dir` reads only `at`. - unsafe { is_dir(at) } + syscall::stat(at, true).is_ok_and(|st| st.file_type == syscall::FileType::Directory) } } @@ -412,29 +377,10 @@ pub unsafe extern "C" fn realpath(path: *const u8, resolved: *mut u8) -> *mut u8 out } -unsafe fn stat_impl(path: *const u8, buf: *mut Stat) -> i32 { - let path_bytes = c_str_to_bytes(path); - // Try opening read-only - match syscall::open(path_bytes, OpenFlags::READ) { - Ok(f) => { - let result = fstat(f.0 as i32, buf); - syscall::close(f); - result - } - Err(_) if is_dir(path_bytes) => stat_a_directory(buf), - Err(e) => set_errno(e), - } -} - +/// ToyOS has no permissions model: whether `path` names anything. #[no_mangle] pub unsafe extern "C" fn access(path: *const u8, _mode: i32) -> i32 { - // ToyOS has no permissions model; just check existence - let path_bytes = c_str_to_bytes(path); - match syscall::open(path_bytes, OpenFlags::READ) { - Ok(f) => { syscall::close(f); 0 } - Err(_) if is_dir(path_bytes) => 0, - Err(e) => set_errno(e), - } + unsafe { stat(path, ptr::null_mut()) } } #[no_mangle] @@ -519,7 +465,6 @@ pub unsafe extern "C" fn opendir(path: *const u8) -> *mut DIR { pub unsafe extern "C" fn readdir(dir: *mut DIR) -> *mut dirent { if dir.is_null() { return ptr::null_mut(); } let d = &mut *dir; - let start = d.at; let Some(entry) = syscall::dirent(&d.listing, &mut d.at) else { return ptr::null_mut() }; let Some(d_name) = crate::listing::d_name(entry.name) else { crate::errno::set(crate::errcode::EOVERFLOW); @@ -529,7 +474,7 @@ pub unsafe extern "C" fn readdir(dir: *mut DIR) -> *mut dirent { // Use a static buffer for the dirent (not thread-safe, matching POSIX convention) static mut DIRENT_BUF: dirent = dirent { d_ino: 0, d_type: 0, d_name: [0; crate::listing::D_NAME] }; let ent = &raw mut DIRENT_BUF; - ent.write(dirent { d_ino: (start + 1) as u64, d_type: if entry.is_dir { DT_DIR } else { DT_UNKNOWN }, d_name }); + ent.write(dirent { d_ino: entry.node, d_type: if entry.is_dir { DT_DIR } else { DT_UNKNOWN }, d_name }); ent }