diff --git a/kernel/src/fs/inode_handle/mod.rs b/kernel/src/fs/inode_handle/mod.rs index 15dc128f4..7d71d8ee7 100644 --- a/kernel/src/fs/inode_handle/mod.rs +++ b/kernel/src/fs/inode_handle/mod.rs @@ -123,7 +123,22 @@ impl HandleInner { } pub(self) fn seek(&self, pos: SeekFrom) -> Result { - do_seek_util(self.path.inode().as_ref(), &self.offset, pos) + if let Some(ref file_io) = self.file_io { + file_io.check_seekable()?; + if file_io.is_offset_aware() { + // TODO: Figure out whether we need to add support for seeking from the end of + // special files. + return do_seek_util(&self.offset, pos, None); + } else { + return Ok(0); + } + } + + let inode = self.path.inode(); + if !inode.type_().is_seekable() { + return_errno_with_message!(Errno::ESPIPE, "seek is not supported"); + } + do_seek_util(&self.offset, pos, inode.seek_end()) } pub(self) fn offset(&self) -> usize { @@ -292,6 +307,10 @@ pub trait FileIo: Pollable + InodeIo + Send + Sync + 'static { fn check_seekable(&self) -> Result<()>; /// Returns whether the `read()`/`write()` operation should use and advance the offset. + /// + /// If [`is_seekable`] succeeds but this method returns `false`, the offset in the `seek()` + /// operation will be ignored. In that case, the `seek()` operation will do nothing but + /// succeed. fn is_offset_aware(&self) -> bool; // See `FileLike::mappable`. @@ -305,34 +324,33 @@ pub trait FileIo: Pollable + InodeIo + Send + Sync + 'static { } pub(super) fn do_seek_util( - inode: &dyn Inode, offset: &Mutex, pos: SeekFrom, + end: Option, ) -> Result { let mut offset = offset.lock(); - let new_offset: isize = match pos { - SeekFrom::Start(off /* as usize */) => { - if off > isize::MAX as usize { - return_errno_with_message!(Errno::EINVAL, "file offset is too large"); + + let new_offset = match pos { + SeekFrom::Start(off) => off, + SeekFrom::End(diff) => { + if let Some(end) = end { + end.wrapping_add_signed(diff) + } else { + return_errno_with_message!( + Errno::EINVAL, + "seeking the file from the end is not supported" + ); } - off as isize } - SeekFrom::End(off /* as isize */) => { - let file_size = inode.size() as isize; - assert!(file_size >= 0); - file_size - .checked_add(off) - .ok_or_else(|| Error::with_message(Errno::EOVERFLOW, "file offset overflow"))? - } - SeekFrom::Current(off /* as isize */) => (*offset as isize) - .checked_add(off) - .ok_or_else(|| Error::with_message(Errno::EOVERFLOW, "file offset overflow"))?, + SeekFrom::Current(diff) => offset.wrapping_add_signed(diff), }; - if new_offset < 0 { - return_errno_with_message!(Errno::EINVAL, "file offset must not be negative"); + + // Invariant: `*offset <= isize::MAX as usize`. + // TODO: Investigate whether `read`/`write` can break this invariant. + if new_offset.cast_signed() < 0 { + return_errno_with_message!(Errno::EINVAL, "the file offset cannot be negative"); } - // Invariant: 0 <= new_offset <= isize::MAX - let new_offset = new_offset as usize; + *offset = new_offset; Ok(new_offset) } diff --git a/kernel/src/fs/procfs/template/dir.rs b/kernel/src/fs/procfs/template/dir.rs index a70967483..00c795465 100644 --- a/kernel/src/fs/procfs/template/dir.rs +++ b/kernel/src/fs/procfs/template/dir.rs @@ -204,6 +204,11 @@ impl Inode for ProcDir { fn is_dentry_cacheable(&self) -> bool { !self.common.is_volatile() } + + fn seek_end(&self) -> Option { + // Seeking directories under `/proc` with `SEEK_END` will start from zero. + Some(0) + } } pub trait DirOps: Sync + Send + Sized { diff --git a/kernel/src/fs/procfs/template/file.rs b/kernel/src/fs/procfs/template/file.rs index fd587ab14..f0e3571ef 100644 --- a/kernel/src/fs/procfs/template/file.rs +++ b/kernel/src/fs/procfs/template/file.rs @@ -78,6 +78,7 @@ impl Inode for ProcFile { fn fs(&self) -> Arc; fn resize(&self, _new_size: usize) -> Result<()> { + // Resizing files under `/proc` will succeed, but will do nothing. Ok(()) } @@ -96,6 +97,11 @@ impl Inode for ProcFile { fn is_dentry_cacheable(&self) -> bool { !self.common.is_volatile() } + + fn seek_end(&self) -> Option { + // Seeking regular files under `/proc` with `SEEK_END` will fail. + None + } } pub trait FileOps: Sync + Send { diff --git a/kernel/src/fs/ramfs/memfd.rs b/kernel/src/fs/ramfs/memfd.rs index 406e65438..0eec5ac38 100644 --- a/kernel/src/fs/ramfs/memfd.rs +++ b/kernel/src/fs/ramfs/memfd.rs @@ -433,7 +433,7 @@ impl FileLike for MemfdFile { return_errno_with_message!(Errno::EBADF, "the file is opened as a path"); } - do_seek_util(self.memfd_inode.as_ref(), &self.offset, pos) + do_seek_util(&self.offset, pos, Some(self.memfd_inode.size())) } fn fallocate(&self, mode: FallocMode, offset: usize, len: usize) -> Result<()> { diff --git a/kernel/src/fs/utils/inode.rs b/kernel/src/fs/utils/inode.rs index 089e1c29d..20c880202 100644 --- a/kernel/src/fs/utils/inode.rs +++ b/kernel/src/fs/utils/inode.rs @@ -389,7 +389,23 @@ pub trait Inode: Any + InodeIo + Send + Sync { true } - /// Get the extension of this inode + /// Returns the end position for [`SeekFrom::End`]. + /// + /// [`SeekFrom::End`]: super::SeekFrom::End + fn seek_end(&self) -> Option { + if self.type_() == InodeType::File { + Some(self.size()) + } else { + // This depends on the file system. For example, seeking directories from the end + // succeeds under procfs and btrfs but fails under tmpfs. Here, we just choose a + // safe default to reject it. + // TODO: Carefully check the Linux behavior of each file system and adjust ours + // accordingly. + None + } + } + + /// Gets the extension of this inode. fn extension(&self) -> Option<&Extension> { None } diff --git a/kernel/src/syscall/lseek.rs b/kernel/src/syscall/lseek.rs index 148b25e30..8524b1cbe 100644 --- a/kernel/src/syscall/lseek.rs +++ b/kernel/src/syscall/lseek.rs @@ -12,20 +12,25 @@ use crate::{ pub fn sys_lseek(fd: FileDesc, offset: isize, whence: u32, ctx: &Context) -> Result { debug!("fd = {}, offset = {}, whence = {}", fd, offset, whence); - let seek_from = match whence { - 0 => { - if offset < 0 { - return_errno!(Errno::EINVAL); - } - SeekFrom::Start(offset as usize) - } - 1 => SeekFrom::Current(offset), - 2 => SeekFrom::End(offset), - _ => return_errno!(Errno::EINVAL), + let seek_from = match SeekType::try_from(whence)? { + SeekType::SEEK_SET => SeekFrom::Start(offset.cast_unsigned()), + SeekType::SEEK_CUR => SeekFrom::Current(offset), + SeekType::SEEK_END => SeekFrom::End(offset), }; + let mut file_table = ctx.thread_local.borrow_file_table_mut(); let file = get_file_fast!(&mut file_table, fd); let offset = file.seek(seek_from)?; Ok(SyscallReturn::Return(offset as _)) } + +// Reference: +#[derive(Clone, Copy, Debug, TryFromInt)] +#[repr(u32)] +#[expect(non_camel_case_types)] +enum SeekType { + SEEK_SET = 0, + SEEK_CUR = 1, + SEEK_END = 2, +} diff --git a/test/src/apps/file_io/access_err.c b/test/src/apps/file_io/access_err.c index bfaf22872..e1a3882ab 100644 --- a/test/src/apps/file_io/access_err.c +++ b/test/src/apps/file_io/access_err.c @@ -52,6 +52,7 @@ FN_TEST(readable) TEST_SUCC(read(fd, buf, sizeof(buf))); TEST_ERRNO(write(fd, buf, sizeof(buf)), EBADF); TEST_SUCC(lseek(fd, 0, SEEK_SET)); + TEST_SUCC(lseek(fd, 0, SEEK_END)); TEST_ERRNO(ioctl(fd, TCGETS), ENOTTY); TEST_ERRNO(ftruncate(fd, 1), EINVAL); TEST_ERRNO(fallocate(fd, FALLOC_FL_KEEP_SIZE, 0, 1), EBADF); @@ -86,6 +87,7 @@ FN_TEST(readable) TEST_ERRNO(read(fd, buf, sizeof(buf)), EISDIR); TEST_ERRNO(write(fd, buf, sizeof(buf)), EBADF); TEST_SUCC(lseek(fd, 0, SEEK_SET)); + TEST_ERRNO(lseek(fd, 0, SEEK_END), EINVAL); TEST_ERRNO(ioctl(fd, TCGETS), ENOTTY); TEST_ERRNO(ftruncate(fd, 1), EINVAL); TEST_ERRNO(fallocate(fd, FALLOC_FL_KEEP_SIZE, 0, 1), EBADF); @@ -126,6 +128,7 @@ FN_TEST(writeable) TEST_ERRNO(read(fd, buf, sizeof(buf)), EBADF); TEST_SUCC(write(fd, buf, sizeof(buf))); TEST_SUCC(lseek(fd, 0, SEEK_SET)); + TEST_SUCC(lseek(fd, 0, SEEK_END)); TEST_ERRNO(ioctl(fd, TCGETS), ENOTTY); TEST_SUCC(ftruncate(fd, 1)); TEST_SUCC(fallocate(fd, FALLOC_FL_KEEP_SIZE, 0, 1)); @@ -171,6 +174,7 @@ FN_TEST(path) TEST_ERRNO(read(fd, buf, sizeof(buf)), EBADF); TEST_ERRNO(write(fd, buf, sizeof(buf)), EBADF); TEST_ERRNO(lseek(fd, 0, SEEK_SET), EBADF); + TEST_ERRNO(lseek(fd, 0, SEEK_END), EBADF); TEST_ERRNO(ioctl(fd, TCGETS), EBADF); TEST_ERRNO(ftruncate(fd, 1), EBADF); TEST_ERRNO(fallocate(fd, FALLOC_FL_KEEP_SIZE, 0, 1), EBADF); @@ -203,6 +207,7 @@ FN_TEST(path) TEST_ERRNO(read(fd, buf, sizeof(buf)), EBADF); TEST_ERRNO(write(fd, buf, sizeof(buf)), EBADF); TEST_ERRNO(lseek(fd, 0, SEEK_SET), EBADF); + TEST_ERRNO(lseek(fd, 0, SEEK_END), EBADF); TEST_ERRNO(ioctl(fd, TCGETS), EBADF); TEST_ERRNO(ftruncate(fd, 1), EBADF); TEST_ERRNO(fallocate(fd, FALLOC_FL_KEEP_SIZE, 0, 1), EBADF); diff --git a/test/src/syscall/gvisor/Makefile b/test/src/syscall/gvisor/Makefile index 323c7c218..ce3396013 100644 --- a/test/src/syscall/gvisor/Makefile +++ b/test/src/syscall/gvisor/Makefile @@ -9,6 +9,7 @@ TESTS ?= \ access_test \ chown_test \ creat_test \ + dev_test \ dup_test \ epoll_test \ eventfd_test \ diff --git a/test/src/syscall/gvisor/blocklists/dev_test b/test/src/syscall/gvisor/blocklists/dev_test new file mode 100644 index 000000000..39dd94695 --- /dev/null +++ b/test/src/syscall/gvisor/blocklists/dev_test @@ -0,0 +1,2 @@ +# TODO: Support `/dev/fuse` +DevTest.ReadDevFuseWithoutMount diff --git a/test/src/syscall/gvisor/blocklists/lseek_test b/test/src/syscall/gvisor/blocklists/lseek_test index 73841810d..0bd7b0437 100644 --- a/test/src/syscall/gvisor/blocklists/lseek_test +++ b/test/src/syscall/gvisor/blocklists/lseek_test @@ -1,6 +1,2 @@ -LseekTest.Overflow -LseekTest.ProcFile +# TODO: Add `/sys/devices` and support `lseek(SEEK_END)`. LseekTest.SysDir -LseekTest.SeekCurrentDir -LseekTest.ProcStatTwice -LseekTest.EtcPasswdDup \ No newline at end of file