Fix some behavior about `seek()`

This commit is contained in:
Ruihan Li 2025-11-07 10:07:49 +08:00 committed by Jianfeng Jiang
parent 24502ac3d4
commit 21365dd0bd
10 changed files with 92 additions and 38 deletions

View File

@ -123,7 +123,22 @@ impl HandleInner {
}
pub(self) fn seek(&self, pos: SeekFrom) -> Result<usize> {
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<usize>,
pos: SeekFrom,
end: Option<usize>,
) -> Result<usize> {
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)
}

View File

@ -204,6 +204,11 @@ impl<D: DirOps + 'static> Inode for ProcDir<D> {
fn is_dentry_cacheable(&self) -> bool {
!self.common.is_volatile()
}
fn seek_end(&self) -> Option<usize> {
// Seeking directories under `/proc` with `SEEK_END` will start from zero.
Some(0)
}
}
pub trait DirOps: Sync + Send + Sized {

View File

@ -78,6 +78,7 @@ impl<F: FileOps + 'static> Inode for ProcFile<F> {
fn fs(&self) -> Arc<dyn FileSystem>;
fn resize(&self, _new_size: usize) -> Result<()> {
// Resizing files under `/proc` will succeed, but will do nothing.
Ok(())
}
@ -96,6 +97,11 @@ impl<F: FileOps + 'static> Inode for ProcFile<F> {
fn is_dentry_cacheable(&self) -> bool {
!self.common.is_volatile()
}
fn seek_end(&self) -> Option<usize> {
// Seeking regular files under `/proc` with `SEEK_END` will fail.
None
}
}
pub trait FileOps: Sync + Send {

View File

@ -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<()> {

View File

@ -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<usize> {
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
}

View File

@ -12,20 +12,25 @@ use crate::{
pub fn sys_lseek(fd: FileDesc, offset: isize, whence: u32, ctx: &Context) -> Result<SyscallReturn> {
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: <https://elixir.bootlin.com/linux/v6.17.7/source/include/uapi/linux/fs.h#L52>
#[derive(Clone, Copy, Debug, TryFromInt)]
#[repr(u32)]
#[expect(non_camel_case_types)]
enum SeekType {
SEEK_SET = 0,
SEEK_CUR = 1,
SEEK_END = 2,
}

View File

@ -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);

View File

@ -9,6 +9,7 @@ TESTS ?= \
access_test \
chown_test \
creat_test \
dev_test \
dup_test \
epoll_test \
eventfd_test \

View File

@ -0,0 +1,2 @@
# TODO: Support `/dev/fuse`
DevTest.ReadDevFuseWithoutMount

View File

@ -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