From 656ca6c2595af516d03c6ed4d9887885adf36bed Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Wed, 26 Aug 2026 08:35:26 +0100 Subject: [PATCH] fix(api): skip mode-bit tests when they cannot bind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four tests force a filesystem failure by freezing a directory to 0o555. The CI container runs as root, mode bits do not constrain root, and the rename those tests expect to fail succeeds — main has been red on a_failed_rename_leaves_the_row_alone since the move-on-root-change work landed, with nextest's fail-fast hiding the other three. The guard probes the filesystem rather than the uid: what the tests depend on is the refusal, and a container can hold CAP_DAC_OVERRIDE without being uid 0. --- crates/arr-api/src/lib.rs | 2 ++ crates/arr-api/src/movies.rs | 4 ++++ crates/arr-api/src/privilege.rs | 36 +++++++++++++++++++++++++++++++++ crates/arr-api/src/roots.rs | 21 +++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 crates/arr-api/src/privilege.rs diff --git a/crates/arr-api/src/lib.rs b/crates/arr-api/src/lib.rs index e6d315a..a147625 100644 --- a/crates/arr-api/src/lib.rs +++ b/crates/arr-api/src/lib.rs @@ -12,6 +12,8 @@ mod metadata; mod movies; mod owners; mod policies; +#[cfg(test)] +mod privilege; mod reclassify; mod relocate; mod roots; diff --git a/crates/arr-api/src/movies.rs b/crates/arr-api/src/movies.rs index 96577e9..b143e88 100644 --- a/crates/arr-api/src/movies.rs +++ b/crates/arr-api/src/movies.rs @@ -1944,6 +1944,10 @@ mod tests { async fn a_failed_rename_leaves_the_row_alone() { use std::os::unix::fs::PermissionsExt; + if !crate::privilege::mode_bits_bind().await { + crate::privilege::skipped_because_privileged("a_failed_rename_leaves_the_row_alone"); + return; + } let (_dir, state, base) = application().await; let movie = add_movie(&base, 693_134, 1).await; let id = movie["id"].as_i64().expect("id"); diff --git a/crates/arr-api/src/privilege.rs b/crates/arr-api/src/privilege.rs new file mode 100644 index 0000000..673e0bd --- /dev/null +++ b/crates/arr-api/src/privilege.rs @@ -0,0 +1,36 @@ +//! Whether mode bits actually constrain this process. +//! +//! Four tests force a filesystem failure by freezing a directory to `0o555` +//! and asserting the handler reports it. Mode bits do not constrain a +//! privileged user, so under `root` — which is what the CI container runs +//! as — the operation succeeds and the assertion fails for a reason that has +//! nothing to do with the code under test. +//! +//! The probe asks the filesystem rather than asking for the uid: what the +//! tests depend on is the refusal, not the identity, and a container can +//! hold `CAP_DAC_OVERRIDE` without being uid 0. + +/// True when a read-only directory refuses a write to this process. +pub(crate) async fn mode_bits_bind() -> bool { + use std::os::unix::fs::PermissionsExt; + + let probe = tempfile::tempdir().expect("probe root"); + let frozen = probe.path().join("frozen"); + tokio::fs::create_dir(&frozen).await.expect("probe folder"); + tokio::fs::set_permissions(&frozen, std::fs::Permissions::from_mode(0o555)) + .await + .expect("freeze the probe folder"); + let refused = tokio::fs::write(frozen.join("probe"), b"x").await.is_err(); + // `TempDir::drop` needs the write back to remove the tree + tokio::fs::set_permissions(&frozen, std::fs::Permissions::from_mode(0o755)) + .await + .expect("thaw the probe folder"); + refused +} + +/// Announces a test that cannot run here, on stderr, which `nextest` prints +/// when the run is given `--no-capture` and swallows otherwise — the same +/// deal every other skipped case in a Rust suite gets. +pub(crate) fn skipped_because_privileged(test: &str) { + eprintln!("{test}: skipped — this process overrides mode bits, so the failure it forces cannot happen"); +} diff --git a/crates/arr-api/src/roots.rs b/crates/arr-api/src/roots.rs index df9ce47..291473c 100644 --- a/crates/arr-api/src/roots.rs +++ b/crates/arr-api/src/roots.rs @@ -1048,6 +1048,13 @@ mod tests { async fn one_folder_that_cannot_move_puts_the_others_back() { use std::os::unix::fs::PermissionsExt; + if !crate::privilege::mode_bits_bind().await { + crate::privilege::skipped_because_privileged( + "one_folder_that_cannot_move_puts_the_others_back", + ); + return; + } + let (_dir, state, base) = application().await; let old = tempfile::tempdir().expect("old root"); let home = tempfile::tempdir().expect("home of the new path"); @@ -1125,6 +1132,13 @@ mod tests { async fn a_new_root_that_already_existed_survives_a_failed_move() { use std::os::unix::fs::PermissionsExt; + if !crate::privilege::mode_bits_bind().await { + crate::privilege::skipped_because_privileged( + "a_new_root_that_already_existed_survives_a_failed_move", + ); + return; + } + let (_dir, state, base) = application().await; let old = tempfile::tempdir().expect("old root"); let new = tempfile::tempdir().expect("new root, already there"); @@ -1393,6 +1407,13 @@ mod tests { async fn a_failed_move_removes_every_level_it_created() { use std::os::unix::fs::PermissionsExt; + if !crate::privilege::mode_bits_bind().await { + crate::privilege::skipped_because_privileged( + "a_failed_move_removes_every_level_it_created", + ); + return; + } + let (_dir, state, base) = application().await; let old = tempfile::tempdir().expect("old root"); let home = tempfile::tempdir().expect("home of the new path");