diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index ad2d28ef7..74936635d 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -11,7 +11,7 @@ use int_enum::IntEnum; use litebox::{ fs::OFlags, platform::{RawConstPointer, RawMutPointer}, - utils::{ReinterpretSignedExt as _, TruncateExt}, + utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}, }; use syscalls::Sysno; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -374,6 +374,172 @@ impl From for FileStat { } } +bitflags::bitflags! { + /// Field-selection mask for [`statx`]. + /// + /// Each bit asks the kernel to fill the corresponding field in [`Statx`]. + /// `STATX__RESERVED` (0x8000_0000) is rejected with `EINVAL` by Linux and + /// must not appear in user input. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct StatxMask: u32 { + const STATX_TYPE = 0x0000_0001; + const STATX_MODE = 0x0000_0002; + const STATX_NLINK = 0x0000_0004; + const STATX_UID = 0x0000_0008; + const STATX_GID = 0x0000_0010; + const STATX_ATIME = 0x0000_0020; + const STATX_MTIME = 0x0000_0040; + const STATX_CTIME = 0x0000_0080; + const STATX_INO = 0x0000_0100; + const STATX_SIZE = 0x0000_0200; + const STATX_BLOCKS = 0x0000_0400; + const STATX_BASIC_STATS = Self::STATX_TYPE.bits() + | Self::STATX_MODE.bits() + | Self::STATX_NLINK.bits() + | Self::STATX_UID.bits() + | Self::STATX_GID.bits() + | Self::STATX_ATIME.bits() + | Self::STATX_MTIME.bits() + | Self::STATX_CTIME.bits() + | Self::STATX_INO.bits() + | Self::STATX_SIZE.bits() + | Self::STATX_BLOCKS.bits(); + /// The basic-stats fields LiteBox actually fills. Excludes the + /// time bits because `FileStatus` doesn't carry timestamps. + const STATX_BASIC_FILLED = Self::STATX_BASIC_STATS.bits() + & !(Self::STATX_ATIME.bits() | Self::STATX_MTIME.bits() | Self::STATX_CTIME.bits()); + const STATX_BTIME = 0x0000_0800; + const STATX_MNT_ID = 0x0000_1000; + const STATX_DIOALIGN = 0x0000_2000; + const STATX_MNT_ID_UNIQUE = 0x0000_4000; + const STATX_SUBVOL = 0x0000_8000; + const STATX_WRITE_ATOMIC = 0x0001_0000; + const STATX_DIO_READ_ALIGN = 0x0002_0000; + + /// Named constant so callers can spell out the EINVAL check explicitly. + const STATX__RESERVED = 0x8000_0000; + + /// Accept unknown future bits without truncating; the kernel silently + /// ignores them and reports the actual filled set via [`Statx::stx_mask`]. + const _ = !0; + } +} + +/// Linux's `struct statx_timestamp` (16 bytes, `linux/stat.h`). +#[repr(C)] +#[derive(Clone, Copy, Default, Debug, FromBytes, IntoBytes, Immutable)] +pub struct StatxTimestamp { + pub tv_sec: i64, + pub tv_nsec: u32, + #[expect(clippy::pub_underscore_fields)] + pub __reserved: i32, +} + +/// Linux's `struct statx` (256 bytes, `linux/stat.h`). +#[repr(C)] +#[derive(Clone, Copy, Default, Debug, FromBytes, IntoBytes, Immutable)] +pub struct Statx { + pub stx_mask: u32, + pub stx_blksize: u32, + pub stx_attributes: u64, + pub stx_nlink: u32, + pub stx_uid: u32, + pub stx_gid: u32, + pub stx_mode: u16, + #[expect(clippy::pub_underscore_fields)] + pub __spare0: [u16; 1], + pub stx_ino: u64, + pub stx_size: u64, + pub stx_blocks: u64, + pub stx_attributes_mask: u64, + pub stx_atime: StatxTimestamp, + pub stx_btime: StatxTimestamp, + pub stx_ctime: StatxTimestamp, + pub stx_mtime: StatxTimestamp, + pub stx_rdev_major: u32, + pub stx_rdev_minor: u32, + pub stx_dev_major: u32, + pub stx_dev_minor: u32, + pub stx_mnt_id: u64, + pub stx_dio_mem_align: u32, + pub stx_dio_offset_align: u32, + #[expect(clippy::pub_underscore_fields)] + pub __spare3: [u64; 12], +} + +/// Extract the major component from a Linux `dev_t` (matches `major(3)` from glibc). +fn dev_major(dev: u64) -> u32 { + (((dev >> 8) & 0xfff) | ((dev >> 32) & !0xfff)).truncate() +} +/// Extract the minor component from a Linux `dev_t` (matches `minor(3)`). +fn dev_minor(dev: u64) -> u32 { + ((dev & 0xff) | ((dev >> 12) & !0xff)).truncate() +} + +impl From for Statx { + fn from(value: litebox::fs::FileStatus) -> Self { + let litebox::fs::FileStatus { + file_type, + mode, + size, + owner: litebox::fs::UserInfo { user, group }, + node_info: litebox::fs::NodeInfo { dev, ino, rdev }, + blksize, + .. + } = value; + let dev = dev as u64; + let rdev = rdev.map_or(0u64, |r| r.get() as u64); + Self { + stx_mask: StatxMask::STATX_BASIC_FILLED.bits(), + stx_blksize: blksize.truncate(), + stx_nlink: 1, + stx_uid: u32::from(user), + stx_gid: u32::from(group), + stx_mode: (mode.bits() | InodeType::from(file_type) as u32).truncate(), + stx_ino: ino as u64, + stx_size: size as u64, + stx_blocks: 0, + stx_rdev_major: dev_major(rdev), + stx_rdev_minor: dev_minor(rdev), + stx_dev_major: dev_major(dev), + stx_dev_minor: dev_minor(dev), + ..Default::default() + } + } +} + +fn statx_timestamp(seconds: i64, nanoseconds: i64) -> StatxTimestamp { + StatxTimestamp { + tv_sec: seconds, + tv_nsec: u32::try_from(nanoseconds).unwrap_or(u32::MAX), + ..Default::default() + } +} + +impl From for Statx { + fn from(value: FileStat) -> Self { + Self { + stx_mask: StatxMask::STATX_BASIC_STATS.bits(), + stx_blksize: value.st_blksize.truncate(), + stx_nlink: value.st_nlink.truncate(), + stx_uid: value.st_uid, + stx_gid: value.st_gid, + stx_mode: value.st_mode.truncate(), + stx_ino: value.st_ino, + stx_size: value.st_size as u64, + stx_blocks: value.st_blocks.reinterpret_as_unsigned(), + stx_atime: statx_timestamp(value.st_atime, value.st_atime_nsec), + stx_ctime: statx_timestamp(value.st_ctime, value.st_ctime_nsec), + stx_mtime: statx_timestamp(value.st_mtime, value.st_mtime_nsec), + stx_rdev_major: dev_major(value.st_rdev), + stx_rdev_minor: dev_minor(value.st_rdev), + stx_dev_major: dev_major(value.st_dev), + stx_dev_minor: dev_minor(value.st_dev), + ..Default::default() + } + } +} + /// Commands for use with `fcntl`. #[derive(Debug)] #[non_exhaustive] @@ -2188,6 +2354,13 @@ pub enum SyscallRequest { new_value: Platform::RawConstPointer, old_value: Option>, }, + Statx { + dirfd: i32, + pathname: Option>, + flags: AtFlags, + mask: StatxMask, + statxbuf: Platform::RawMutPointer, + }, } impl SyscallRequest { @@ -2655,9 +2828,15 @@ impl SyscallRequest { Sysno::alarm => sys_req!(Alarm { seconds }), Sysno::pause => SyscallRequest::Pause, Sysno::setitimer => sys_req!(SetITimer { which:?, new_value:*, old_value:* }), - + Sysno::statx => sys_req!(Statx { + dirfd, + pathname:*, + flags, + mask, + statxbuf:*, + }), // Noisy unsupported syscalls. - Sysno::statx | Sysno::io_uring_setup | Sysno::rseq | Sysno::statfs => { + Sysno::io_uring_setup | Sysno::rseq | Sysno::statfs => { return Err(errno::Errno::ENOSYS); } sysno => { @@ -2981,6 +3160,7 @@ reinterpret_truncated_from_usize_for! { EfdFlags, RngFlags, TimerFlags, + StatxMask, ], } diff --git a/litebox_runner_linux_userland/tests/helpers.h b/litebox_runner_linux_userland/tests/helpers.h index b03a6d369..6d185570e 100644 --- a/litebox_runner_linux_userland/tests/helpers.h +++ b/litebox_runner_linux_userland/tests/helpers.h @@ -19,17 +19,20 @@ #include #include +#define TEST_ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (line %d): %s (errno=%d: %s)\n", \ + __func__, __LINE__, msg, errno, strerror(errno)); \ + exit(1); \ + } \ + } while (0) + static inline void die(const char *msg) { perror(msg); exit(1); } -static inline void fail_errno(const char *op, int expected_errno) { - fprintf(stderr, "FAIL: %s expected errno=%d (%s), got errno=%d (%s)\n", - op, expected_errno, strerror(expected_errno), errno, strerror(errno)); - exit(1); -} - static inline void expect_sys_shutdown(int fd, int how, const char *op) { errno = 0; if (syscall(SYS_shutdown, fd, how) != 0) { @@ -40,25 +43,15 @@ static inline void expect_sys_shutdown(int fd, int how, const char *op) { static inline void expect_sys_shutdown_errno(int fd, int how, int expected_errno, const char *op) { errno = 0; long ret = syscall(SYS_shutdown, fd, how); - if (ret != -1) { - fprintf(stderr, "FAIL: %s expected failure, got %ld\n", op, ret); - exit(1); - } - if (errno != expected_errno) { - fail_errno(op, expected_errno); - } + TEST_ASSERT(ret == -1, op); + TEST_ASSERT(errno == expected_errno, op); } static inline void expect_send_errno(int fd, int expected_errno, const char *op) { errno = 0; ssize_t n = send(fd, "x", 1, MSG_DONTWAIT | MSG_NOSIGNAL); - if (n != -1) { - fprintf(stderr, "FAIL: %s expected failure, got %zd\n", op, n); - exit(1); - } - if (errno != expected_errno) { - fail_errno(op, expected_errno); - } + TEST_ASSERT(n == -1, op); + TEST_ASSERT(errno == expected_errno, op); } // Blocking recv (no MSG_DONTWAIT) that we expect to time out via SO_RCVTIMEO. Distinct from @@ -69,13 +62,8 @@ static inline void expect_blocking_recv_eagain(int fd, const char *op) { errno = 0; ssize_t n = recv(fd, buf, sizeof(buf), 0); - if (n != -1) { - fprintf(stderr, "FAIL: %s expected timeout failure, got %zd\n", op, n); - exit(1); - } - if (errno != EAGAIN) { - fail_errno(op, EAGAIN); - } + TEST_ASSERT(n == -1, op); + TEST_ASSERT(errno == EAGAIN, op); } static inline void expect_recv_errno(int fd, int expected_errno, const char *op) { @@ -83,13 +71,8 @@ static inline void expect_recv_errno(int fd, int expected_errno, const char *op) errno = 0; ssize_t n = recv(fd, buf, sizeof(buf), MSG_DONTWAIT); - if (n != -1) { - fprintf(stderr, "FAIL: %s expected failure, got %zd\n", op, n); - exit(1); - } - if (errno != expected_errno) { - fail_errno(op, expected_errno); - } + TEST_ASSERT(n == -1, op); + TEST_ASSERT(errno == expected_errno, op); } static inline void expect_recv_eof(int fd, const char *op) { diff --git a/litebox_runner_linux_userland/tests/statx.c b/litebox_runner_linux_userland/tests/statx.c new file mode 100644 index 000000000..21b8f1575 --- /dev/null +++ b/litebox_runner_linux_userland/tests/statx.c @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Tests: statx happy path, AT_EMPTY_PATH on fds, ENOENT/EBADF/EINVAL errno +// branches. Goes through syscall(SYS_statx, ...) to exercise the raw kernel +// surface that LiteBox intercepts (not the libc wrapper, which may massage +// arguments). + +#include "helpers.h" + +#include +#include +#include +#include + +#ifndef STATX__RESERVED +#define STATX__RESERVED 0x80000000U +#endif + +#define TEST_PATH "/tmp/statx_test_file" +#define TEST_CONTENT "hello statx" +#define TEST_CONTENT_LEN ((size_t)(sizeof(TEST_CONTENT) - 1)) + +static int raw_statx(int dirfd, const char *pathname, int flags, + unsigned int mask, struct statx *buf) { + return (int)syscall(SYS_statx, dirfd, pathname, flags, mask, buf); +} + +static void expect_statx(int dirfd, const char *pathname, int flags, + unsigned int mask, struct statx *sx, const char *op) { + memset(sx, 0, sizeof(*sx)); + errno = 0; + TEST_ASSERT(raw_statx(dirfd, pathname, flags, mask, sx) == 0, op); +} + +static void expect_statx_errno(int dirfd, const char *pathname, int flags, + unsigned int mask, int expected_errno, + const char *op) { + struct statx sx; + memset(&sx, 0, sizeof(sx)); + errno = 0; + int rc = raw_statx(dirfd, pathname, flags, mask, &sx); + TEST_ASSERT(rc == -1, op); + TEST_ASSERT(errno == expected_errno, op); +} + +static void seed_test_file(void) { + int fd = openat(AT_FDCWD, TEST_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0644); + TEST_ASSERT(fd >= 0, "openat create test file"); + ssize_t n = write(fd, TEST_CONTENT, TEST_CONTENT_LEN); + TEST_ASSERT(n == (ssize_t)TEST_CONTENT_LEN, "write test content"); + TEST_ASSERT(close(fd) == 0, "close test file"); +} + +static void cleanup_test_file(void) { + (void)unlink(TEST_PATH); +} + +static void test_absolute_happy_path(void) { + struct statx sx; + expect_statx(AT_FDCWD, TEST_PATH, 0, STATX_BASIC_STATS, &sx, + "statx absolute happy path"); + TEST_ASSERT((sx.stx_mask & STATX_TYPE) != 0, "stx_mask STATX_TYPE set"); + TEST_ASSERT((sx.stx_mask & STATX_SIZE) != 0, "stx_mask STATX_SIZE set"); + TEST_ASSERT((sx.stx_mode & S_IFMT) == S_IFREG, "stx_mode says regular file"); + TEST_ASSERT(sx.stx_size == TEST_CONTENT_LEN, "stx_size matches written length"); +} + +static void test_at_empty_path_fd(void) { + int fd = openat(AT_FDCWD, TEST_PATH, O_RDONLY); + TEST_ASSERT(fd >= 0, "openat for AT_EMPTY_PATH"); + struct statx sx; + memset(&sx, 0, sizeof(sx)); + int rc = raw_statx(fd, "", AT_EMPTY_PATH, STATX_BASIC_STATS, &sx); + int saved = errno; + TEST_ASSERT(close(fd) == 0, "close AT_EMPTY_PATH fd"); + errno = saved; + TEST_ASSERT(rc == 0, "statx AT_EMPTY_PATH on fd"); + TEST_ASSERT((sx.stx_mode & S_IFMT) == S_IFREG, "stx_mode regular via AT_EMPTY_PATH"); + TEST_ASSERT(sx.stx_size == TEST_CONTENT_LEN, "size via AT_EMPTY_PATH on fd"); +} + +static void test_at_empty_path_cwd(void) { + // With dirfd==AT_FDCWD and an empty path + AT_EMPTY_PATH, operate on cwd. + struct statx sx; + expect_statx(AT_FDCWD, "", AT_EMPTY_PATH, STATX_BASIC_STATS, &sx, + "statx AT_EMPTY_PATH on AT_FDCWD"); + TEST_ASSERT((sx.stx_mode & S_IFMT) == S_IFDIR, "stx_mode says directory for cwd"); +} + +static void test_enoent(void) { + expect_statx_errno(AT_FDCWD, "/tmp/statx_does_not_exist_xyzzy", 0, + STATX_BASIC_STATS, ENOENT, "statx ENOENT on missing path"); +} + +static void test_enoent_empty_path_no_flag(void) { + // Empty pathname without AT_EMPTY_PATH must fail with ENOENT — covers the + // `FsPath::Cwd | FsPath::Fd(_)` fall-through arm in the shim. + expect_statx_errno(AT_FDCWD, "", 0, STATX_BASIC_STATS, ENOENT, + "statx ENOENT on empty path without AT_EMPTY_PATH"); +} + +static void test_ebadf(void) { + // dirfd = -99 with a relative path is invalid; Linux returns EBADF. + expect_statx_errno(-99, "relpath", 0, STATX_BASIC_STATS, EBADF, + "statx EBADF on bad dirfd"); +} + +static void test_einval_reserved_mask(void) { + expect_statx_errno(AT_FDCWD, TEST_PATH, 0, STATX__RESERVED, EINVAL, + "statx EINVAL on STATX__RESERVED mask bit"); +} + +static void test_einval_bad_flag(void) { + // Pick a bit that is not in any documented AT_* / AT_STATX_* flag. + const int bogus_flag = 0x40000000; + expect_statx_errno(AT_FDCWD, TEST_PATH, bogus_flag, STATX_BASIC_STATS, EINVAL, + "statx EINVAL on bogus flag bit"); +} + +int main(void) { + printf("statx tests starting...\n"); + seed_test_file(); + test_absolute_happy_path(); + test_at_empty_path_fd(); + test_at_empty_path_cwd(); + test_enoent(); + test_enoent_empty_path_no_flag(); + test_ebadf(); + test_einval_reserved_mask(); + test_einval_bad_flag(); + cleanup_test_file(); + printf("All statx tests passed.\n"); + return 0; +} diff --git a/litebox_runner_linux_userland/tests/unix_dgram_shutdown.c b/litebox_runner_linux_userland/tests/unix_dgram_shutdown.c index c8e2a2361..1ec767f6d 100644 --- a/litebox_runner_linux_userland/tests/unix_dgram_shutdown.c +++ b/litebox_runner_linux_userland/tests/unix_dgram_shutdown.c @@ -163,13 +163,8 @@ static void test_pre_connect_shutdown_write_blocks_future_sends(void) { errno = 0; ssize_t n = sendto(sender, "x", 1, MSG_DONTWAIT | MSG_NOSIGNAL, (struct sockaddr *)&server_addr, sizeof(server_addr)); - if (n != -1) { - fprintf(stderr, "FAIL: sendto after pre-connect SHUT_WR expected failure, got %zd\n", n); - exit(1); - } - if (errno != EPIPE) { - fail_errno("sendto after pre-connect SHUT_WR", EPIPE); - } + TEST_ASSERT(n == -1, "sendto after pre-connect SHUT_WR"); + TEST_ASSERT(errno == EPIPE, "sendto after pre-connect SHUT_WR"); if (connect(sender, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) { die("connect after pre-connect SHUT_WR"); @@ -209,13 +204,8 @@ static void test_shutdown_invalid_how_returns_einval(void) { errno = 0; long ret = syscall(SYS_shutdown, sv[0], 99); - if (ret != -1) { - fprintf(stderr, "FAIL: shutdown(invalid how) expected failure, got %ld\n", ret); - exit(1); - } - if (errno != EINVAL) { - fail_errno("shutdown(invalid how)", EINVAL); - } + TEST_ASSERT(ret == -1, "shutdown(invalid how)"); + TEST_ASSERT(errno == EINVAL, "shutdown(invalid how)"); close_pair(sv); } diff --git a/litebox_runner_linux_userland/tests/unix_stream_shutdown.c b/litebox_runner_linux_userland/tests/unix_stream_shutdown.c index f45e33f09..c0f6372d5 100644 --- a/litebox_runner_linux_userland/tests/unix_stream_shutdown.c +++ b/litebox_runner_linux_userland/tests/unix_stream_shutdown.c @@ -199,13 +199,8 @@ static void test_listen_shutdown_nonblocking_accept_returns_eagain(void) { errno = 0; int a = accept(fd, NULL, NULL); - if (a != -1) { - fprintf(stderr, "FAIL: nonblocking accept on shut-down listen expected -1, got %d\n", a); - exit(1); - } - if (errno != EAGAIN) { - fail_errno("nonblocking accept on shut-down listen", EAGAIN); - } + TEST_ASSERT(a == -1, "nonblocking accept on shut-down listen"); + TEST_ASSERT(errno == EAGAIN, "nonblocking accept on shut-down listen"); close(fd); unlink(sa.sun_path); @@ -305,13 +300,9 @@ static void test_listen_shutdown_read_preserves_queued_connections(void) { die("socket(refused client)"); } errno = 0; - if (connect(refused_client, (struct sockaddr *)&sa, sizeof(sa)) != -1) { - fprintf(stderr, "FAIL: connect after listen SHUT_RD expected failure\n"); - exit(1); - } - if (errno != ECONNREFUSED) { - fail_errno("connect after listen SHUT_RD", ECONNREFUSED); - } + TEST_ASSERT(connect(refused_client, (struct sockaddr *)&sa, sizeof(sa)) == -1, + "connect after listen SHUT_RD"); + TEST_ASSERT(errno == ECONNREFUSED, "connect after listen SHUT_RD"); int accepted = accept(fd, NULL, NULL); if (accepted < 0) { @@ -320,13 +311,8 @@ static void test_listen_shutdown_read_preserves_queued_connections(void) { errno = 0; int drained = accept(fd, NULL, NULL); - if (drained != -1) { - fprintf(stderr, "FAIL: drained accept after listen SHUT_RD expected -1, got %d\n", drained); - exit(1); - } - if (errno != EAGAIN) { - fail_errno("drained accept after listen SHUT_RD", EAGAIN); - } + TEST_ASSERT(drained == -1, "drained accept after listen SHUT_RD"); + TEST_ASSERT(errno == EAGAIN, "drained accept after listen SHUT_RD"); close(accepted); close(refused_client); diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 05aa45b36..5fce12efa 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -902,6 +902,30 @@ impl Task { .map(|()| 0) }) }), + SyscallRequest::Statx { + dirfd, + pathname, + flags, + mask, + statxbuf, + } => { + let (path, flags) = match pathname { + // Linux 6.11+ treats a NULL statx path as a request to stat dirfd. + None => ( + Ok(c"".into()), + flags | litebox_common_linux::AtFlags::AT_EMPTY_PATH, + ), + Some(p) => (p.to_cstring().ok_or(Errno::EFAULT), flags), + }; + path.and_then(|path| { + self.sys_statx(dirfd, path, flags, mask).and_then(|sx| { + statxbuf + .write_at_offset(0, sx) + .ok_or(Errno::EFAULT) + .map(|()| 0) + }) + }) + } SyscallRequest::Eventfd2 { initval, flags } => { syscall!(sys_eventfd2(initval, flags)) } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 1855504ee..2e18c3623 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -18,7 +18,7 @@ use litebox::{ }; use litebox_common_linux::{ AtFlags, EfdFlags, EpollCreateFlags, FcntlArg, FileDescriptorFlags, FileStat, InodeType, - IoReadVec, IoWriteVec, IoctlArg, TimeParam, errno::Errno, signal::Signal, + IoReadVec, IoWriteVec, IoctlArg, Statx, StatxMask, TimeParam, errno::Errno, signal::Signal, }; use litebox_platform_multiplex::Platform; use thiserror::Error; @@ -932,114 +932,54 @@ impl Task { } } -fn descriptor_stat(raw_fd: usize, task: &Task) -> Result { - let fstat = task - .files - .borrow() +fn descriptor_stat(raw_fd: usize, task: &Task) -> Result +where + T: From + From, +{ + // TODO: give correct values for the synthesized branches. + let synthetic = |mode_bits: u32, blksize: usize| FileStat { + st_dev: 0, + st_ino: 0, + st_nlink: 1, + st_mode: mode_bits.truncate(), + st_uid: 0, + st_gid: 0, + st_rdev: 0, + st_size: 0, + st_blksize: blksize, + st_blocks: 0, + ..Default::default() + }; + let socket_mode = litebox_common_linux::InodeType::Socket as u32 + | (Mode::RWXU | Mode::RWXG | Mode::RWXO).bits(); + let rw_user_mode = (Mode::RUSR | Mode::WUSR).bits(); + let files = task.files.borrow(); + files .run_on_raw_fd( raw_fd, |fd| { - task.files - .borrow() + files .fs .fd_file_status(fd) - .map(FileStat::from) + .map(T::from) .map_err(Errno::from) }, - |_fd| { - Ok(FileStat { - // TODO: give correct values - st_dev: 0, - st_ino: 0, - st_nlink: 1, - st_mode: (litebox_common_linux::InodeType::Socket as u32 - | (Mode::RWXU | Mode::RWXG | Mode::RWXO).bits()) - .truncate(), - st_uid: 0, - st_gid: 0, - st_rdev: 0, - st_size: 0, - st_blksize: 4096, - st_blocks: 0, - ..Default::default() - }) - }, + |_fd| Ok(T::from(synthetic(socket_mode, 4096))), |fd| { let half_pipe_type = task.global.pipes.half_pipe_type(fd)?; let read_write_mode = match half_pipe_type { litebox::pipes::HalfPipeType::SenderHalf => Mode::WUSR, litebox::pipes::HalfPipeType::ReceiverHalf => Mode::RUSR, }; - Ok(FileStat { - // TODO: give correct values - st_dev: 0, - st_ino: 0, - st_nlink: 1, - st_mode: (read_write_mode.bits() - | litebox_common_linux::InodeType::NamedPipe as u32) - .truncate(), - st_uid: 0, - st_gid: 0, - st_rdev: 0, - st_size: 0, - st_blksize: 4096, - st_blocks: 0, - ..Default::default() - }) - }, - |_fd| { - Ok(FileStat { - // TODO: give correct values - st_dev: 0, - st_ino: 0, - st_nlink: 1, - st_mode: (Mode::RUSR | Mode::WUSR).bits().truncate(), - st_uid: 0, - st_gid: 0, - st_rdev: 0, - st_size: 0, - st_blksize: 4096, - st_blocks: 0, - ..Default::default() - }) - }, - |_fd| { - Ok(FileStat { - // TODO: give correct values - st_dev: 0, - st_ino: 0, - st_nlink: 1, - st_mode: (Mode::RUSR | Mode::WUSR).bits().truncate(), - st_uid: 0, - st_gid: 0, - st_rdev: 0, - st_size: 0, - st_blksize: 0, - st_blocks: 0, - ..Default::default() - }) - }, - |_fd| { - Ok(FileStat { - // TODO: give correct values - st_dev: 0, - st_ino: 0, - st_nlink: 1, - st_mode: (litebox_common_linux::InodeType::Socket as u32 - | (Mode::RWXU | Mode::RWXG | Mode::RWXO).bits()) - .truncate(), - st_uid: 0, - st_gid: 0, - st_rdev: 0, - st_size: 0, - st_blksize: 4096, - st_blocks: 0, - ..Default::default() - }) + let pipe_mode = + read_write_mode.bits() | litebox_common_linux::InodeType::NamedPipe as u32; + Ok(T::from(synthetic(pipe_mode, 4096))) }, + |_fd| Ok(T::from(synthetic(rw_user_mode, 4096))), + |_fd| Ok(T::from(synthetic(rw_user_mode, 0))), + |_fd| Ok(T::from(synthetic(socket_mode, 4096))), ) - .flatten()?; - Ok(fstat) + .flatten() } pub(crate) fn get_file_descriptor_flags( @@ -1103,7 +1043,11 @@ 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 { + fn do_stat>( + &self, + pathname: impl path::Arg, + follow_symlink: bool, + ) -> Result { let normalized_path = pathname.normalized()?; let path = if follow_symlink { self.do_readlink(normalized_path.as_str()) @@ -1112,7 +1056,7 @@ impl Task { normalized_path }; let status = self.files.borrow().fs.file_status(path)?; - Ok(FileStat::from(status)) + Ok(T::from(status)) } /// Handle syscall `stat` @@ -1139,39 +1083,83 @@ impl Task { descriptor_stat(raw_fd, self) } - /// Handle syscall `newfstatat` - pub fn sys_newfstatat( + fn do_fstatat( &self, dirfd: i32, pathname: impl path::Arg, flags: AtFlags, - ) -> Result { - let current_support_flags = AtFlags::AT_EMPTY_PATH; - if flags.contains(current_support_flags.complement()) { - log_unsupported!("unsupported flags: {flags:?}"); - return Err(Errno::EINVAL); - } - - let files = self.files.borrow(); + ) -> Result + where + T: From + From, + { let get_cwd = || self.fs.borrow().cwd.read().clone(); let fs_path = FsPath::new(dirfd, pathname, get_cwd)?; - let fstat: FileStat = match fs_path { + match fs_path { FsPath::Absolute { path } => { - self.do_stat(path, !flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW))? + self.do_stat(path, !flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW)) } FsPath::Cwd if flags.contains(AtFlags::AT_EMPTY_PATH) => { - files.fs.file_status(get_cwd())?.into() + Ok(T::from(self.files.borrow().fs.file_status(get_cwd())?)) } FsPath::Fd(fd) if flags.contains(AtFlags::AT_EMPTY_PATH) => { - descriptor_stat(fd as usize, self)? + descriptor_stat(fd as usize, self) } - FsPath::Cwd | FsPath::Fd(_) => return Err(Errno::ENOENT), + FsPath::Cwd | FsPath::Fd(_) => Err(Errno::ENOENT), FsPath::FdRelative { .. } => { log_unsupported!("relative fstatat with AT_EMPTY_PATH unset is not supported yet"); - return Err(Errno::EINVAL); + Err(Errno::EINVAL) } - }; - Ok(fstat) + } + } + + /// Handle syscall `newfstatat` + pub(crate) fn sys_newfstatat( + &self, + dirfd: i32, + pathname: impl path::Arg, + flags: AtFlags, + ) -> Result { + let current_support_flags = AtFlags::AT_EMPTY_PATH; + if flags.intersects(current_support_flags.complement()) { + log_unsupported!("unsupported flags: {flags:?}"); + return Err(Errno::EINVAL); + } + + self.do_fstatat(dirfd, pathname, flags) + } + + /// Handle syscall `statx` + pub(crate) fn sys_statx( + &self, + dirfd: i32, + pathname: impl path::Arg, + flags: AtFlags, + mask: StatxMask, + ) -> Result { + if mask.contains(StatxMask::STATX__RESERVED) { + return Err(Errno::EINVAL); + } + // `AT_NO_AUTOMOUNT` and the `AT_STATX_*` sync + // hints are accepted as no-ops since LiteBox filesystems + // do not automount or sync to a remote. + let allowed = AtFlags::AT_EMPTY_PATH + | AtFlags::AT_NO_AUTOMOUNT + | AtFlags::AT_SYMLINK_NOFOLLOW + | AtFlags::AT_STATX_FORCE_SYNC + | AtFlags::AT_STATX_DONT_SYNC; + if flags.intersects(allowed.complement()) { + log_unsupported!("unsupported statx flags: {flags:?}"); + return Err(Errno::EINVAL); + } + if flags.contains(AtFlags::AT_STATX_FORCE_SYNC | AtFlags::AT_STATX_DONT_SYNC) { + return Err(Errno::EINVAL); + } + + // `mask` is informational past this point: the underlying FS doesn't + // support field selection, so we always fill the basic stats and + // report the actual filled set via `Statx::stx_mask`. Matches Linux's + // documented behavior of returning more than what was asked. + self.do_fstatat(dirfd, pathname, flags) } pub(crate) fn sys_fcntl( @@ -1456,7 +1444,8 @@ impl Task { let (pipe_flags, cloexec) = { use litebox::pipes::Flags; let mut f = Flags::empty(); - if flags.contains((OFlags::CLOEXEC | OFlags::NONBLOCK | OFlags::DIRECT).complement()) { + if flags.intersects((OFlags::CLOEXEC | OFlags::NONBLOCK | OFlags::DIRECT).complement()) + { return Err(Errno::EINVAL); } f.set(Flags::NON_BLOCKING, flags.contains(OFlags::NONBLOCK)); @@ -1518,7 +1507,7 @@ impl Task { pub fn sys_eventfd2(&self, initval: u32, flags: EfdFlags) -> Result { if flags - .contains((EfdFlags::SEMAPHORE | EfdFlags::CLOEXEC | EfdFlags::NONBLOCK).complement()) + .intersects((EfdFlags::SEMAPHORE | EfdFlags::CLOEXEC | EfdFlags::NONBLOCK).complement()) { return Err(Errno::EINVAL); } @@ -1749,7 +1738,7 @@ impl Task { /// Handle syscall `epoll_create` and `epoll_create1` pub fn sys_epoll_create(&self, flags: EpollCreateFlags) -> Result { - if flags.contains(EpollCreateFlags::EPOLL_CLOEXEC.complement()) { + if flags.intersects(EpollCreateFlags::EPOLL_CLOEXEC.complement()) { return Err(Errno::EINVAL); }