mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-24 06:34:33 +08:00
fix(sandbox): preserve standard sbin paths for CDI workloads
Signed-off-by: Evan Lezar <elezar@nvidia.com>
This commit is contained in:
@@ -2216,4 +2216,3 @@ containerEdits:
|
||||
- nodev
|
||||
- rbind
|
||||
- rprivate
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ pub struct LocalBoundaryExec {
|
||||
ca_file_paths: Option<Arc<(std::path::PathBuf, std::path::PathBuf)>>,
|
||||
provider_credentials: ProviderCredentialState,
|
||||
user_environment: HashMap<String, String>,
|
||||
repair_standard_sbin: bool,
|
||||
runtime: Arc<crate::boundary_io::BoundaryRuntimeState>,
|
||||
#[cfg(target_os = "linux")]
|
||||
launcher: openshell_isolation_interface::linux::workload_launcher::WorkloadLauncher,
|
||||
@@ -59,6 +60,7 @@ impl LocalBoundaryExec {
|
||||
ca_file_paths,
|
||||
provider_credentials,
|
||||
user_environment,
|
||||
repair_standard_sbin: false,
|
||||
runtime,
|
||||
#[cfg(target_os = "linux")]
|
||||
launcher,
|
||||
@@ -121,6 +123,13 @@ impl LocalBoundaryExec {
|
||||
})
|
||||
}
|
||||
|
||||
/// Configure whether workload paths should include the standard sbin directories.
|
||||
#[must_use]
|
||||
pub fn with_standard_sbin_path_repair(mut self, enabled: bool) -> Self {
|
||||
self.repair_standard_sbin = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
fn command(&self, spec: &ExecSpec) -> Result<Command, BackendError> {
|
||||
let (program, args) = if let Some(shell_spec) = &spec.shell {
|
||||
let shell = openshell_core::shell::find_login_shell().ok_or_else(|| {
|
||||
@@ -151,12 +160,18 @@ impl LocalBoundaryExec {
|
||||
if program.is_empty() {
|
||||
return Err(BackendError::Process("exec program is empty".to_string()));
|
||||
}
|
||||
let repair_standard_sbin = self.repair_standard_sbin;
|
||||
let args = crate::process::process_args_with_standard_sbin_paths(
|
||||
&program,
|
||||
&args,
|
||||
repair_standard_sbin,
|
||||
);
|
||||
let mut command = Command::new(&program);
|
||||
command.args(&args);
|
||||
command.args(args);
|
||||
let effective_workdir = spec.workdir.as_deref().or(self.base_workdir.as_deref());
|
||||
let (session_user, session_home) =
|
||||
crate::process::session_user_and_home(&self.policy, effective_workdir);
|
||||
let path = std::env::var("PATH").unwrap_or_else(|_| "/usr/local/bin:/usr/bin:/bin".into());
|
||||
let path = crate::child_env::child_path_from_env(repair_standard_sbin);
|
||||
command
|
||||
.env_clear()
|
||||
.env(openshell_core::sandbox_env::SANDBOX, "1")
|
||||
@@ -169,7 +184,17 @@ impl LocalBoundaryExec {
|
||||
}
|
||||
for (key, value) in &self.user_environment {
|
||||
if !key.starts_with("OPENSHELL_") {
|
||||
command.env(key, value);
|
||||
if key == "PATH" {
|
||||
command.env(
|
||||
key,
|
||||
crate::child_env::maybe_path_with_standard_sbin_paths(
|
||||
value,
|
||||
repair_standard_sbin,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
command.env(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((ca_cert_path, combined_bundle_path)) = self.ca_file_paths.as_deref() {
|
||||
@@ -179,13 +204,33 @@ impl LocalBoundaryExec {
|
||||
}
|
||||
for (key, value) in self.provider_credentials.child_env_with_gcp_resolved() {
|
||||
if !crate::process::is_supervisor_only_env_var(&key) {
|
||||
command.env(key, value);
|
||||
if key == "PATH" {
|
||||
command.env(
|
||||
key,
|
||||
crate::child_env::maybe_path_with_standard_sbin_paths(
|
||||
&value,
|
||||
repair_standard_sbin,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
command.env(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::process::strip_proxy_env_std(&mut command);
|
||||
for (key, value) in &spec.env {
|
||||
if !key.starts_with("OPENSHELL_") {
|
||||
command.env(key, value);
|
||||
if key == "PATH" {
|
||||
command.env(
|
||||
key,
|
||||
crate::child_env::maybe_path_with_standard_sbin_paths(
|
||||
value,
|
||||
repair_standard_sbin,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
command.env(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(workdir) = spec.workdir.as_deref().or(self.base_workdir.as_deref()) {
|
||||
|
||||
@@ -2732,6 +2732,7 @@ mod linux {
|
||||
process_id: format!("{}:main:0", self.config.generation),
|
||||
spec,
|
||||
policy,
|
||||
cdi_active: self.config.cdi_context.is_some(),
|
||||
provider_env_revision,
|
||||
provider_env,
|
||||
ca_file_paths,
|
||||
@@ -3124,6 +3125,7 @@ mod linux {
|
||||
process_id: String,
|
||||
spec: AgentSpecWire,
|
||||
policy: openshell_core::policy::SandboxPolicy,
|
||||
cdi_active: bool,
|
||||
provider_env_revision: u64,
|
||||
provider_env: std::collections::HashMap<String, String>,
|
||||
ca_file_paths: Option<(std::path::PathBuf, std::path::PathBuf)>,
|
||||
@@ -3158,6 +3160,7 @@ mod linux {
|
||||
process_id,
|
||||
spec,
|
||||
policy,
|
||||
cdi_active,
|
||||
provider_env_revision,
|
||||
provider_env,
|
||||
ca_file_paths,
|
||||
@@ -3178,6 +3181,7 @@ mod linux {
|
||||
spec.timeout_secs,
|
||||
spec.interactive,
|
||||
&policy,
|
||||
cdi_active,
|
||||
entrypoint_pid,
|
||||
provider_credentials.clone(),
|
||||
provider_env,
|
||||
@@ -5734,6 +5738,7 @@ containerEdits:
|
||||
process_id: "generation-retained:main:0".to_string(),
|
||||
spec: agent_spec.clone(),
|
||||
policy,
|
||||
cdi_active: false,
|
||||
provider_env_revision: 0,
|
||||
provider_env: std::collections::HashMap::new(),
|
||||
ca_file_paths: None,
|
||||
|
||||
@@ -1,8 +1,215 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use openshell_core::policy::SandboxPolicy;
|
||||
|
||||
pub const DEFAULT_CHILD_PATH: &str = "/usr/local/bin:/usr/bin:/bin";
|
||||
const STANDARD_SBIN_PATHS: &[&str] = &["/usr/local/sbin", "/usr/sbin", "/sbin"];
|
||||
const ENSURE_STANDARD_SBIN_PATHS_SCRIPT: &str = "for dir in /usr/local/sbin /usr/sbin /sbin; do case \":${PATH:-}:\" in *:\"$dir\":*) ;; *) PATH=\"${PATH:+$PATH:}$dir\" ;; esac; done; export PATH";
|
||||
const STARTUP_SNIPPET_MARKER: &str = "# OpenShell standard sbin PATH";
|
||||
|
||||
enum StartupFile {
|
||||
Missing,
|
||||
Regular(String),
|
||||
Unsafe,
|
||||
}
|
||||
|
||||
pub fn standard_sbin_path_repair_enabled(policy: &SandboxPolicy, cdi_active: bool) -> bool {
|
||||
cdi_active && policy_has_standard_sbin_path(policy)
|
||||
}
|
||||
|
||||
fn policy_has_standard_sbin_path(policy: &SandboxPolicy) -> bool {
|
||||
policy
|
||||
.filesystem
|
||||
.read_only
|
||||
.iter()
|
||||
.chain(policy.filesystem.read_write.iter())
|
||||
.any(|path| {
|
||||
STANDARD_SBIN_PATHS
|
||||
.iter()
|
||||
.any(|dir| Path::new(dir).starts_with(path) || path.starts_with(dir))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn child_path_from_env(repair_standard_sbin: bool) -> String {
|
||||
let path = std::env::var("PATH")
|
||||
.ok()
|
||||
.filter(|path| !path.trim().is_empty())
|
||||
.unwrap_or_else(|| DEFAULT_CHILD_PATH.to_string());
|
||||
|
||||
maybe_path_with_standard_sbin_paths(&path, repair_standard_sbin)
|
||||
}
|
||||
|
||||
pub fn maybe_path_with_standard_sbin_paths(path: &str, repair_standard_sbin: bool) -> String {
|
||||
if repair_standard_sbin {
|
||||
path_with_standard_sbin_paths(path)
|
||||
} else {
|
||||
path.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path_with_standard_sbin_paths(path: &str) -> String {
|
||||
let mut path = if path.trim().is_empty() {
|
||||
DEFAULT_CHILD_PATH.to_string()
|
||||
} else {
|
||||
path.to_string()
|
||||
};
|
||||
|
||||
for dir in STANDARD_SBIN_PATHS {
|
||||
if !path.split(':').any(|entry| entry == *dir) {
|
||||
if !path.is_empty() {
|
||||
path.push(':');
|
||||
}
|
||||
path.push_str(dir);
|
||||
}
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
pub fn shell_command_with_standard_sbin_paths(command: &str) -> String {
|
||||
format!("{ENSURE_STANDARD_SBIN_PATHS_SCRIPT}\n{command}")
|
||||
}
|
||||
|
||||
pub fn maybe_shell_command_with_standard_sbin_paths(
|
||||
command: &str,
|
||||
repair_standard_sbin: bool,
|
||||
) -> String {
|
||||
if repair_standard_sbin {
|
||||
shell_command_with_standard_sbin_paths(command)
|
||||
} else {
|
||||
command.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install_standard_sbin_path_startup_files(home: Option<&str>) {
|
||||
if let Some(home) = home {
|
||||
let bashrc_path = Path::new(home).join(".bashrc");
|
||||
if let Err(error) = append_startup_snippet(&bashrc_path) {
|
||||
tracing::debug!(
|
||||
path = %bashrc_path.display(),
|
||||
error = %error,
|
||||
"failed to install OpenShell PATH shell startup snippet"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn startup_snippet() -> String {
|
||||
format!("{STARTUP_SNIPPET_MARKER}\n{ENSURE_STANDARD_SBIN_PATHS_SCRIPT}\n")
|
||||
}
|
||||
|
||||
fn append_startup_snippet(path: &Path) -> std::io::Result<()> {
|
||||
if let Some(parent) = path.parent()
|
||||
&& !existing_directory_without_symlink(parent)?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let existing = match read_startup_file(path)? {
|
||||
StartupFile::Regular(content) => content,
|
||||
StartupFile::Missing | StartupFile::Unsafe => return Ok(()),
|
||||
};
|
||||
if existing.contains(STARTUP_SNIPPET_MARKER) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let snippet = startup_snippet();
|
||||
let mut file = open_startup_file_for_append(path)?;
|
||||
if !existing.is_empty() && !existing.ends_with('\n') {
|
||||
file.write_all(b"\n")?;
|
||||
}
|
||||
file.write_all(snippet.as_bytes())
|
||||
}
|
||||
|
||||
fn read_startup_file(path: &Path) -> std::io::Result<StartupFile> {
|
||||
let metadata = match std::fs::symlink_metadata(path) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
return Ok(StartupFile::Missing);
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let file_type = metadata.file_type();
|
||||
if file_type.is_symlink() || !file_type.is_file() {
|
||||
tracing::debug!(
|
||||
path = %path.display(),
|
||||
"skipping OpenShell PATH startup repair for non-regular file"
|
||||
);
|
||||
return Ok(StartupFile::Unsafe);
|
||||
}
|
||||
|
||||
let mut content = String::new();
|
||||
open_startup_file_for_read(path)?.read_to_string(&mut content)?;
|
||||
Ok(StartupFile::Regular(content))
|
||||
}
|
||||
|
||||
fn existing_directory_without_symlink(path: &Path) -> std::io::Result<bool> {
|
||||
if let Some(parent) = path.parent()
|
||||
&& parent != path
|
||||
&& !parent.as_os_str().is_empty()
|
||||
&& !existing_directory_without_symlink(parent)?
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
match std::fs::symlink_metadata(path) {
|
||||
Ok(metadata) if metadata.file_type().is_symlink() => {
|
||||
tracing::debug!(
|
||||
path = %path.display(),
|
||||
"skipping OpenShell PATH startup repair through symlink directory"
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
Ok(metadata) if metadata.is_dir() => Ok(true),
|
||||
Ok(_) => {
|
||||
tracing::debug!(
|
||||
path = %path.display(),
|
||||
"skipping OpenShell PATH startup repair through non-directory path"
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_startup_file_for_read(path: &Path) -> std::io::Result<std::fs::File> {
|
||||
let file = no_follow_options().read(true).open(path)?;
|
||||
ensure_opened_file_is_regular(&file, path)?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn open_startup_file_for_append(path: &Path) -> std::io::Result<std::fs::File> {
|
||||
let file = no_follow_options().append(true).open(path)?;
|
||||
ensure_opened_file_is_regular(&file, path)?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn ensure_opened_file_is_regular(file: &std::fs::File, path: &Path) -> std::io::Result<()> {
|
||||
let metadata = file.metadata()?;
|
||||
if metadata.is_file() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(std::io::Error::other(format!(
|
||||
"'{}' is not a regular file",
|
||||
path.display()
|
||||
)))
|
||||
}
|
||||
|
||||
fn no_follow_options() -> std::fs::OpenOptions {
|
||||
let mut options = std::fs::OpenOptions::new();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt as _;
|
||||
options.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
pub fn tls_env_vars(
|
||||
ca_cert_path: &Path,
|
||||
combined_bundle_path: &Path,
|
||||
@@ -27,6 +234,198 @@ mod tests {
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
|
||||
#[test]
|
||||
fn path_with_standard_sbin_paths_uses_default_for_empty_path() {
|
||||
assert_eq!(
|
||||
path_with_standard_sbin_paths(""),
|
||||
"/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_with_standard_sbin_paths_appends_missing_sbin_dirs() {
|
||||
assert_eq!(
|
||||
path_with_standard_sbin_paths("/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin"),
|
||||
"/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_with_standard_sbin_paths_is_idempotent() {
|
||||
let path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
||||
assert_eq!(path_with_standard_sbin_paths(path), path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_path_with_standard_sbin_paths_respects_gate() {
|
||||
let path = "/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin";
|
||||
|
||||
assert_eq!(maybe_path_with_standard_sbin_paths(path, false), path);
|
||||
assert_eq!(
|
||||
maybe_path_with_standard_sbin_paths(path, true),
|
||||
"/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_shell_command_with_standard_sbin_paths_respects_gate() {
|
||||
assert_eq!(
|
||||
maybe_shell_command_with_standard_sbin_paths("nvidia-smi -L", false),
|
||||
"nvidia-smi -L"
|
||||
);
|
||||
assert!(
|
||||
maybe_shell_command_with_standard_sbin_paths("nvidia-smi -L", true)
|
||||
.contains("/usr/sbin")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_sbin_repair_requires_cdi_context_and_policy_path() {
|
||||
let policy = policy_with_read_only(["/usr/sbin/nvidia-smi"]);
|
||||
|
||||
assert!(standard_sbin_path_repair_enabled(&policy, true));
|
||||
assert!(!standard_sbin_path_repair_enabled(&policy, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_sbin_repair_requires_standard_sbin_policy_path() {
|
||||
let policy = policy_with_read_only(["/usr/local/bin/nvidia-smi"]);
|
||||
|
||||
assert!(!standard_sbin_path_repair_enabled(&policy, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_sbin_repair_accepts_read_write_standard_sbin_policy_path() {
|
||||
let mut policy = policy_with_read_only(std::iter::empty::<&str>());
|
||||
policy
|
||||
.filesystem
|
||||
.read_write
|
||||
.push("/sbin/vendor-tool".into());
|
||||
|
||||
assert!(standard_sbin_path_repair_enabled(&policy, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_sbin_repair_accepts_read_only_ancestor() {
|
||||
let policy = policy_with_read_only(["/usr"]);
|
||||
|
||||
assert!(standard_sbin_path_repair_enabled(&policy, true));
|
||||
}
|
||||
|
||||
fn policy_with_read_only(
|
||||
paths: impl IntoIterator<Item = impl Into<std::path::PathBuf>>,
|
||||
) -> SandboxPolicy {
|
||||
SandboxPolicy {
|
||||
version: 1,
|
||||
filesystem: openshell_core::policy::FilesystemPolicy {
|
||||
read_only: paths.into_iter().map(Into::into).collect(),
|
||||
read_write: Vec::new(),
|
||||
include_workdir: false,
|
||||
},
|
||||
network: openshell_core::policy::NetworkPolicy::default(),
|
||||
landlock: openshell_core::policy::LandlockPolicy::default(),
|
||||
process: openshell_core::policy::ProcessPolicy::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_command_with_standard_sbin_paths_extends_runtime_path() {
|
||||
let command = shell_command_with_standard_sbin_paths("printf '%s' \"$PATH\"");
|
||||
let output = Command::new("/bin/sh")
|
||||
.arg("-c")
|
||||
.arg(format!(
|
||||
"PATH=/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin\n{command}"
|
||||
))
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.expect("spawn shell");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"shell command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8(output.stdout).expect("utf8"),
|
||||
"/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bashrc_startup_snippet_is_idempotent() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
// macOS commonly exposes its temporary directory through `/var`, a
|
||||
// symlink to `/private/var`. Use the canonical directory because the
|
||||
// startup-file writer deliberately refuses to traverse symlinked
|
||||
// ancestors.
|
||||
let root = dir.path().canonicalize().expect("canonical tempdir");
|
||||
let home = root.join("sandbox");
|
||||
std::fs::create_dir_all(&home).expect("home dir");
|
||||
let bashrc_path = home.join(".bashrc");
|
||||
std::fs::write(
|
||||
&bashrc_path,
|
||||
"export PATH=\"/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin\"\n",
|
||||
)
|
||||
.expect("write bashrc");
|
||||
|
||||
append_startup_snippet(&bashrc_path).expect("append startup snippet");
|
||||
append_startup_snippet(&bashrc_path).expect("append startup snippet again");
|
||||
|
||||
let bashrc = std::fs::read_to_string(&bashrc_path).expect("read bashrc");
|
||||
|
||||
assert_eq!(bashrc.matches(STARTUP_SNIPPET_MARKER).count(), 1);
|
||||
assert!(bashrc.contains("export PATH=\"/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin\""));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn bashrc_snippet_skips_symlink() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let target = dir.path().join("target");
|
||||
std::fs::write(&target, "keep me").expect("write target");
|
||||
let bashrc_path = dir.path().join(".bashrc");
|
||||
symlink(&target, &bashrc_path).expect("symlink bashrc");
|
||||
|
||||
append_startup_snippet(&bashrc_path).expect("skip symlink bashrc");
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&target).expect("read target"),
|
||||
"keep me"
|
||||
);
|
||||
assert!(
|
||||
std::fs::symlink_metadata(&bashrc_path)
|
||||
.expect("bashrc metadata")
|
||||
.file_type()
|
||||
.is_symlink()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn bashrc_snippet_skips_symlink_parent() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let target_dir = dir.path().join("target-dir");
|
||||
std::fs::create_dir(&target_dir).expect("target dir");
|
||||
std::fs::write(
|
||||
target_dir.join(".bashrc"),
|
||||
"export PATH=\"/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin\"\n",
|
||||
)
|
||||
.expect("target bashrc");
|
||||
let home = dir.path().join("home");
|
||||
symlink(&target_dir, &home).expect("symlink home");
|
||||
|
||||
append_startup_snippet(&home.join(".bashrc")).expect("skip symlink home");
|
||||
|
||||
let bashrc = std::fs::read_to_string(target_dir.join(".bashrc")).expect("read target");
|
||||
assert!(!bashrc.contains(STARTUP_SNIPPET_MARKER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_tls_env_sets_node_and_bundle_paths() {
|
||||
let mut cmd = Command::new("/usr/bin/env");
|
||||
|
||||
@@ -35,6 +35,7 @@ pub async fn spawn_workload(
|
||||
timeout_secs: u64,
|
||||
interactive: bool,
|
||||
policy: &SandboxPolicy,
|
||||
cdi_active: bool,
|
||||
entrypoint_pid: Arc<AtomicU32>,
|
||||
provider_credentials: ProviderCredentialState,
|
||||
provider_env: std::collections::HashMap<String, String>,
|
||||
@@ -47,6 +48,12 @@ pub async fn spawn_workload(
|
||||
// setup inside the capability-free boundary.
|
||||
let workspace = ResolvedWorkspace::new(workdir.map(str::to_string), true);
|
||||
|
||||
let repair_standard_sbin =
|
||||
crate::child_env::standard_sbin_path_repair_enabled(policy, cdi_active);
|
||||
if repair_standard_sbin {
|
||||
crate::child_env::install_standard_sbin_path_startup_files(workspace.home());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(workspace_root) = workspace.root()
|
||||
&& workspace_root != openshell_core::driver_mounts::DEFAULT_WORKSPACE_ROOT
|
||||
@@ -78,8 +85,8 @@ pub async fn spawn_workload(
|
||||
let loopback_connector: Arc<dyn BoundaryLoopbackConnector> = Arc::new(
|
||||
crate::boundary_io::LocalLoopbackConnector::new(Some(boundary_runtime.clone())),
|
||||
);
|
||||
let boundary_exec: Arc<dyn BoundaryExec> =
|
||||
Arc::new(crate::boundary_exec::LocalBoundaryExec::new(
|
||||
let boundary_exec: Arc<dyn BoundaryExec> = Arc::new(
|
||||
crate::boundary_exec::LocalBoundaryExec::new(
|
||||
policy.clone(),
|
||||
workspace.owned_root(),
|
||||
ca_file_paths.clone().map(Arc::new),
|
||||
@@ -87,7 +94,9 @@ pub async fn spawn_workload(
|
||||
user_environment,
|
||||
boundary_runtime.clone(),
|
||||
launcher.clone(),
|
||||
));
|
||||
)
|
||||
.with_standard_sbin_path_repair(repair_standard_sbin),
|
||||
);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let mut handle = ProcessHandle::spawn(
|
||||
@@ -97,6 +106,7 @@ pub async fn spawn_workload(
|
||||
&workspace,
|
||||
interactive,
|
||||
policy,
|
||||
repair_standard_sbin,
|
||||
ca_file_paths.as_ref(),
|
||||
&provider_env,
|
||||
)
|
||||
@@ -108,6 +118,7 @@ pub async fn spawn_workload(
|
||||
&workspace,
|
||||
interactive,
|
||||
policy,
|
||||
repair_standard_sbin,
|
||||
ca_file_paths.as_ref(),
|
||||
&provider_env,
|
||||
)?;
|
||||
|
||||
@@ -163,12 +163,23 @@ pub fn is_proxy_env_var(key: &str) -> bool {
|
||||
PROXY_ENV_VARS.contains(&key)
|
||||
}
|
||||
|
||||
fn inject_provider_env(cmd: &mut Command, provider_env: &HashMap<String, String>) {
|
||||
fn inject_provider_env(
|
||||
cmd: &mut Command,
|
||||
provider_env: &HashMap<String, String>,
|
||||
repair_standard_sbin: bool,
|
||||
) {
|
||||
for (key, value) in provider_env {
|
||||
if is_supervisor_only_env_var(key) {
|
||||
continue;
|
||||
}
|
||||
cmd.env(key, value);
|
||||
if key == "PATH" {
|
||||
cmd.env(
|
||||
key,
|
||||
child_env::maybe_path_with_standard_sbin_paths(value, repair_standard_sbin),
|
||||
);
|
||||
} else {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,8 +215,18 @@ fn apply_canonical_process_environment(
|
||||
workspace: &ResolvedWorkspace,
|
||||
interactive: bool,
|
||||
user_environment: &HashMap<String, String>,
|
||||
repair_standard_sbin: bool,
|
||||
) {
|
||||
cmd.envs(user_environment);
|
||||
for (key, value) in user_environment {
|
||||
if key == "PATH" {
|
||||
cmd.env(
|
||||
key,
|
||||
child_env::maybe_path_with_standard_sbin_paths(value, repair_standard_sbin),
|
||||
);
|
||||
} else {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
}
|
||||
let (session_user, session_home) = session_user_and_home(policy, workspace.home());
|
||||
// Resolve a shell present in the workload image. This code runs inside the
|
||||
// workload boundary, where the image filesystem is visible.
|
||||
@@ -380,6 +401,35 @@ pub enum ProcessIo {
|
||||
},
|
||||
}
|
||||
|
||||
fn shell_command_arg_index(args: &[String]) -> Option<usize> {
|
||||
for (index, arg) in args.iter().enumerate() {
|
||||
if arg == "-c" || (arg.starts_with('-') && !arg.starts_with("--") && arg.contains('c')) {
|
||||
return (index + 1 < args.len()).then_some(index + 1);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn process_args_with_standard_sbin_paths(
|
||||
program: &str,
|
||||
args: &[String],
|
||||
repair_standard_sbin: bool,
|
||||
) -> Vec<String> {
|
||||
let mut args = args.to_vec();
|
||||
if !repair_standard_sbin {
|
||||
return args;
|
||||
}
|
||||
|
||||
let basename = program.rsplit('/').next().unwrap_or(program);
|
||||
if matches!(basename, "bash" | "sh")
|
||||
&& let Some(command_index) = shell_command_arg_index(&args)
|
||||
{
|
||||
args[command_index] =
|
||||
child_env::shell_command_with_standard_sbin_paths(&args[command_index]);
|
||||
}
|
||||
args
|
||||
}
|
||||
|
||||
impl ProcessHandle {
|
||||
/// Spawn a new process.
|
||||
///
|
||||
@@ -395,6 +445,7 @@ impl ProcessHandle {
|
||||
workspace: &ResolvedWorkspace,
|
||||
interactive: bool,
|
||||
policy: &SandboxPolicy,
|
||||
repair_standard_sbin: bool,
|
||||
ca_paths: Option<&(PathBuf, PathBuf)>,
|
||||
provider_env: &HashMap<String, String>,
|
||||
) -> Result<Self> {
|
||||
@@ -405,6 +456,7 @@ impl ProcessHandle {
|
||||
workspace,
|
||||
interactive,
|
||||
policy,
|
||||
repair_standard_sbin,
|
||||
ca_paths,
|
||||
provider_env,
|
||||
)
|
||||
@@ -423,6 +475,7 @@ impl ProcessHandle {
|
||||
workspace: &ResolvedWorkspace,
|
||||
interactive: bool,
|
||||
policy: &SandboxPolicy,
|
||||
repair_standard_sbin: bool,
|
||||
ca_paths: Option<&(PathBuf, PathBuf)>,
|
||||
provider_env: &HashMap<String, String>,
|
||||
) -> Result<Self> {
|
||||
@@ -432,6 +485,7 @@ impl ProcessHandle {
|
||||
workspace,
|
||||
interactive,
|
||||
policy,
|
||||
repair_standard_sbin,
|
||||
ca_paths,
|
||||
provider_env,
|
||||
)
|
||||
@@ -446,13 +500,16 @@ impl ProcessHandle {
|
||||
workspace: &ResolvedWorkspace,
|
||||
interactive: bool,
|
||||
policy: &SandboxPolicy,
|
||||
repair_standard_sbin: bool,
|
||||
ca_paths: Option<&(PathBuf, PathBuf)>,
|
||||
provider_env: &HashMap<String, String>,
|
||||
) -> Result<Self> {
|
||||
let args = process_args_with_standard_sbin_paths(program, args, repair_standard_sbin);
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.args(args)
|
||||
.kill_on_drop(true)
|
||||
.env(openshell_core::sandbox_env::SANDBOX, "1");
|
||||
.env(openshell_core::sandbox_env::SANDBOX, "1")
|
||||
.env("PATH", child_env::child_path_from_env(repair_standard_sbin));
|
||||
|
||||
let mut pty_master = None;
|
||||
let mut terminal_slave_fd = None;
|
||||
@@ -487,10 +544,11 @@ impl ProcessHandle {
|
||||
workspace,
|
||||
interactive,
|
||||
&configured_user_environment(),
|
||||
repair_standard_sbin,
|
||||
);
|
||||
strip_supervisor_only_env(&mut cmd);
|
||||
|
||||
inject_provider_env(&mut cmd, provider_env);
|
||||
inject_provider_env(&mut cmd, provider_env, repair_standard_sbin);
|
||||
|
||||
if let Some(dir) = workspace.root() {
|
||||
cmd.current_dir(dir);
|
||||
@@ -611,13 +669,16 @@ impl ProcessHandle {
|
||||
workspace: &ResolvedWorkspace,
|
||||
interactive: bool,
|
||||
policy: &SandboxPolicy,
|
||||
repair_standard_sbin: bool,
|
||||
ca_paths: Option<&(PathBuf, PathBuf)>,
|
||||
provider_env: &HashMap<String, String>,
|
||||
) -> Result<Self> {
|
||||
let args = process_args_with_standard_sbin_paths(program, args, repair_standard_sbin);
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.args(args)
|
||||
.kill_on_drop(true)
|
||||
.env(openshell_core::sandbox_env::SANDBOX, "1");
|
||||
.env(openshell_core::sandbox_env::SANDBOX, "1")
|
||||
.env("PATH", child_env::child_path_from_env(repair_standard_sbin));
|
||||
|
||||
let mut pty_master = None;
|
||||
let mut terminal_slave_fd = None;
|
||||
@@ -652,10 +713,11 @@ impl ProcessHandle {
|
||||
workspace,
|
||||
interactive,
|
||||
&configured_user_environment(),
|
||||
repair_standard_sbin,
|
||||
);
|
||||
strip_supervisor_only_env(&mut cmd);
|
||||
|
||||
inject_provider_env(&mut cmd, provider_env);
|
||||
inject_provider_env(&mut cmd, provider_env, repair_standard_sbin);
|
||||
|
||||
if let Some(dir) = workspace.root() {
|
||||
cmd.current_dir(dir);
|
||||
@@ -1072,6 +1134,35 @@ mod tests {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::process::Stdio as StdStdio;
|
||||
|
||||
#[test]
|
||||
fn process_args_wrap_shell_c_command_with_standard_sbin_paths() {
|
||||
let args = vec!["-lc".to_string(), "nvidia-smi -L".to_string()];
|
||||
|
||||
let wrapped = process_args_with_standard_sbin_paths("sh", &args, true);
|
||||
|
||||
assert_eq!(wrapped[0], "-lc");
|
||||
assert!(wrapped[1].contains("/usr/sbin"));
|
||||
assert!(wrapped[1].contains("nvidia-smi -L"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_args_leave_non_shell_commands_unchanged() {
|
||||
let args = vec!["nvidia-smi -L".to_string()];
|
||||
|
||||
let wrapped = process_args_with_standard_sbin_paths("python", &args, true);
|
||||
|
||||
assert_eq!(wrapped, args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_args_do_not_wrap_when_standard_sbin_repair_disabled() {
|
||||
let args = vec!["-lc".to_string(), "nvidia-smi -L".to_string()];
|
||||
|
||||
let wrapped = process_args_with_standard_sbin_paths("sh", &args, false);
|
||||
|
||||
assert_eq!(wrapped, args);
|
||||
}
|
||||
|
||||
/// Helper to create a minimal `SandboxPolicy` with the given process policy.
|
||||
fn policy_with_process(process: ProcessPolicy) -> SandboxPolicy {
|
||||
SandboxPolicy {
|
||||
@@ -1101,7 +1192,14 @@ mod tests {
|
||||
.env("TERM", "dumb")
|
||||
.stdout(StdStdio::piped());
|
||||
|
||||
apply_canonical_process_environment(&mut cmd, &policy, &workspace, true, &HashMap::new());
|
||||
apply_canonical_process_environment(
|
||||
&mut cmd,
|
||||
&policy,
|
||||
&workspace,
|
||||
true,
|
||||
&HashMap::new(),
|
||||
false,
|
||||
);
|
||||
|
||||
let output = cmd.output().await.expect("run environment probe");
|
||||
assert!(output.status.success());
|
||||
@@ -1150,6 +1248,7 @@ mod tests {
|
||||
&ResolvedWorkspace::default(),
|
||||
interactive,
|
||||
&declared,
|
||||
false,
|
||||
);
|
||||
strip_supervisor_only_env(&mut cmd);
|
||||
inject_provider_env(
|
||||
@@ -1158,6 +1257,7 @@ mod tests {
|
||||
"ANTHROPIC_API_KEY".into(),
|
||||
"openshell:resolve:env:ANTHROPIC_API_KEY".into(),
|
||||
)]),
|
||||
false,
|
||||
);
|
||||
let output = cmd.output().await.expect("run environment probe");
|
||||
assert!(output.status.success());
|
||||
@@ -1316,13 +1416,61 @@ mod tests {
|
||||
))
|
||||
.collect();
|
||||
|
||||
inject_provider_env(&mut cmd, &provider_env);
|
||||
inject_provider_env(&mut cmd, &provider_env, false);
|
||||
|
||||
let output = cmd.output().await.expect("spawn env");
|
||||
let stdout = String::from_utf8(output.stdout).expect("utf8");
|
||||
assert!(stdout.contains("ANTHROPIC_API_KEY=openshell:resolve:env:ANTHROPIC_API_KEY"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn inject_provider_env_appends_standard_sbin_to_path_when_enabled() {
|
||||
let mut cmd = Command::new("/usr/bin/env");
|
||||
cmd.env_clear()
|
||||
.stdin(StdStdio::null())
|
||||
.stdout(StdStdio::piped())
|
||||
.stderr(StdStdio::null());
|
||||
|
||||
let provider_env = HashMap::from([(
|
||||
"PATH".to_string(),
|
||||
"/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin".to_string(),
|
||||
)]);
|
||||
|
||||
inject_provider_env(&mut cmd, &provider_env, true);
|
||||
|
||||
let output = cmd.output().await.expect("spawn env");
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8(output.stdout).expect("utf8");
|
||||
assert!(stdout.lines().any(|line| {
|
||||
line == "PATH=/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn inject_provider_env_leaves_path_unchanged_when_standard_sbin_repair_disabled() {
|
||||
let mut cmd = Command::new("/usr/bin/env");
|
||||
cmd.env_clear()
|
||||
.stdin(StdStdio::null())
|
||||
.stdout(StdStdio::piped())
|
||||
.stderr(StdStdio::null());
|
||||
|
||||
let provider_env = HashMap::from([(
|
||||
"PATH".to_string(),
|
||||
"/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin".to_string(),
|
||||
)]);
|
||||
|
||||
inject_provider_env(&mut cmd, &provider_env, false);
|
||||
|
||||
let output = cmd.output().await.expect("spawn env");
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8(output.stdout).expect("utf8");
|
||||
assert!(
|
||||
stdout
|
||||
.lines()
|
||||
.any(|line| { line == "PATH=/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin" })
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
#[allow(unsafe_code)]
|
||||
@@ -1538,7 +1686,7 @@ mod tests {
|
||||
),
|
||||
]);
|
||||
|
||||
inject_provider_env(&mut cmd, &provider_env);
|
||||
inject_provider_env(&mut cmd, &provider_env, false);
|
||||
|
||||
let output = cmd.output().await.expect("spawn env");
|
||||
assert!(output.status.success());
|
||||
|
||||
Reference in New Issue
Block a user