Skip to content

Commit

Permalink
feat(tests): add support for EROFS tests (#133)
Browse files Browse the repository at this point in the history
The execution of these tests is conditioned to the `allow_erofs` flag,
since they require to remount the file system as read-only.

---------

Co-authored-by: Alan Somers <asomers@gmail.com>
  • Loading branch information
musikid and asomers committed Feb 25, 2024
1 parent 6827423 commit de9dca1
Show file tree
Hide file tree
Showing 19 changed files with 308 additions and 10 deletions.
4 changes: 4 additions & 0 deletions book/src/configuration-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,12 @@ entries = [
```toml
[settings]
naptime = 0.001
allow_remount = false
```

- `naptime` - The duration for a "short" sleep. It should be greater than the
timestamp granularity of the file system under test. The default value is 1
second.
- `allow_remount` - If set to `true`, the runner will run the EROFS tests,
which require to remount the file system on which
pjdsfstest is run as read-only.
3 changes: 3 additions & 0 deletions rust/pjdfstest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ posix_fallocate = {}
# naptime is the duration of various short sleeps. It should be greater than
# the timestamp granularity of the file system under test.
naptime = 0.001
# Allow to run the EROFS tests, which require to remount the file system on which
# pjdsfstest is run as read-only.
allow_remount = false

# This section allows to modify the mecanism for switching users, which is required by some tests.
# [dummy_auth]
Expand Down
2 changes: 2 additions & 0 deletions rust/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ pub struct FeaturesConfig {
pub struct SettingsConfig {
#[serde(default = "default_naptime")]
pub naptime: f64,
pub allow_remount: bool,
}

impl Default for SettingsConfig {
fn default() -> Self {
SettingsConfig {
naptime: default_naptime(),
allow_remount: false,
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion rust/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use rand::distributions::{Alphanumeric, DistString};
use std::{
cell::Cell,
fs::create_dir_all,
ops::Deref,
ops::{Deref, DerefMut},
os::unix::prelude::RawFd,
panic::{catch_unwind, resume_unwind, AssertUnwindSafe},
path::{Path, PathBuf},
Expand Down Expand Up @@ -93,6 +93,12 @@ impl<'a> Deref for SerializedTestContext<'a> {
}
}

impl<'a> DerefMut for SerializedTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.ctx
}
}

impl<'a> SerializedTestContext<'a> {
pub fn new(config: &'a Config, entries: &'a [DummyAuthEntry], base_dir: &'a Path) -> Self {
Self {
Expand Down
4 changes: 4 additions & 0 deletions rust/src/tests/chflags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use super::{
errors::enametoolong::{enametoolong_comp_test_case, enametoolong_path_test_case},
errors::enoent::{enoent_comp_test_case, enoent_named_file_test_case},
errors::enotdir::enotdir_comp_test_case,
errors::erofs::erofs_named_test_case,
};

//TODO: Split tests with unprivileged tests for user flags
Expand Down Expand Up @@ -276,3 +277,6 @@ fn securelevel(ctx: &mut TestContext, ft: FileType) {

// chflags/13.t
efault_path_test_case!(chflags, |ptr| nix::libc::chflags(ptr, 0));

// chflags/12.t
erofs_named_test_case!(chflags(~path, FileFlag::empty()));
7 changes: 7 additions & 0 deletions rust/src/tests/chmod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use super::errors::{
enoent_comp_test_case, enoent_named_file_test_case, enoent_symlink_named_file_test_case,
},
enotdir::enotdir_comp_test_case,
erofs::erofs_named_test_case,
};

const ALLPERMS_STICKY: mode_t = ALLPERMS | Mode::S_ISVTX.bits();
Expand Down Expand Up @@ -137,6 +138,9 @@ eloop_comp_test_case!(chmod(~path, Mode::empty()));
// chmod/06.t
eloop_final_comp_test_case!(chmod(~path, Mode::empty()));

// chmod/09.t
erofs_named_test_case!(chmod(~path, Mode::empty()));

// chmod/10.t
efault_path_test_case!(chmod, |ptr| nix::libc::chmod(ptr, 0));

Expand Down Expand Up @@ -204,6 +208,9 @@ mod lchmod {
enametoolong_comp_test_case!(lchmod(~path, Mode::empty()));
enametoolong_path_test_case!(lchmod(~path, Mode::empty()));

// chmod/09.t
erofs_named_test_case!(lchmod(~path, Mode::empty()));

// chmod/10.t
// TODO: lchmod is missing in libc
efault_path_test_case!(lchmod, |ptr| nix::libc::fchmodat(
Expand Down
7 changes: 7 additions & 0 deletions rust/src/tests/chown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use super::errors::enoent::{
enoent_comp_test_case, enoent_named_file_test_case, enoent_symlink_named_file_test_case,
};
use super::errors::enotdir::enotdir_comp_test_case;
use super::errors::erofs::erofs_named_test_case;

fn chown_wrapper(ctx: &mut TestContext, path: &std::path::Path) -> nix::Result<()> {
let user = ctx.get_new_user();
Expand Down Expand Up @@ -38,6 +39,9 @@ enametoolong_comp_test_case!(chown, chown_wrapper);
// chown/03.t
enametoolong_path_test_case!(chown, chown_wrapper);

// chown/09.t
erofs_named_test_case!(chown, chown_wrapper);

// chown/10.t
efault_path_test_case!(chown, |ptr| nix::libc::chown(ptr, 0, 0));

Expand All @@ -62,6 +66,9 @@ mod lchown {
enametoolong_comp_test_case!(lchown, lchown_wrapper);
enametoolong_path_test_case!(lchown, lchown_wrapper);

// chown/09.t
erofs_named_test_case!(lchown, lchown_wrapper);

// chown/10.t
efault_path_test_case!(lchown, |ptr| nix::libc::lchown(ptr, 0, 0));
}
3 changes: 2 additions & 1 deletion rust/src/tests/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ pub(super) mod eloop;
pub(super) mod enametoolong;
pub(super) mod enoent;
pub(super) mod enotdir;
pub(super) mod erofs;
pub(super) mod etxtbsy;
pub(super) mod exdev;
pub(super) mod exdev;
182 changes: 182 additions & 0 deletions rust/src/tests/errors/erofs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
use std::{
panic::{catch_unwind, resume_unwind, AssertUnwindSafe},
path::Path,
process::Command,
};

use crate::utils::get_mountpoint;

enum RemountOptions {
ReadOnly,
ReadWrite,
}

/// Guard to allow execution of this test only if it's allowed to run.
pub(crate) fn can_run_erofs(conf: &crate::config::Config, _: &Path) -> anyhow::Result<()> {
if !conf.settings.allow_remount {
anyhow::bail!("Remounts (allow_remount) are not allowed in the configuration file")
}

Ok(())
}

/// Remount the file system mounted at `mountpoint` with the provided options.
fn remount<P: AsRef<Path>>(mountpoint: P, options: RemountOptions) -> Result<(), anyhow::Error> {
if mountpoint.as_ref().parent().is_none() {
anyhow::bail!("Cannot remount root file system")
}

let opt = match options {
RemountOptions::ReadOnly => "ro",
RemountOptions::ReadWrite => "rw",
};

#[cfg(target_os = "linux")]
let opt = format!("remount,{opt}");

let mut cmd = Command::new("mount");

#[cfg(any(
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
))]
cmd.arg("-u");

// XXX: It is possible to do it directly with mount(2) but we use the CLI for now
let mount_result = cmd.arg("-o").arg(opt).arg(mountpoint.as_ref()).output()?;

if !mount_result.status.success() {
anyhow::bail!(
"Failed to remount: {}",
String::from_utf8_lossy(&mount_result.stderr)
)
}

Ok(())
}

/// Execute a function with a read-only file system and restore read/write flags after.
// TODO: Get the original flags
pub(crate) fn with_readonly_fs<F, P: AsRef<Path>>(path: P, f: F)
where
F: FnOnce(),
{
let mountpoint = get_mountpoint(path.as_ref()).unwrap();

remount(mountpoint, RemountOptions::ReadOnly).unwrap();

let res = catch_unwind(AssertUnwindSafe(f));

remount(mountpoint, RemountOptions::ReadWrite).unwrap();

if let Err(e) = res {
resume_unwind(e);
}
}

/// Create a test case which asserts that the syscall returns EROFS
/// if the path resides on a read-only file system.
/// There are multiple forms for this macro:
///
/// - A basic form which takes the syscall, and optionally a `~path` argument
/// to indicate where the `path` argument should be substituted if the path
/// is not the only argument taken by the syscall.
///
/// ```
/// // `unlink` accepts only a path as argument.
/// erofs_new_file_test_case!(unlink);
/// // `chflags` takes a path and the flags to set as arguments.
/// // We need to add `~path` where the path argument should normally be taken.
/// erofs_new_file_test_case!(chflags(~path, FileFlags::empty()));
/// ```
///
/// - A more complex form which takes multiple functions
/// with the context and the path as arguments for syscalls
/// requring to compute other arguments.
///
/// ```
/// erofs_new_file_test_case!(chown, |ctx: &mut TestContext, path: &Path| {
/// let user = ctx.get_new_user();
/// chown(path, Some(user.uid), None)
/// })
/// ```
macro_rules! erofs_new_file_test_case {
($syscall: ident, $($f: expr),+) => {
crate::test_case! {
#[doc = concat!(stringify!($syscall),
" returns EROFS if the path for the file to be created resides on a read-only file system")]
erofs_new_file, serialized, root; crate::tests::errors::erofs::can_run_erofs
}
fn erofs_new_file(ctx: &mut crate::context::SerializedTestContext) {
use crate::tests::errors::erofs::with_readonly_fs;
let path = ctx.base_path().to_owned();
let file = ctx.gen_path();
with_readonly_fs(path, || {
$( assert_eq!($f(ctx, &file), Err(nix::errno::Errno::EROFS)); )+
});
}
};

($syscall: ident $( ($( $($before:expr),* ,)? ~path $(, $($after:expr),*)?) )?) => {
crate::tests::errors::erofs::erofs_new_file_test_case!($syscall, |_ctx, path: &std::path::Path| {
$syscall($( $($($before),* ,)? )? path $( $(, $($after),*)? )?)
});
};
}

pub(crate) use erofs_new_file_test_case;

/// Create a test case which asserts that the syscall returns EROFS
/// if the named file resides on a read-only file system.
/// There are multiple forms for this macro:
///
/// - A basic form which takes the syscall, and optionally a `~path` argument
/// to indicate where the `path` argument should be substituted if the path
/// is not the only argument taken by the syscall.
///
/// ```
/// // `unlink` accepts only a path as argument.
/// erofs_test_case!(unlink);
/// // `chflags` takes a path and the flags to set as arguments.
/// // We need to add `~path` where the path argument should normally be taken.
/// erofs_test_case!(chflags(~path, FileFlags::empty()));
/// ```
///
/// - A more complex form which takes multiple functions
/// with the context and the path as arguments for syscalls
/// requring to compute other arguments.
///
/// ```
/// erofs_test_case!(chown, |ctx: &mut TestContext, path: &Path| {
/// let user = ctx.get_new_user();
/// chown(path, Some(user.uid), None)
/// })
/// ```
macro_rules! erofs_named_test_case {
($syscall: ident, $($f: expr),+) => {
crate::test_case! {
#[doc = concat!(stringify!($syscall),
" returns EROFS if the named file resides on a read-only file system")]
erofs_named, serialized, root; crate::tests::errors::erofs::can_run_erofs
}
fn erofs_named(ctx: &mut crate::context::SerializedTestContext) {
use crate::tests::errors::erofs::with_readonly_fs;
use crate::context::FileType;
let path = ctx.base_path().to_owned();
let file = ctx.new_file(FileType::Regular).name(path.join("file")).create().unwrap();
with_readonly_fs(path, || {
$( assert_eq!($f(ctx, &file), Err(nix::errno::Errno::EROFS)); )+
});
}
};

($syscall: ident $( ($( $($before:expr),* ,)? ~path $(, $($after:expr),*)?) )?) => {
crate::tests::errors::erofs::erofs_named_test_case!($syscall, |_ctx, path: &std::path::Path| {
$syscall($( $($($before),* ,)? )? path $( $(, $($after),*)? )?)
});
};
}

pub(crate) use erofs_named_test_case;
7 changes: 7 additions & 0 deletions rust/src/tests/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use super::{
efault::efault_either_test_case,
eloop::eloop_either_test_case,
enametoolong::{enametoolong_either_comp_test_case, enametoolong_either_path_test_case},
erofs::erofs_named_test_case,
exdev::exdev_target_test_case,
},
CTIME, MTIME,
Expand Down Expand Up @@ -212,6 +213,12 @@ enoent_either_named_file_test_case!(link);
// link/08.t
eloop_either_test_case!(link);

// link/16.t
erofs_named_test_case!(link, |ctx: &mut TestContext, file| {
let path = ctx.gen_path();
link(file, &path)
});

// link/09.t
crate::test_case! {
/// link returns ENOENT if the source file does not exist
Expand Down
4 changes: 4 additions & 0 deletions rust/src/tests/mkdir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use super::errors::efault::efault_path_test_case;
use super::errors::eloop::eloop_comp_test_case;
use super::errors::enametoolong::{enametoolong_comp_test_case, enametoolong_path_test_case};
use super::errors::enoent::enoent_comp_test_case;
use super::errors::erofs::erofs_new_file_test_case;
use super::mksyscalls::{assert_perms_from_mode_and_umask, assert_uid_gid};
use super::{assert_times_changed, errors::enotdir::enotdir_comp_test_case, ATIME, CTIME, MTIME};

Expand Down Expand Up @@ -64,6 +65,9 @@ enoent_comp_test_case!(mkdir(~path, Mode::empty()));
// mkdir/07.t
eloop_comp_test_case!(mkdir(~path, Mode::empty()));

// mkdir/09.t
erofs_new_file_test_case!(mkdir(~path, Mode::empty()));

// mkdir/10.t
eexist_file_exists_test_case!(mkdir(~path, Mode::empty()));

Expand Down
4 changes: 4 additions & 0 deletions rust/src/tests/mkfifo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use super::errors::eloop::eloop_comp_test_case;
use super::errors::enametoolong::{enametoolong_comp_test_case, enametoolong_path_test_case};
use super::errors::enoent::enoent_comp_test_case;
use super::errors::enotdir::enotdir_comp_test_case;
use super::errors::erofs::erofs_new_file_test_case;
use super::mksyscalls::{assert_perms_from_mode_and_umask, assert_uid_gid};
use super::{assert_times_changed, ATIME, CTIME, MTIME};

Expand Down Expand Up @@ -66,6 +67,9 @@ enoent_comp_test_case!(mkfifo(~path, Mode::empty()));
// mkfifo/07.t
eloop_comp_test_case!(mkfifo(~path, Mode::empty()));

// mkfifo/08.t
erofs_new_file_test_case!(mkfifo(~path, Mode::empty()));

// mkfifo/09.t
eexist_file_exists_test_case!(mkfifo(~path, Mode::empty()));

Expand Down

0 comments on commit de9dca1

Please sign in to comment.