From eb8aefc0fb17b74a22b3a2243c2a9f5f8efdc525 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 16:43:59 +0200 Subject: [PATCH] mind-body: directories the writer creates are 0755 ancestors included; the probe reports its sweeps Under the binary's umask 077 a first write would have left /etc/gamecult/minds 0700 and unreadable by the units. The attacker reports how much it swept. Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/src/control.rs | 11 ++++++++++- crates/mind-body/tests/control.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/crates/mind-body/src/control.rs b/crates/mind-body/src/control.rs index f42dfc4..db3ad98 100644 --- a/crates/mind-body/src/control.rs +++ b/crates/mind-body/src/control.rs @@ -1,4 +1,13 @@ -//! The operator's brake and burn-rate dial, and the one reader of them. + // Every directory this write creates is 0755, ancestors included: a + // restrictive process umask would otherwise leave /etc/gamecult/minds + // untraversable by the units that only read. + let missing: Vec<&Path> = directory.ancestors().take_while(|ancestor| !ancestor.exists()).collect(); + if !missing.is_empty() { + std::fs::create_dir_all(directory).with_context(|| format!("failed to create {}", directory.display()))?; + for created in missing { + set_mode(created, DIR_MODE)?; + } + }//! The operator's brake and burn-rate dial, and the one reader of them. //! //! State is a CultCache single-file store at //! `/etc/gamecult/minds//control.cc`, root-owned and world-readable. diff --git a/crates/mind-body/tests/control.rs b/crates/mind-body/tests/control.rs index 8896b22..1d9131e 100644 --- a/crates/mind-body/tests/control.rs +++ b/crates/mind-body/tests/control.rs @@ -364,6 +364,7 @@ mod modes { // Spins on an open-for-write of the live store; any success is a hit. let stop = std::env::var("MIND_BODY_STOP").unwrap(); let started = std::time::Instant::now(); + let (mut sweeps, mut seen) = (0u64, 0usize); while !Path::new(&stop).exists() && started.elapsed() < Duration::from_secs(120) { // The live store, and anything else in its directory (the staged // file CultCache writes before its rename is the wider window). @@ -372,6 +373,8 @@ mod modes { if let Ok(entries) = std::fs::read_dir(path.parent().unwrap()) { targets.extend(entries.flatten().map(|entry| entry.path())); } + sweeps += 1; + seen = seen.max(targets.len()); for target in targets { if std::fs::OpenOptions::new().write(true).open(&target).is_ok() { std::process::exit(3); @@ -379,6 +382,7 @@ mod modes { } } } + eprintln!("attacker: {sweeps} sweeps, at most {seen} entries seen"); } "intruder" => { assert!(ControlWriter::open(&path).and_then(|mut writer| writer.set_brake(false, at(1), "intruder")).is_err()); @@ -577,3 +581,27 @@ mod unsafe_installs { assert_eq!(attacker_status.code(), Some(0), "a non-root process opened the live store for write"); } } + +#[cfg(unix)] +#[test] +fn directories_the_writer_creates_are_0755_ancestors_included_under_a_restrictive_umask() { + use std::os::unix::fs::PermissionsExt; + let dir = tempfile::tempdir().unwrap(); + let mode = |path: &Path| std::fs::metadata(path).unwrap().permissions().mode() & 0o777; + let top = dir.path().join("gamecult"); + let path = top.join("minds").join("eureka").join("control.cc"); + // Create nothing but run the writer; the umask is the test process's own, so + // the child role does the write under 077. + let status = std::process::Command::new("sh") + .args(["-c", "umask 077; exec \"$@\" --exact modes::mode_child --nocapture", "sh"]) + .arg(std::env::current_exe().unwrap()) + .env("MIND_BODY_MODE_CHILD", "brake") + .env("MIND_BODY_MODE_ROOT", top.join("minds")) + .status() + .unwrap(); + assert!(status.success()); + assert!(path.exists()); + for created in [&top, &top.join("minds"), &top.join("minds").join("eureka")] { + assert_eq!(mode(created), 0o755, "{}", created.display()); + } +}