From 6c34758a30cdc34c829e9e32272331326bf880ad Mon Sep 17 00:00:00 2001 From: Eureka Date: Sat, 14 Nov 2026 08:21:12 +0000 Subject: [PATCH 1/2] Static release carry: explicit 0444 under UMask 0027 The carry file was created with .mode(0o444), which the service's UMask=0027 masks to 0440; www-data could not read it and the sealed-tree verifier rejected it. Set the permissions on the open handle before the rename, and prove it in a child process under umask 0027. Co-Authored-By: Claude Sonnet 5.5 --- src/drivers.rs | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/drivers.rs b/src/drivers.rs index 46cb916..714c460 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -762,7 +762,7 @@ impl StaticReleasePort for FilesystemStaticRelease { binding: &StaticReleaseBinding, release_id: &str, ) -> Result { - use std::os::unix::fs::{OpenOptionsExt, symlink}; + use std::os::unix::fs::{OpenOptionsExt, PermissionsExt, symlink}; // Everything up to and including the rename can refuse, and only that // refuses: the link has not moved when `FlipRefused` is returned. @@ -823,6 +823,10 @@ impl StaticReleasePort for FilesystemStaticRelease { .create_new(true) .mode(0o444) .open(&temporary_carry)?; + // The creation mode is masked by the service's umask + // (UMask=0027 yields 0440); the sealed-tree verifier accepts + // exactly 0444, so say it on the open handle. + file.set_permissions(fs::Permissions::from_mode(0o444))?; file.write_all(&bytes)?; file.sync_all()?; fs::rename(&temporary_carry, &carried)?; @@ -17045,6 +17049,43 @@ processes 9 Ok(()) } + /// The service runs with UMask=0027, which masks the create mode to 0440 + /// and hides the carry from the web user. The umask is process-global, so + /// the proof runs in a child copy of this test binary and no sibling test + /// ever sees it. + #[cfg(unix)] + #[test] + fn static_release_carry_is_0444_under_the_service_umask() -> Result<()> { + use std::os::unix::fs::PermissionsExt; + const CHILD: &str = "IDUNN_TEST_CARRY_UMASK_CHILD"; + const NAME: &str = "static_release_carry_is_0444_under_the_service_umask"; + if std::env::var_os(CHILD).is_none() { + let path = format!("{}::{NAME}", module_path!().split_once("::").map_or("", |(_, rest)| rest)); + let output = std::process::Command::new(std::env::current_exe()?) + .args(["--exact", path.as_str(), "--nocapture", "--test-threads=1"]) + .env(CHILD, "1") + .output()?; + assert!( + output.status.success(), + "child under umask 0027 failed:\n{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!( + String::from_utf8_lossy(&output.stdout).contains("1 passed"), + "the child filter matched no test:\n{}", + String::from_utf8_lossy(&output.stdout) + ); + return Ok(()); + } + unsafe { libc::umask(0o027) }; + let site = stand_in_site()?; + FilesystemStaticRelease.carry_and_flip(&site.binding, RELEASE_A)?; + let carried = site.binding.current_link.join("config.json"); + assert_eq!(fs::metadata(&carried)?.permissions().mode() & 0o777, 0o444); + Ok(()) + } + #[cfg(unix)] #[test] fn static_release_missing_site_config_fails_before_the_link_moves() -> Result<()> { -- 2.47.3 From d13d98a08342d59ca43b8cc5c31ea6ba44a16026 Mon Sep 17 00:00:00 2001 From: Eureka Date: Sat, 14 Nov 2026 08:44:30 +0000 Subject: [PATCH 2/2] Carry comment: the web server reads the file, no verifier enforces it Co-Authored-By: Claude Sonnet 5.5 --- src/drivers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/drivers.rs b/src/drivers.rs index 714c460..7dfeaee 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -824,8 +824,8 @@ impl StaticReleasePort for FilesystemStaticRelease { .mode(0o444) .open(&temporary_carry)?; // The creation mode is masked by the service's umask - // (UMask=0027 yields 0440); the sealed-tree verifier accepts - // exactly 0444, so say it on the open handle. + // (UMask=0027 yields 0440), and the web server reads this file + // as "other"; say 0444 on the open handle. file.set_permissions(fs::Permissions::from_mode(0o444))?; file.write_all(&bytes)?; file.sync_all()?; -- 2.47.3