From ed5af16f898e435f06658f8f9438d9cb435cd144 Mon Sep 17 00:00:00 2001 From: weitengchen Date: Sat, 28 Feb 2026 15:13:18 -0800 Subject: [PATCH 1/2] support chdir --- litebox_common_linux/src/lib.rs | 4 + litebox_shim_linux/src/lib.rs | 3 + litebox_shim_linux/src/syscalls/file.rs | 336 +++++++++++++++++++++--- 3 files changed, 311 insertions(+), 32 deletions(-) diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index 9108995d3..34ba9fa3c 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -1872,6 +1872,9 @@ pub enum SyscallRequest { pathname: Platform::RawConstPointer, mode: u32, }, + Chdir { + pathname: Platform::RawConstPointer, + }, Mmap { addr: usize, length: usize, @@ -2360,6 +2363,7 @@ impl SyscallRequest { Sysno::fstat => sys_req!(Fstat { fd, buf:* }), Sysno::lstat => sys_req!(Lstat { pathname:*, buf:* }), Sysno::mkdir => sys_req!(Mkdir { pathname:*, mode }), + Sysno::chdir => sys_req!(Chdir { pathname:* }), #[cfg(target_arch = "x86_64")] Sysno::mmap => sys_req!(Mmap { addr, diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index eed9d6159..a07dbc256 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -713,6 +713,9 @@ impl Task { SyscallRequest::Mkdir { pathname, mode } => pathname .to_cstring() .map_or(Err(Errno::EINVAL), |path| syscall!(sys_mkdir(path, mode))), + SyscallRequest::Chdir { pathname } => pathname + .to_cstring() + .map_or(Err(Errno::EINVAL), |path| syscall!(sys_chdir(path))), SyscallRequest::RtSigprocmask { how, set, diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 7e05ed22b..2e3b8f871 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -28,12 +28,17 @@ use core::sync::atomic::Ordering; /// Task state shared by `CLONE_FS`. pub(crate) struct FsState { umask: core::sync::atomic::AtomicU32, + /// The current working directory + /// + /// Must end with a '/'. + cwd: litebox::sync::RwLock, } impl Clone for FsState { fn clone(&self) -> Self { Self { umask: self.umask.load(Ordering::Relaxed).into(), + cwd: litebox::sync::RwLock::new(self.cwd.read().clone()), } } } @@ -42,6 +47,7 @@ impl FsState { pub fn new() -> Self { Self { umask: (Mode::WGRP | Mode::WOTH).bits().into(), + cwd: litebox::sync::RwLock::new(String::from("/")), } } @@ -68,16 +74,15 @@ impl FilesState { } /// Path in the file system -enum FsPath { +#[derive(Debug)] +enum FsPath { /// Absolute path - Absolute { path: P }, - /// Path is relative to `cwd` - CwdRelative { path: P }, + Absolute { path: CString }, /// Current working directory Cwd, /// Path is relative to a file descriptor #[expect(dead_code, reason = "currently unused, might want to use later")] - FdRelative { fd: u32, path: P }, + FdRelative { fd: u32, path: CString }, /// Fd Fd(u32), } @@ -85,26 +90,42 @@ enum FsPath { /// Maximum size of a file path pub const PATH_MAX: usize = 4096; -impl FsPath

{ - fn new(dirfd: i32, path: P) -> Result { +impl FsPath { + /// Create a new `FsPath` from a dirfd and path. + /// + /// CWD-relative paths are resolved immediately to absolute paths. + fn new( + dirfd: i32, + path: impl path::Arg, + get_cwd: impl FnOnce() -> String, + ) -> Result { let path_str = path.as_rust_str()?; if path_str.len() > PATH_MAX { return Err(Errno::ENAMETOOLONG); } let fs_path = if path_str.starts_with('/') { - FsPath::Absolute { path } + let cpath = path.to_c_str()?.into_owned(); + FsPath::Absolute { path: cpath } } else if dirfd >= 0 { let dirfd = u32::try_from(dirfd).expect("dirfd >= 0"); if path_str.is_empty() { FsPath::Fd(dirfd) } else { - FsPath::FdRelative { fd: dirfd, path } + let cpath = path.to_c_str()?.into_owned(); + FsPath::FdRelative { + fd: dirfd, + path: cpath, + } } } else if dirfd == litebox_common_linux::AT_FDCWD { if path_str.is_empty() { FsPath::Cwd } else { - FsPath::CwdRelative { path } + // Resolve CWD-relative path to absolute. + let mut abs = get_cwd(); + abs.push_str(path_str); + let cpath = CString::new(abs).map_err(|_| Errno::EINVAL)?; + FsPath::Absolute { path: cpath } } } else { return Err(Errno::EBADF); @@ -118,6 +139,18 @@ impl Task { self.fs.borrow().umask() } + /// Resolve a path against the current working directory. + fn resolve_path(&self, path: impl path::Arg) -> Result { + let path_str = path.as_rust_str().map_err(|_| Errno::EINVAL)?; + if path_str.starts_with('/') { + CString::new(path_str.to_string()).map_err(|_| Errno::EINVAL) + } else { + let mut cwd = self.fs.borrow().cwd.read().clone(); + cwd.push_str(path_str); + CString::new(cwd).map_err(|_| Errno::EINVAL) + } + } + /// Handle syscall `umask` pub(crate) fn sys_umask(&self, new_mask: u32) -> Mode { let new_mask = Mode::from_bits_truncate(new_mask) & (Mode::RWXU | Mode::RWXG | Mode::RWXO); @@ -131,6 +164,7 @@ impl Task { /// Handle syscall `open` pub fn sys_open(&self, path: impl path::Arg, flags: OFlags, mode: Mode) -> Result { + let path = self.resolve_path(path)?; let mode = mode & !self.get_umask(); let file = self.global.fs.open(path, flags - OFlags::CLOEXEC, mode)?; if flags.contains(OFlags::CLOEXEC) { @@ -160,12 +194,11 @@ impl Task { flags: OFlags, mode: Mode, ) -> Result { - let fs_path = FsPath::new(dirfd, pathname)?; + let get_cwd = || self.fs.borrow().cwd.read().clone(); + let fs_path = FsPath::new(dirfd, pathname, get_cwd)?; match fs_path { - FsPath::Absolute { path } | FsPath::CwdRelative { path } => { - self.sys_open(path, flags, mode) - } - FsPath::Cwd => self.sys_open("", flags, mode), + FsPath::Absolute { path } => self.sys_open(path, flags, mode), + FsPath::Cwd => self.sys_open(get_cwd(), flags, mode), FsPath::Fd(_fd) => { log_unsupported!("openat with FsPath::Fd"); Err(Errno::EINVAL) @@ -213,9 +246,9 @@ impl Task { return Err(Errno::EINVAL); } - let fs_path = FsPath::new(dirfd, pathname)?; + let fs_path = FsPath::new(dirfd, pathname, || self.fs.borrow().cwd.read().clone())?; match fs_path { - FsPath::Absolute { path } | FsPath::CwdRelative { path } => { + FsPath::Absolute { path } => { if flags.contains(AtFlags::AT_REMOVEDIR) { self.global.fs.rmdir(path).map_err(Errno::from) } else { @@ -402,6 +435,7 @@ impl Task { /// Handle syscall `mkdir` pub fn sys_mkdir(&self, pathname: impl path::Arg, mode: u32) -> Result<(), Errno> { + let pathname = self.resolve_path(pathname)?; let mode = Mode::from_bits_retain(mode) & !self.get_umask(); self.global.fs.mkdir(pathname, mode).map_err(Errno::from) } @@ -615,6 +649,7 @@ impl Task { pathname: impl path::Arg, mode: litebox_common_linux::AccessFlags, ) -> Result<(), Errno> { + let pathname = self.resolve_path(pathname)?; let status = self.global.fs.file_status(pathname)?; if mode == litebox_common_linux::AccessFlags::F_OK { return Ok(()); @@ -641,10 +676,11 @@ impl Task { /// Read the target of a symbolic link /// + /// The caller must pass an absolute path. + /// /// Note that this function only handles the following cases that we hardcoded: /// - `/proc/self/fd/` fn do_readlink(&self, fullpath: &str) -> Result { - // It assumes that the path is absolute. Will fix once #71 is done. if let Some(stripped) = fullpath.strip_prefix("/proc/self/fd/") { let fd = stripped.parse::().map_err(|_| Errno::EINVAL)?; match fd { @@ -671,15 +707,14 @@ impl Task { pathname: impl path::Arg, buf: &mut [u8], ) -> Result { - let fspath = FsPath::new(dirfd, pathname)?; + let fspath = FsPath::new(dirfd, pathname, || self.fs.borrow().cwd.read().clone())?; let path = match fspath { - FsPath::Absolute { path } => self.do_readlink(path.normalized()?.as_str()), - // Note we don't support changing cwd yet; cwd is always `/`. - FsPath::Cwd => self.do_readlink("/"), - FsPath::CwdRelative { path } => { - let normalized_path = path.normalized()?; - let full_path = alloc::format!("/{}", normalized_path.as_str()); - self.do_readlink(&full_path) + FsPath::Absolute { path } => { + self.do_readlink(path.to_str().map_err(|_| Errno::EINVAL)?) + } + FsPath::Cwd => { + let cwd = self.fs.borrow().cwd.read().clone(); + self.do_readlink(&cwd) } FsPath::Fd(_) | FsPath::FdRelative { .. } => unimplemented!(), }?; @@ -869,10 +904,12 @@ impl Descriptor { } impl Task { + /// Get the file status of `pathname`. + /// + /// The `pathname` must be absolute. fn do_stat(&self, pathname: impl path::Arg, follow_symlink: bool) -> Result { let normalized_path = pathname.normalized()?; let path = if follow_symlink { - // TODO: `do_readlink` assumes the path is absolute self.do_readlink(normalized_path.as_str()) .unwrap_or(normalized_path) } else { @@ -884,6 +921,7 @@ impl Task { /// Handle syscall `stat` pub fn sys_stat(&self, pathname: impl path::Arg) -> Result { + let pathname = self.resolve_path(pathname)?; self.do_stat(pathname, true) } @@ -893,6 +931,7 @@ impl Task { /// then it returns information about the link itself, not the file that the link refers to. /// TODO: we do not support symbolic links yet. pub fn sys_lstat(&self, pathname: impl path::Arg) -> Result { + let pathname = self.resolve_path(pathname)?; self.do_stat(pathname, false) } @@ -923,12 +962,13 @@ impl Task { } let files = self.files.borrow(); - let fs_path = FsPath::new(dirfd, pathname)?; + let get_cwd = || self.fs.borrow().cwd.read().clone(); + let fs_path = FsPath::new(dirfd, pathname, get_cwd)?; let fstat: FileStat = match fs_path { - FsPath::Absolute { path } | FsPath::CwdRelative { path } => { + FsPath::Absolute { path } => { self.do_stat(path, !flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW))? } - FsPath::Cwd => self.global.fs.file_status("")?.into(), + FsPath::Cwd => self.global.fs.file_status(get_cwd())?.into(), FsPath::Fd(fd) => files .file_descriptors .read() @@ -1164,8 +1204,7 @@ impl Task { /// Handle syscall `getcwd` pub fn sys_getcwd(&self, buf: &mut [u8]) -> Result { - // TODO: use a fixed path for now - let cwd = "/"; + let cwd = self.fs.borrow().cwd.read().clone(); // need to account for the null terminator if cwd.len() >= buf.len() { return Err(Errno::ERANGE); @@ -1178,6 +1217,44 @@ impl Task { buf[..bytes.len()].copy_from_slice(bytes); Ok(bytes.len()) } + + /// Handle syscall `chdir` + pub fn sys_chdir(&self, pathname: impl path::Arg) -> Result<(), Errno> { + use litebox::fs::FileType; + use litebox::fs::errors::{FileStatusError, PathError}; + use litebox::path::Arg as _; + + // Resolve relative paths against CWD, then normalize (handle `.` / `..`). + let resolved = self.resolve_path(pathname)?; + let abs_path = resolved.normalized().map_err(|_| Errno::EINVAL)?; + + // Verify the path exists and is a directory. + match self.global.fs.file_status(abs_path.as_str()) { + Ok(status) => { + if status.file_type != FileType::Directory { + return Err(Errno::ENOTDIR); + } + } + Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) => { + return Err(Errno::ENOENT); + } + Err(FileStatusError::PathError(_)) => { + return Err(Errno::EACCES); + } + Err(_) => { + return Err(Errno::ENOENT); + } + } + + // Ensure the CWD ends with '/'. + let mut new_cwd = abs_path; + if !new_cwd.ends_with('/') { + new_cwd.push('/'); + } + + *self.fs.borrow().cwd.write() = new_cwd; + Ok(()) + } } const DEFAULT_PIPE_BUF_SIZE: usize = 1024 * 1024; @@ -1922,3 +1999,198 @@ impl Task { )? } } + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::String; + use litebox::fs::Mode; + + extern crate std; + + #[test] + fn fspath_new() { + // Absolute paths should never invoke the get_cwd closure. + let fp = FsPath::new(litebox_common_linux::AT_FDCWD, "/usr/bin", || { + panic!("get_cwd should not be called for absolute paths") + }) + .unwrap(); + assert!(matches!(fp, FsPath::Absolute { path } if path.to_str().unwrap() == "/usr/bin")); + + // Relative path resolves against CWD. + let fp = FsPath::new(litebox_common_linux::AT_FDCWD, "foo/bar", || { + String::from("/home/") + }) + .unwrap(); + assert!( + matches!(fp, FsPath::Absolute { path } if path.to_str().unwrap() == "/home/foo/bar") + ); + + // Empty path at AT_FDCWD → Cwd variant. + let fp = FsPath::new(litebox_common_linux::AT_FDCWD, "", || { + panic!("get_cwd should not be called for empty Cwd path") + }) + .unwrap(); + assert!(matches!(fp, FsPath::Cwd)); + + // Positive fd + empty path → Fd variant. + let fp = FsPath::new(5, "", || panic!("should not be called")).unwrap(); + assert!(matches!(fp, FsPath::Fd(5))); + + // Invalid dirfd → EBADF. + let err = FsPath::new(-1, "file.txt", || panic!("should not be called")).unwrap_err(); + assert_eq!(err, Errno::EBADF); + + // Path exceeding PATH_MAX → ENAMETOOLONG. + let long_path = "a".repeat(PATH_MAX + 1); + let err = FsPath::new(litebox_common_linux::AT_FDCWD, long_path.as_str(), || { + String::from("/") + }) + .unwrap_err(); + assert_eq!(err, Errno::ENAMETOOLONG); + } + + #[test] + fn getcwd_and_chdir() { + let task = crate::syscalls::tests::init_platform(None); + + // Default CWD is root. + let mut buf = [0u8; 256]; + let len = task.sys_getcwd(&mut buf).unwrap(); + let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); // strip NUL + assert_eq!(cwd, "/"); + + // chdir + getcwd round trip. + task.sys_mkdir("/test_chdir_dir", 0o777).unwrap(); + task.sys_chdir("/test_chdir_dir").unwrap(); + let len = task.sys_getcwd(&mut buf).unwrap(); + let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); + assert_eq!(cwd, "/test_chdir_dir/"); + + // chdir to nonexistent path → ENOENT. + assert_eq!( + task.sys_chdir("/does_not_exist").unwrap_err(), + Errno::ENOENT + ); + + // chdir to a regular file → ENOTDIR. + let fd = task + .sys_open( + "/test_chdir_file", + litebox::fs::OFlags::CREAT | litebox::fs::OFlags::WRONLY, + Mode::RUSR | Mode::WUSR, + ) + .unwrap(); + let _ = task.sys_close(i32::try_from(fd).unwrap()); + assert_eq!( + task.sys_chdir("/test_chdir_file").unwrap_err(), + Errno::ENOTDIR + ); + + // getcwd with too-small buffer → ERANGE. + let mut tiny = [0u8; 1]; + assert_eq!(task.sys_getcwd(&mut tiny).unwrap_err(), Errno::ERANGE); + } + + #[test] + fn chdir_relative_path() { + let task = crate::syscalls::tests::init_platform(None); + + // Create nested dirs: /rel_parent/rel_child + task.sys_mkdir("/rel_parent", 0o777).unwrap(); + task.sys_mkdir("/rel_parent/rel_child", 0o777).unwrap(); + + // chdir to /rel_parent first, then relative chdir into child. + task.sys_chdir("/rel_parent").unwrap(); + task.sys_chdir("rel_child").unwrap(); + + let mut buf = [0u8; 256]; + let len = task.sys_getcwd(&mut buf).unwrap(); + let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); + assert_eq!(cwd, "/rel_parent/rel_child/"); + + // chdir("..") should normalize back to /rel_parent/. + task.sys_chdir("..").unwrap(); + let len = task.sys_getcwd(&mut buf).unwrap(); + let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); + assert_eq!(cwd, "/rel_parent/"); + } + + /// Verify every path-taking syscall resolves relative paths after `chdir`. + #[test] + fn all_path_syscalls_respect_chdir() { + use litebox_common_linux::{AccessFlags, AtFlags}; + + let task = crate::syscalls::tests::init_platform(None); + + // Set up: mkdir + chdir into /cwd_test/. + task.sys_mkdir("/cwd_test", 0o777).unwrap(); + task.sys_chdir("/cwd_test").unwrap(); + + // ── sys_open: create a file via relative path ── + let fd = task + .sys_open( + "file.txt", + litebox::fs::OFlags::CREAT | litebox::fs::OFlags::WRONLY, + Mode::RUSR | Mode::WUSR, + ) + .unwrap(); + task.sys_close(i32::try_from(fd).unwrap()).unwrap(); + + // ── sys_stat: stat the relative file ── + task.sys_stat("file.txt").unwrap(); + + // ── sys_lstat: lstat the relative file ── + task.sys_lstat("file.txt").unwrap(); + + // ── sys_access: check relative file is accessible ── + task.sys_access("file.txt", AccessFlags::F_OK).unwrap(); + + // ── sys_mkdir: create a subdirectory via relative path ── + task.sys_mkdir("subdir", 0o777).unwrap(); + task.sys_stat("/cwd_test/subdir").unwrap(); // verify via absolute + + // ── sys_openat (AT_FDCWD + relative): open inside the new subdir ── + let fd = task + .sys_openat( + litebox_common_linux::AT_FDCWD, + "subdir/inner.txt", + litebox::fs::OFlags::CREAT | litebox::fs::OFlags::WRONLY, + Mode::RUSR | Mode::WUSR, + ) + .unwrap(); + task.sys_close(i32::try_from(fd).unwrap()).unwrap(); + + // ── sys_newfstatat (AT_FDCWD + relative) ── + task.sys_newfstatat( + litebox_common_linux::AT_FDCWD, + "subdir/inner.txt", + AtFlags::empty(), + ) + .unwrap(); + + // ── sys_unlinkat: remove a file via relative path ── + task.sys_unlinkat( + litebox_common_linux::AT_FDCWD, + "subdir/inner.txt", + AtFlags::empty(), + ) + .unwrap(); + assert_eq!( + task.sys_stat("/cwd_test/subdir/inner.txt").unwrap_err(), + Errno::ENOENT + ); + + // ── sys_unlinkat (AT_REMOVEDIR): remove directory via relative path ── + task.sys_unlinkat( + litebox_common_linux::AT_FDCWD, + "subdir", + AtFlags::AT_REMOVEDIR, + ) + .unwrap(); + assert_eq!( + task.sys_stat("/cwd_test/subdir").unwrap_err(), + Errno::ENOENT + ); + } +} From a80378e290d1a749af9723053e6a3eb527a1243b Mon Sep 17 00:00:00 2001 From: weitengchen Date: Mon, 2 Mar 2026 10:54:24 -0800 Subject: [PATCH 2/2] fix --- litebox_shim_linux/src/syscalls/file.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 2e3b8f871..f225aaca8 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -246,7 +246,8 @@ impl Task { return Err(Errno::EINVAL); } - let fs_path = FsPath::new(dirfd, pathname, || self.fs.borrow().cwd.read().clone())?; + let get_cwd = || self.fs.borrow().cwd.read().clone(); + let fs_path = FsPath::new(dirfd, pathname, get_cwd)?; match fs_path { FsPath::Absolute { path } => { if flags.contains(AtFlags::AT_REMOVEDIR) { @@ -707,7 +708,8 @@ impl Task { pathname: impl path::Arg, buf: &mut [u8], ) -> Result { - let fspath = FsPath::new(dirfd, pathname, || self.fs.borrow().cwd.read().clone())?; + let get_cwd = || self.fs.borrow().cwd.read().clone(); + let fspath = FsPath::new(dirfd, pathname, get_cwd)?; let path = match fspath { FsPath::Absolute { path } => { self.do_readlink(path.to_str().map_err(|_| Errno::EINVAL)?)