From 26d5cfbdb42290d2b411417f1ebba71e1cfa7106 Mon Sep 17 00:00:00 2001 From: John Myers Date: Thu, 23 Apr 2026 15:52:36 -0700 Subject: [PATCH] fix(sandbox): route console logs to stderr Signed-off-by: John Myers --- architecture/sandbox-custom-containers.md | 2 +- crates/openshell-sandbox/src/main.rs | 14 +++---- .../openshell-sandbox/tests/stdout_logging.rs | 41 +++++++++++++++++++ 3 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 crates/openshell-sandbox/tests/stdout_logging.rs diff --git a/architecture/sandbox-custom-containers.md b/architecture/sandbox-custom-containers.md index cbdc9a666..5d482ffe0 100644 --- a/architecture/sandbox-custom-containers.md +++ b/architecture/sandbox-custom-containers.md @@ -96,7 +96,7 @@ openshell sandbox create --from ./my-sandbox/ # directory with Dockerfile The `openshell-sandbox` supervisor adapts to arbitrary environments: -- **Log file fallback**: Attempts to open `/var/log/openshell.log` for append; silently falls back to stdout-only logging if the path is not writable. +- **Log file fallback**: Attempts to open `/var/log/openshell.log` for append; if the path is not writable, the supervisor keeps console shorthand logging on stderr only. - **Command resolution**: Executes the command from CLI args, then the `OPENSHELL_SANDBOX_COMMAND` env var (set to `sleep infinity` by the server), then `/bin/bash` as a last resort. - **Startup seccomp prelude**: Before parsing CLI args or starting the async runtime, the supervisor sets `PR_SET_NO_NEW_PRIVS` and installs a narrow seccomp filter that blocks mount/remount, the new mount API syscalls, module loading, kexec, `bpf`, `perf_event_open`, and `userfaultfd`. This closes the privileged remount window while still leaving required child-setup syscalls such as `setns` available. - **Network namespace**: Requires successful namespace creation for proxy isolation; startup fails in proxy mode if required capabilities (`CAP_NET_ADMIN`, `CAP_SYS_ADMIN`) or `iproute2` are unavailable. If the `iptables` package is present, the supervisor installs OUTPUT chain rules (LOG + REJECT) inside the namespace to provide fast-fail behavior (immediate `ECONNREFUSED` instead of a 30-second timeout) and diagnostic logging when processes attempt direct connections that bypass the HTTP CONNECT proxy. If `iptables` is absent, the supervisor logs a warning and continues — core network isolation still works via routing. diff --git a/crates/openshell-sandbox/src/main.rs b/crates/openshell-sandbox/src/main.rs index b6b8ac24a..c7aa41a8f 100644 --- a/crates/openshell-sandbox/src/main.rs +++ b/crates/openshell-sandbox/src/main.rs @@ -101,7 +101,7 @@ struct Args { fn main() -> Result<()> { let args = Args::parse(); - // Try to open a rolling log file; fall back to stdout-only logging if it fails + // Try to open a rolling log file; fall back to stderr-only logging if it fails // (e.g., /var/log is not writable in custom workload images). // Rotates daily, keeps the 3 most recent files to bound disk usage. let file_logging = tracing_appender::rolling::RollingFileAppender::builder() @@ -116,7 +116,7 @@ fn main() -> Result<()> { (writer, guard) }); - let stdout_filter = + let console_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)); let runtime = tokio::runtime::Builder::new_multi_thread() @@ -176,9 +176,9 @@ fn main() -> Result<()> { tracing_subscriber::registry() .with( - OcsfShorthandLayer::new(std::io::stdout()) + OcsfShorthandLayer::new(std::io::stderr()) .with_non_ocsf(true) - .with_filter(stdout_filter), + .with_filter(console_filter), ) .with( OcsfShorthandLayer::new(file_writer) @@ -192,14 +192,14 @@ fn main() -> Result<()> { } else { tracing_subscriber::registry() .with( - OcsfShorthandLayer::new(std::io::stdout()) + OcsfShorthandLayer::new(std::io::stderr()) .with_non_ocsf(true) - .with_filter(stdout_filter), + .with_filter(console_filter), ) .with(push_layer) .init(); // Log the warning after the subscriber is initialized - warn!("Could not open /var/log for log rotation; using stdout-only logging"); + warn!("Could not open /var/log for log rotation; using stderr-only logging"); (None, None) }; diff --git a/crates/openshell-sandbox/tests/stdout_logging.rs b/crates/openshell-sandbox/tests/stdout_logging.rs new file mode 100644 index 000000000..c4f5213de --- /dev/null +++ b/crates/openshell-sandbox/tests/stdout_logging.rs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::process::Command; + +#[test] +fn startup_logs_go_to_stderr_not_stdout() { + let output = Command::new(env!("CARGO_BIN_EXE_openshell-sandbox")) + .arg("--") + .arg("/usr/bin/printf") + .arg("hello") + .env("OPENSHELL_LOG_LEVEL", "info") + .env_remove("RUST_LOG") + .env_remove("OPENSHELL_POLICY_RULES") + .env_remove("OPENSHELL_POLICY_DATA") + .env_remove("OPENSHELL_SANDBOX_ID") + .env_remove("OPENSHELL_ENDPOINT") + .output() + .expect("spawn openshell-sandbox"); + + assert!( + !output.status.success(), + "expected sandbox startup to fail without a policy source" + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + stdout.trim().is_empty(), + "expected startup logs on stderr only, got stdout: {stdout}" + ); + assert!( + stderr.contains("Starting sandbox"), + "expected startup log on stderr, got: {stderr}" + ); + assert!( + stderr.contains("Sandbox policy required"), + "expected missing-policy error on stderr, got: {stderr}" + ); +}