From 18485eafbb4087fb4bb9ed4c31c2b74923bda300 Mon Sep 17 00:00:00 2001 From: weitengchen Date: Fri, 17 Apr 2026 22:30:42 +0000 Subject: [PATCH 1/2] support mknodat --- litebox_common_linux/src/lib.rs | 14 ++++++++ litebox_shim_linux/src/lib.rs | 8 +++++ litebox_shim_linux/src/syscalls/file.rs | 43 +++++++++++++++++++++++-- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index 3929df6a5..357c88ab5 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -219,6 +219,7 @@ bitflags::bitflags! { } #[repr(u32)] +#[derive(IntEnum)] pub enum InodeType { /// FIFO (named pipe) NamedPipe = 0o010000, @@ -2136,6 +2137,12 @@ pub enum SyscallRequest { fd: i32, length: usize, }, + Mknodat { + dirfd: i32, + pathname: Platform::RawConstPointer, + mode_and_type: u32, + dev: u32, + }, Unlinkat { dirfd: i32, pathname: Platform::RawConstPointer, @@ -2723,6 +2730,13 @@ impl SyscallRequest { mode: ctx.sys_req_arg(2), } } + Sysno::mknodat => sys_req!(Mknodat { dirfd,pathname:*,mode_and_type,dev }), + Sysno::mknod => SyscallRequest::Mknodat { + dirfd: AT_FDCWD, + pathname: ctx.sys_req_ptr(0), + mode_and_type: ctx.sys_req_arg(1), + dev: ctx.sys_req_arg(2), + }, Sysno::unlinkat => sys_req!(Unlinkat { dirfd,pathname:*,flags }), Sysno::unlink => { // unlink is equivalent to unlinkat with dirfd AT_FDCWD and flags 0 diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 2834f7b72..62d1e4ad8 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -861,6 +861,14 @@ impl Task { syscall!(sys_openat(dirfd, path, flags, mode)) }), SyscallRequest::Ftruncate { fd, length } => syscall!(sys_ftruncate(fd, length)), + SyscallRequest::Mknodat { + dirfd, + pathname, + mode_and_type, + dev, + } => pathname.to_cstring().map_or(Err(Errno::EFAULT), |path| { + syscall!(sys_mknodat(dirfd, path, mode_and_type, dev)) + }), SyscallRequest::Unlinkat { dirfd, pathname, diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 46971a4cf..dd02f2bc2 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -17,8 +17,8 @@ use litebox::{ utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}, }; use litebox_common_linux::{ - AtFlags, EfdFlags, EpollCreateFlags, FcntlArg, FileDescriptorFlags, FileStat, IoReadVec, - IoWriteVec, IoctlArg, TimeParam, errno::Errno, + AtFlags, EfdFlags, EpollCreateFlags, FcntlArg, FileDescriptorFlags, FileStat, InodeType, + IoReadVec, IoWriteVec, IoctlArg, TimeParam, errno::Errno, }; use litebox_platform_multiplex::Platform; @@ -258,6 +258,45 @@ impl Task { .flatten() } + /// Handle syscall `mknodat` — create a filesystem node. + pub(crate) fn sys_mknodat( + &self, + dirfd: i32, + pathname: impl path::Arg, + mode_and_type: u32, + _dev: u32, + ) -> Result<(), Errno> { + const FILE_TYPE_MASK: u32 = 0o170000; + + let file_type = mode_and_type & FILE_TYPE_MASK; + let file_type = if file_type == 0 { + // zero translates to S_IFREG + InodeType::File + } else { + InodeType::try_from(file_type).map_err(|_| Errno::EINVAL)? + }; + match file_type { + InodeType::File => { + let mode = Mode::from_bits_truncate(mode_and_type & !FILE_TYPE_MASK); + let fd = self.sys_openat( + dirfd, + pathname, + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + mode, + )?; + self.sys_close(fd.cast_signed())?; + } + // TODO: Named pipe, socket, block and char files are not supported + InodeType::NamedPipe + | InodeType::Socket + | InodeType::BlockDevice + | InodeType::CharDevice + | InodeType::Dir => return Err(Errno::EPERM), + InodeType::SymLink => return Err(Errno::EINVAL), + } + Ok(()) + } + /// Handle syscall `unlinkat` pub(crate) fn sys_unlinkat( &self, From c5337b4c9d8db1f3cb346879a07796ad280faa31 Mon Sep 17 00:00:00 2001 From: weitengchen Date: Thu, 14 May 2026 14:59:55 -0700 Subject: [PATCH 2/2] fix issues --- litebox/src/fs/in_mem.rs | 17 +- litebox/src/fs/layered.rs | 5 +- litebox/src/fs/tests.rs | 52 ++++++ litebox_shim_linux/src/syscalls/file.rs | 214 +++++++++++++++++------- 4 files changed, 214 insertions(+), 74 deletions(-) diff --git a/litebox/src/fs/in_mem.rs b/litebox/src/fs/in_mem.rs index 483982304..94a3f8df1 100644 --- a/litebox/src/fs/in_mem.rs +++ b/litebox/src/fs/in_mem.rs @@ -195,14 +195,14 @@ impl super::FileSystem for FileSystem unimplemented!("{flags:?}") } let path = self.absolute_path(path)?; - let entry = if flags.contains(OFlags::CREAT) { + let (entry, created) = if flags.contains(OFlags::CREAT) { let mut root = self.root.write(); let (parent, entry) = root.parent_and_entry(&path, self.current_user)?; if let Some(entry) = entry { if flags.contains(OFlags::EXCL) { return Err(OpenError::AlreadyExists); } - entry + (entry, false) } else { let Some((_, parent)) = parent else { // Only `/` does not have a parent; any other scenario (e.g., missing ancestor) @@ -233,7 +233,7 @@ impl super::FileSystem for FileSystem }))); let old = root.entries.insert(path, entry.clone()); assert!(old.is_none()); - entry + (entry, true) } } else { let root = self.root.read(); @@ -241,18 +241,19 @@ impl super::FileSystem for FileSystem let Some(entry) = entry else { return Err(PathError::NoSuchFileOrDirectory)?; }; - entry + (entry, false) }; - let read_allowed = if flags.contains(OFlags::RDONLY) || flags.contains(OFlags::RDWR) { - if !self.current_user.can_read(&entry.perms()) { + let access_mode = flags & (OFlags::WRONLY | OFlags::RDWR); + let read_allowed = if access_mode == OFlags::RDONLY || access_mode == OFlags::RDWR { + if !created && !self.current_user.can_read(&entry.perms()) { return Err(OpenError::AccessNotAllowed); } true } else { false }; - let write_allowed = if flags.contains(OFlags::WRONLY) || flags.contains(OFlags::RDWR) { - if !self.current_user.can_write(&entry.perms()) { + let write_allowed = if access_mode == OFlags::WRONLY || access_mode == OFlags::RDWR { + if !created && !self.current_user.can_write(&entry.perms()) { return Err(OpenError::AccessNotAllowed); } true diff --git a/litebox/src/fs/layered.rs b/litebox/src/fs/layered.rs index f1ceff00b..f0689af44 100644 --- a/litebox/src/fs/layered.rs +++ b/litebox/src/fs/layered.rs @@ -740,9 +740,8 @@ impl< .litebox .descriptor_table() .with_entry(fd, |descriptor| { - if !descriptor.entry.flags.contains(OFlags::RDONLY) - && !descriptor.entry.flags.contains(OFlags::RDWR) - { + let access_mode = descriptor.entry.flags & (OFlags::WRONLY | OFlags::RDWR); + if access_mode == OFlags::WRONLY { Err(ReadError::NotForReading) } else { Ok(Arc::clone(&descriptor.entry.entry)) diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index fcc768e68..cfc34b083 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -60,6 +60,58 @@ mod in_mem { }); } + #[test] + fn write_only_open_does_not_require_read_permission() { + let litebox = LiteBox::new(MockPlatform::new()); + let mut fs = in_mem::FileSystem::new(&litebox); + fs.with_root_privileges(|fs| { + fs.mkdir("/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("Failed to create /tmp"); + }); + + let path = "/tmp/write_only"; + let fd = fs + .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::WUSR) + .expect("Failed to create write-only file"); + fs.write(&fd, b"x", None).expect("Failed to write file"); + + let mut buffer = [0]; + assert!(matches!( + fs.read(&fd, &mut buffer, None), + Err(crate::fs::errors::ReadError::NotForReading) + )); + fs.close(&fd).expect("Failed to close file"); + + assert!(matches!( + fs.open(path, OFlags::RDONLY, Mode::empty()), + Err(crate::fs::errors::OpenError::AccessNotAllowed) + )); + } + + #[test] + fn newly_created_file_does_not_require_its_own_permissions() { + let litebox = LiteBox::new(MockPlatform::new()); + let mut fs = in_mem::FileSystem::new(&litebox); + fs.with_root_privileges(|fs| { + fs.mkdir("/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("Failed to create /tmp"); + }); + + let path = "/tmp/zero_mode"; + let fd = fs + .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::empty()) + .expect("Failed to create zero-mode file"); + fs.write(&fd, b"x", None).expect("Failed to write file"); + fs.close(&fd).expect("Failed to close file"); + + let status = fs.file_status(path).expect("Failed to stat file"); + assert_eq!(status.mode, Mode::empty()); + assert!(matches!( + fs.open(path, OFlags::WRONLY, Mode::empty()), + Err(crate::fs::errors::OpenError::AccessNotAllowed) + )); + } + #[test] fn root_directory_creation_and_removal() { let litebox = LiteBox::new(MockPlatform::new()); diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index dd02f2bc2..878837497 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -168,6 +168,9 @@ impl Task { /// 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.is_empty() { + return Err(Errno::ENOENT); + } if path_str.starts_with('/') { CString::new(path_str.to_string()).map_err(|_| Errno::EINVAL) } else { @@ -177,26 +180,48 @@ impl Task { } } - /// 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); - let old_mask = self - .fs - .borrow() - .umask - .swap(new_mask.bits(), Ordering::Relaxed); - Mode::from_bits_retain(old_mask) + /// Resolve a path relative to a dirfd. + /// + /// Note that an empty path is not valid for this function, and will be rejected with `ENOENT`. + fn resolve_path_at(&self, dirfd: i32, pathname: impl path::Arg) -> Result { + let get_cwd = || self.fs.borrow().cwd.read().clone(); + let fs_path = FsPath::new(dirfd, pathname, get_cwd)?; + match fs_path { + FsPath::Absolute { path } => Ok(path), + FsPath::Cwd | FsPath::Fd(_) => Err(Errno::ENOENT), + FsPath::FdRelative { fd: _, path: _ } => { + log_unsupported!("path resolution with FsPath::FdRelative"); + Err(Errno::EINVAL) + } + } } - /// Handle syscall `open` - pub fn sys_open(&self, path: impl path::Arg, flags: OFlags, mode: Mode) -> Result { - let path = self.resolve_path(path)?; + fn do_open( + &self, + path: impl path::Arg, + flags: OFlags, + mode: Mode, + ) -> Result, Errno> { let mode = mode & !self.get_umask(); - let file = self - .files + self.files .borrow() .fs - .open(path, flags - OFlags::CLOEXEC, mode)?; + .open(path, flags - OFlags::CLOEXEC, mode) + .map_err(Errno::from) + } + + fn do_openat( + &self, + dirfd: i32, + pathname: impl path::Arg, + flags: OFlags, + mode: Mode, + ) -> Result, Errno> { + let path = self.resolve_path_at(dirfd, pathname)?; + self.do_open(path, flags, mode) + } + + fn insert_raw_file_fd(&self, file: TypedFd, flags: OFlags) -> Result { if flags.contains(OFlags::CLOEXEC) { let None = self .global @@ -215,6 +240,24 @@ impl Task { Ok(u32::try_from(raw_fd).unwrap()) } + /// 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); + let old_mask = self + .fs + .borrow() + .umask + .swap(new_mask.bits(), Ordering::Relaxed); + Mode::from_bits_retain(old_mask) + } + + /// Handle syscall `open` + pub fn sys_open(&self, path: impl path::Arg, flags: OFlags, mode: Mode) -> Result { + let path = self.resolve_path(path)?; + let file = self.do_open(path, flags, mode)?; + self.insert_raw_file_fd(file, flags) + } + /// Handle syscall `openat` pub fn sys_openat( &self, @@ -223,20 +266,8 @@ impl Task { flags: OFlags, mode: Mode, ) -> Result { - let get_cwd = || self.fs.borrow().cwd.read().clone(); - let fs_path = FsPath::new(dirfd, pathname, get_cwd)?; - match fs_path { - 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) - } - FsPath::FdRelative { fd: _, path: _ } => { - log_unsupported!("openat with FsPath::FdRelative"); - Err(Errno::EINVAL) - } - } + let file = self.do_openat(dirfd, pathname, flags, mode)?; + self.insert_raw_file_fd(file, flags) } /// Handle syscall `ftruncate` @@ -278,13 +309,14 @@ impl Task { match file_type { InodeType::File => { let mode = Mode::from_bits_truncate(mode_and_type & !FILE_TYPE_MASK); - let fd = self.sys_openat( + let file = self.do_openat( dirfd, pathname, OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, mode, )?; - self.sys_close(fd.cast_signed())?; + let files = self.files.borrow(); + let _ = files.fs.close(&file); } // TODO: Named pipe, socket, block and char files are not supported InodeType::NamedPipe @@ -308,18 +340,11 @@ impl Task { return Err(Errno::EINVAL); } - 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) { - self.files.borrow().fs.rmdir(path).map_err(Errno::from) - } else { - self.files.borrow().fs.unlink(path).map_err(Errno::from) - } - } - FsPath::Cwd => Err(Errno::EINVAL), - FsPath::Fd(_) | FsPath::FdRelative { .. } => unimplemented!(), + let path = self.resolve_path_at(dirfd, pathname)?; + if flags.contains(AtFlags::AT_REMOVEDIR) { + self.files.borrow().fs.rmdir(path).map_err(Errno::from) + } else { + self.files.borrow().fs.unlink(path).map_err(Errno::from) } } @@ -779,18 +804,8 @@ impl Task { pathname: impl path::Arg, buf: &mut [u8], ) -> Result { - 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)?) - } - FsPath::Cwd => { - let cwd = self.fs.borrow().cwd.read().clone(); - self.do_readlink(&cwd) - } - FsPath::Fd(_) | FsPath::FdRelative { .. } => unimplemented!(), - }?; + let pathname = self.resolve_path_at(dirfd, pathname)?; + let path = self.do_readlink(pathname.to_str().map_err(|_| Errno::EINVAL)?)?; let bytes = path.as_bytes(); let min_len = core::cmp::min(buf.len(), bytes.len()); buf[..min_len].copy_from_slice(&bytes[..min_len]); @@ -1014,7 +1029,8 @@ impl Task { ) -> Result { let current_support_flags = AtFlags::AT_EMPTY_PATH; if flags.contains(current_support_flags.complement()) { - todo!("unsupported flags"); + log_unsupported!("unsupported flags: {flags:?}"); + return Err(Errno::EINVAL); } let files = self.files.borrow(); @@ -1024,14 +1040,17 @@ impl Task { FsPath::Absolute { path } => { self.do_stat(path, !flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW))? } - FsPath::Cwd => files.fs.file_status(get_cwd())?.into(), - FsPath::Fd(fd) => { - let Ok(raw_fd) = usize::try_from(fd) else { - return Err(Errno::EBADF); - }; - descriptor_stat(raw_fd, self)? + FsPath::Cwd if flags.contains(AtFlags::AT_EMPTY_PATH) => { + files.fs.file_status(get_cwd())?.into() + } + FsPath::Fd(fd) if flags.contains(AtFlags::AT_EMPTY_PATH) => { + descriptor_stat(fd as usize, self)? + } + FsPath::Cwd | FsPath::Fd(_) => return Err(Errno::ENOENT), + FsPath::FdRelative { .. } => { + log_unsupported!("relative fstatat with AT_EMPTY_PATH unset is not supported yet"); + return Err(Errno::EINVAL); } - FsPath::FdRelative { .. } => todo!(), }; Ok(fstat) } @@ -2270,6 +2289,75 @@ mod tests { assert_eq!(cwd, "/rel_parent/"); } + #[test] + fn mknodat_regular_file_does_not_consume_fd_limit() { + use litebox_common_linux::{Rlimit, RlimitResource}; + + let task = crate::syscalls::tests::init_platform(None); + let old_limit = task.do_prlimit(RlimitResource::NOFILE, None).unwrap(); + task.do_prlimit( + RlimitResource::NOFILE, + Some(Rlimit { + rlim_cur: 3, + rlim_max: old_limit.rlim_max, + }), + ) + .unwrap(); + let path = "/mknodat_at_fd_limit"; + + let result = task.sys_mknodat( + litebox_common_linux::AT_FDCWD, + path, + InodeType::File as u32 | (Mode::RUSR | Mode::WUSR).bits(), + 0, + ); + + assert!( + task.sys_stat(path).is_ok(), + "mknodat created the file before returning {result:?}" + ); + assert_eq!(result, Ok(())); + } + + #[test] + fn empty_pathnames_return_enoent() { + let task = crate::syscalls::tests::init_platform(None); + + assert_eq!( + task.sys_open("", OFlags::RDONLY, Mode::empty()) + .unwrap_err(), + Errno::ENOENT + ); + assert_eq!( + task.sys_open("", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .unwrap_err(), + Errno::ENOENT + ); + assert_eq!(task.sys_stat("").unwrap_err(), Errno::ENOENT); + assert_eq!( + task.sys_unlinkat(litebox_common_linux::AT_FDCWD, "", AtFlags::empty()) + .unwrap_err(), + Errno::ENOENT + ); + assert_eq!(task.sys_mkdir("", 0o755).unwrap_err(), Errno::ENOENT); + assert_eq!( + task.sys_mknodat( + litebox_common_linux::AT_FDCWD, + "", + InodeType::File as u32 | Mode::RWXU.bits(), + 0, + ) + .unwrap_err(), + Errno::ENOENT + ); + let mut buffer = [0u8; 16]; + assert_eq!( + task.sys_readlinkat(litebox_common_linux::AT_FDCWD, "", &mut buffer) + .unwrap_err(), + Errno::ENOENT + ); + } + /// Verify every path-taking syscall resolves relative paths after `chdir`. #[test] fn all_path_syscalls_respect_chdir() {