From 4e2899102851f2ec5fe66673b383b7b330cea7b5 Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Sat, 24 Oct 2026 16:38:49 +0200 Subject: [PATCH 1/3] test: fail a confined or mount test on CI when it cannot run The confined launch tests and the live mount tests return early when the host has no unprivileged namespaces or no FUSE. They then pass, and the run does not show that they did not run. On CI (the CI variable is set) such a test now fails and says why. Elsewhere it still says that it skipped. The ledger tests do the same for PostgreSQL. Co-Authored-By: Claude Opus 5.5 --- crates/memory/tests/launch.rs | 17 +++++++++++++---- crates/mount/tests/live.rs | 22 +++++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/crates/memory/tests/launch.rs b/crates/memory/tests/launch.rs index 6188226..16f8f85 100644 --- a/crates/memory/tests/launch.rs +++ b/crates/memory/tests/launch.rs @@ -205,16 +205,21 @@ fn launch_refuses_without_the_credential_its_mode_needs() { } /// Whether this kernel lets an unprivileged process create user and network namespaces. -/// Ubuntu's AppArmor restriction and some CI kernels say no; the confined test is then -/// reported as skipped rather than failed. +/// Ubuntu's AppArmor restriction says no by default; a confined test is then reported as +/// skipped rather than failed, except on CI, which is set up to run it and fails instead. #[cfg(target_os = "linux")] fn namespaces_available() -> bool { - Command::new("unshare") + let available = Command::new("unshare") .args(["--user", "--map-root-user", "--net", "--mount", "true"]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() - .is_ok_and(|s| s.success()) + .is_ok_and(|s| s.success()); + assert!( + available || std::env::var_os("CI").is_none(), + "unprivileged namespaces are not available, and on CI the confined tests must run" + ); + available } #[cfg(target_os = "linux")] @@ -1079,6 +1084,10 @@ fn a_file_read_inside_the_sandbox_is_a_get_the_memory_serves() { let reads = dm_server::reads::all(&root).unwrap(); let gets: Vec<_> = reads.iter().filter(|r| r.tool == "get").collect(); if stderr.contains("is a render:") { + assert!( + std::env::var_os("CI").is_none(), + "on CI the projection must be a live mount: {stderr}" + ); eprintln!("skipping the rest: {stderr}"); assert!(gets.is_empty(), "no hook, no mount, no get: {gets:?}"); return; diff --git a/crates/mount/tests/live.rs b/crates/mount/tests/live.rs index 5a5c889..c354bb7 100644 --- a/crates/mount/tests/live.rs +++ b/crates/mount/tests/live.rs @@ -1,5 +1,5 @@ //! The mount, live: a memory's render served over FUSE, read with ordinary file calls. -//! Skipped where the host has no FUSE, saying so. +//! Skipped where the host has no FUSE, saying so; on CI that is a failure. // An integration test is test code; a panic here is the failure being reported. #![allow(clippy::unwrap_used)] @@ -30,6 +30,20 @@ async fn rendered(root: &Path) -> (Arc, String, PathBuf) { (m, w.id, render) } +/// Whether this host can serve a FUSE mount. Without it a test says it skipped, except on +/// CI, which is set up to run it and fails instead. +fn fuse_available() -> bool { + let Err(why) = dm_mount::availability() else { + return true; + }; + assert!( + std::env::var_os("CI").is_none(), + "{why}, and on CI the mount tests must run" + ); + eprintln!("skipping: {why}"); + false +} + fn names(dir: &Path) -> Vec { let mut v: Vec = std::fs::read_dir(dir) .unwrap() @@ -42,8 +56,7 @@ fn names(dir: &Path) -> Vec { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn an_open_in_the_mount_is_a_get_the_memory_serves_logs_and_tapes() { - if let Err(why) = dm_mount::availability() { - eprintln!("skipping: {why}"); + if !fuse_available() { return; } let dir = tempfile::tempdir().unwrap(); @@ -124,8 +137,7 @@ async fn an_open_in_the_mount_is_a_get_the_memory_serves_logs_and_tapes() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn the_record_s_word_decides_an_open_not_the_render_s() { - if let Err(why) = dm_mount::availability() { - eprintln!("skipping: {why}"); + if !fuse_available() { return; } let dir = tempfile::tempdir().unwrap(); From d1faf0e75c570f22f11498458ed56f274b5b5ab2 Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Sat, 24 Oct 2026 16:51:54 +0200 Subject: [PATCH 2/3] ci: let the Linux jobs create user namespaces Ubuntu's AppArmor lets an unprivileged process create a user namespace but not map its uid in it. So the confined tests could not run on the runner, and CI skipped them without a message: the previous commit makes all four confined launch tests fail there. The check and coverage jobs now set kernel.apparmor_restrict_unprivileged_userns to 0 before the tests, as the harness spikes workflow does. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 752d61f..6bf81dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,9 @@ jobs: components: rustfmt, clippy targets: wasm32-wasip2, x86_64-pc-windows-gnu - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + # Ubuntu's AppArmor lets an unprivileged process create a user namespace but not map + # its uid in it, so no confined test could run; the runner allows it, as a laptop does + - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - run: make check # the embedded PostgreSQL for the ledger tests lists its releases through GitHub's # API; unauthenticated, a runner's address is soon refused @@ -164,6 +167,8 @@ jobs: - uses: taiki-e/install-action@83ac0ad63c0167e6f06796fab0fce28db1bf3db0 # v2.87.22 with: tool: cargo-llvm-cov + # the confined tests run here too, as in the check job + - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - run: make coverage # the embedded PostgreSQL for the ledger tests lists its releases through GitHub's # API; unauthenticated, a runner's address is soon refused From 51c9ac1001d4b9a3e1b9e977702a3e7e8b121079 Mon Sep 17 00:00:00 2001 From: Sander Striker Date: Sat, 24 Oct 2026 17:30:13 +0200 Subject: [PATCH 3/3] ci: say why the runner turns the restriction off, not the profile The comment said that a laptop allows unprivileged user namespaces. Stock Ubuntu does not: a laptop installs the amem profile in packaging/apparmor. The tests probe with unshare, not with amem, so the profile does not apply to them, and the runner turns the restriction off. The doc comment of the projection test now says that on CI a render is a failure. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 4 +++- crates/memory/tests/launch.rs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bf81dd..07e353e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,9 @@ jobs: targets: wasm32-wasip2, x86_64-pc-windows-gnu - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 # Ubuntu's AppArmor lets an unprivileged process create a user namespace but not map - # its uid in it, so no confined test could run; the runner allows it, as a laptop does + # its uid in it, so no confined test could run. A laptop installs the amem profile in + # packaging/apparmor; the tests' own probe is unshare, not amem, so this throwaway VM + # turns the restriction off instead - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - run: make check # the embedded PostgreSQL for the ledger tests lists its releases through GitHub's diff --git a/crates/memory/tests/launch.rs b/crates/memory/tests/launch.rs index 16f8f85..f03c519 100644 --- a/crates/memory/tests/launch.rs +++ b/crates/memory/tests/launch.rs @@ -1042,7 +1042,8 @@ fn a_kept_tree_is_the_commit_and_repeated_runs_are_not_a_subscription_s() { /// The projection as a live mount: a shell `cat` inside the sandbox, which no hook /// sees, is a `get` the memory serves and logs. Where this host has no FUSE the launch -/// says the projection is a render, and the test says it skipped the rest. +/// says the projection is a render, and the test says it skipped the rest; on CI, which +/// has FUSE, that is a failure. #[cfg(target_os = "linux")] #[test] fn a_file_read_inside_the_sandbox_is_a_get_the_memory_serves() {