From a7565c49ad13e86fa4ecc5459db3e0e404936d94 Mon Sep 17 00:00:00 2001 From: Sam & Claude Date: Sun, 21 Jun 2026 17:37:32 +0200 Subject: [PATCH] fix(spawner): stage jail spawn files under daemon-owned home, not /var/run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #135. The daemon stages per-spawn launch.sh/env.sh under the jail root; the previous location /var/run/colibri-stage is root-owned, so the daemon (running as clawdie) could not create per-spawn subdirs there — the second jail-spawn EACCES, worked around in #134 by pre-creating the dir in agent-jail-bootstrap.sh. Move the default staging root to the daemon user's home, /home/clawdie/.cache/colibri/stage, which clawdie owns by construction of the jail account. create_dir_all now succeeds with no privileged pre-creation step, and /home is persistent (unlike a tmpfs /var/run). The path is overridable via COLIBRI_JAIL_STAGE_DIR, matching the daemon's other env-configurable paths. - spawner.rs: const → staged_jail_run_dir() resolver; updated unit test. - agent-jail-bootstrap.sh: drop the now-unnecessary install -d staging block and DAEMON_USER var (the #134 workaround). - docs: update jailed-spawn design + truss analysis to the new location. clippy clean; spawner suite green (21 tests); sh -n clean; touched docs pass the markdown gate. Co-Authored-By: Claude Opus 4.8 --- crates/colibri-daemon/src/spawner.rs | 23 ++++++++++++++++++----- docs/COLIBRI-JAILED-AGENT-SPAWN-DESIGN.md | 4 +++- docs/TRUSS-SPAWN-ANALYSIS.md | 8 +++++--- packaging/freebsd/agent-jail-bootstrap.sh | 8 -------- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/crates/colibri-daemon/src/spawner.rs b/crates/colibri-daemon/src/spawner.rs index a0c1d3a..abc0db6 100644 --- a/crates/colibri-daemon/src/spawner.rs +++ b/crates/colibri-daemon/src/spawner.rs @@ -121,7 +121,20 @@ impl PrivMode { /// Default path to the setuid jail-spawn helper used in `PrivMode::Helper`. pub const DEFAULT_JAIL_HELPER: &str = "/usr/local/libexec/colibri-jail-spawn"; -const STAGED_JAIL_RUN_DIR: &str = "/var/run/colibri-stage"; + +/// Jail-relative directory under which the daemon stages per-spawn `launch.sh` / +/// `env.sh` files. It lives under the daemon user's home (`clawdie`) rather than +/// root-owned `/var/run`, so the daemon can create per-spawn subdirs itself with +/// no privileged pre-creation step — and `/home` is persistent, unlike a tmpfs +/// `/var/run`. Overridable via `COLIBRI_JAIL_STAGE_DIR`. +const DEFAULT_JAIL_STAGE_DIR: &str = "/home/clawdie/.cache/colibri/stage"; + +fn staged_jail_run_dir() -> String { + std::env::var("COLIBRI_JAIL_STAGE_DIR") + .ok() + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| DEFAULT_JAIL_STAGE_DIR.to_string()) +} /// Optional FreeBSD jail confinement for a spawned agent. /// @@ -383,7 +396,7 @@ pub async fn prepare_spawn_command( ) })?; - let jail_stage_dir = format!("{STAGED_JAIL_RUN_DIR}/{stage_id}"); + let jail_stage_dir = format!("{}/{stage_id}", staged_jail_run_dir()); let host_stage_dir = Path::new(stage_root).join(jail_stage_dir.trim_start_matches('/')); tokio::fs::create_dir_all(&host_stage_dir).await?; @@ -1030,12 +1043,12 @@ mod jail_tests { assert_eq!(prepared.argv[1], "/bin/sh"); assert_eq!( prepared.argv[2], - "/var/run/colibri-stage/agent-test/launch.sh" + "/home/clawdie/.cache/colibri/stage/agent-test/launch.sh" ); assert!(prepared.env.is_empty()); - let launcher = root.join("var/run/colibri-stage/agent-test/launch.sh"); - let env_file = root.join("var/run/colibri-stage/agent-test/env.sh"); + let launcher = root.join("home/clawdie/.cache/colibri/stage/agent-test/launch.sh"); + let env_file = root.join("home/clawdie/.cache/colibri/stage/agent-test/env.sh"); assert!(launcher.exists()); assert!(env_file.exists()); let env_raw = std::fs::read_to_string(env_file).unwrap(); diff --git a/docs/COLIBRI-JAILED-AGENT-SPAWN-DESIGN.md b/docs/COLIBRI-JAILED-AGENT-SPAWN-DESIGN.md index 0002f37..7087463 100644 --- a/docs/COLIBRI-JAILED-AGENT-SPAWN-DESIGN.md +++ b/docs/COLIBRI-JAILED-AGENT-SPAWN-DESIGN.md @@ -65,7 +65,9 @@ just `jexec`. We choose the escalation per host via `PrivMode` When a jailed spawn needs env vars or a working dir, `prepare_spawn_command()` writes a 0600 `env.sh` (sorted, single-quoted exports) and a `launch.sh` wrapper into a staged directory under the jail's `root_path` at -`/var/run/colibri-stage//`. The jail command runs `/bin/sh launch.sh`, which +`/home/clawdie/.cache/colibri/stage//` (the daemon user's home, so the +daemon creates it with no privileged step; overridable via +`COLIBRI_JAIL_STAGE_DIR`). The jail command runs `/bin/sh launch.sh`, which sources the env file and `cd`s to the working dir before `exec`-ing the agent binary. This bypasses the env-passthrough problem entirely — no reliance on `jexec`/`mdo` inheriting env vars. diff --git a/docs/TRUSS-SPAWN-ANALYSIS.md b/docs/TRUSS-SPAWN-ANALYSIS.md index b72e03c..b3bf34d 100644 --- a/docs/TRUSS-SPAWN-ANALYSIS.md +++ b/docs/TRUSS-SPAWN-ANALYSIS.md @@ -30,9 +30,11 @@ For jailed spawns with environment variables, the daemon's created by a previous run (as root) and was mode 755 root:wheel. The daemon runs as `clawdie` and could not write staging files there. -**Fix:** `chmod 777 /var/run/colibri-stage` (or, better: -`agent-jail-bootstrap.sh` should pre-create this directory with appropriate -ownership). +**Fix (history):** initially `chmod 777 /var/run/colibri-stage`, then +`agent-jail-bootstrap.sh` pre-created it clawdie-owned `0700` (#134). **Final +(#135):** staging moved out of root-owned `/var/run` to the daemon user's home +at `/home/clawdie/.cache/colibri/stage//`, so the daemon creates it itself +with no privileged pre-creation step (overridable via `COLIBRI_JAIL_STAGE_DIR`). ## The Winning Spawn diff --git a/packaging/freebsd/agent-jail-bootstrap.sh b/packaging/freebsd/agent-jail-bootstrap.sh index 7ef579d..a87e3a1 100755 --- a/packaging/freebsd/agent-jail-bootstrap.sh +++ b/packaging/freebsd/agent-jail-bootstrap.sh @@ -11,7 +11,6 @@ set -eu JAIL_NAME="${1:-}" PKG_CACHE_DIR="${PKG_CACHE_DIR:-/var/cache/pkg}" -DAEMON_USER="${DAEMON_USER:-clawdie}" # The jail name becomes a path component, so reject anything that could escape # /usr/local/bastille/jails//root (empty, traversal, odd characters). @@ -98,11 +97,4 @@ if ! grep -q '/etc/profile.d/clawdie-npm.sh' "${JAIL_ROOT}/etc/profile" 2>/dev/n >> "${JAIL_ROOT}/etc/profile" fi -# Pre-create the daemon's per-spawn staging directory. The daemon runs as -# ${DAEMON_USER} and stages launch.sh/env.sh under subdirs here, so -# it must own this directory. Created clawdie-owned 0700 rather than left for a -# root-owned /var/run to block (the spawn EACCES) or patched world-writable. -install -d -o "${DAEMON_USER}" -g "${DAEMON_USER}" -m 0700 \ - "${JAIL_ROOT}/var/run/colibri-stage" - echo "Done — ${JAIL_NAME} ready for vault provision." -- 2.45.3