From b8869307089661721f1b8baf59e6ff5ea82836c8 Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 7 Aug 2026 20:39:33 -0400 Subject: [PATCH] fix(container): ownership probe uses systemd-run with output capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first drift-gate attempt called plain sudo stat, which the daemon's privilege path doesn't answer — the probe silently failed and the chown loop continued. host_sudo_output mirrors host_sudo (systemd-run --pipe) but returns the process output, so the ownership check gets a real answer. Co-Authored-By: Claude --- .../src/container/prod_orchestrator.rs | 6 +----- core/archipelago/src/update.rs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/core/archipelago/src/container/prod_orchestrator.rs b/core/archipelago/src/container/prod_orchestrator.rs index 37ae6330..fb05df92 100644 --- a/core/archipelago/src/container/prod_orchestrator.rs +++ b/core/archipelago/src/container/prod_orchestrator.rs @@ -362,11 +362,7 @@ fn ownership_already_correct(path: &str, host_uid_gid: &str) -> bool { } async fn ownership_already_correct_from_host(path: &str, host_uid_gid: &str) -> bool { - let Ok(out) = tokio::process::Command::new("sudo") - .args(["stat", "-c", "%u:%g", path]) - .output() - .await - else { + let Ok(out) = crate::update::host_sudo_output(&["stat", "-c", "%u:%g", path]).await else { return false; }; out.status.success() && String::from_utf8_lossy(&out.stdout).trim() == host_uid_gid diff --git a/core/archipelago/src/update.rs b/core/archipelago/src/update.rs index 69a3345d..13e1ee35 100644 --- a/core/archipelago/src/update.rs +++ b/core/archipelago/src/update.rs @@ -1496,6 +1496,25 @@ pub(crate) async fn host_sudo(args: &[&str]) -> Result .context("sudo systemd-run spawn failed") } +/// Same mechanism as `host_sudo` but captures stdout — for read-only probes +/// (e.g. `stat`) where the answer is in the output, not the exit status. +pub(crate) async fn host_sudo_output(args: &[&str]) -> Result { + let mut full: Vec<&str> = vec![ + "systemd-run", + "--wait", + "--quiet", + "--collect", + "--pipe", + "--", + ]; + full.extend_from_slice(args); + tokio::process::Command::new("sudo") + .args(&full) + .output() + .await + .context("sudo systemd-run output spawn failed") +} + /// Apply a downloaded update. Backs up current binaries, replaces with staged versions. pub async fn apply_update(data_dir: &Path) -> Result<()> { let _op = UPDATE_OP_LOCK.try_lock().map_err(|_| {