Files
OpenShell/e2e/rust/tests/driver_config_volume.rs
Drew Newberry c1f2e7189f feat(isolation): implement the RFC 0012 sandbox architecture (#2942)
* feat(isolation): add RFC 0012 backend contract

Signed-off-by: Drew Newberry <385+drew@users.noreply.github.com>

* refactor(isolation): name the interface crate explicitly

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): expose trusted host gateway

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(agents): inventory the MXC driver

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): add mediated DNS transport

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): tighten interface error and digest contracts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(isolation): remove unrelated driver inventory

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): define capability-free launch contract

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): seal confirmed boundary state

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): validate confirmation for external backend implementations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): clarify mediated DNS identity

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): generalize loopback connector

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): unify typed network mediation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): bind launches to sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(mxc): initialize extended sandbox status

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): add boundary protocol and Linux primitives

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): harden signals and separate process status from transport

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): validate remote confirmation through public contract

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): validate wire state and propagate snapshot failures

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(isolation): import owned agent specification explicitly

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(isolation): describe mediated DNS channel

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): bound mediation attach without nested retries

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): generalize loopback protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): add transport-neutral session authentication

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): separate sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): harden runtime boundary controls

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): add terminal boundary operation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): split supervisor and sandbox runtimes

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): harden boundary isolation and lifecycle ownership

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): reject private root redirects and adopt typed errors

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): preserve accept thread ownership on musl

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(sandbox): isolate credential probes from filtered threads

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): return retained exec exit status to independent waiters

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): bound network mediation and preserve socket authorization

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): bound control admission and retire stale mediation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* ci(e2e): select migrated drivers per stack layer

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(sandbox): implement loopback connector

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(isolation): authenticate the Sandbox Protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(supervisor): rotate launch-scoped authentication

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(sandbox): consume dedicated backend crate

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(sandbox): align topology session fixture

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): align projected bootstrap bundle

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): validate refreshed credentials before rotation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): fail closed across supervisor disconnects

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): repair rebased sandbox CI

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* build(runtime): publish separate sandbox and supervisor images

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(config): configure the sandbox runtime image

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(ci): validate sandbox binary linkage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(isolation): use backend and runtime terminology

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(sandbox): use a scratch runtime image

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(ci): refresh schema and dependency policy

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): bind reconnects to supervisor process

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs: align runtime split operational guidance

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* chore(security): document Kubernetes runtime RBAC

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): enforce runtime lifecycle invariants

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(compute): identify sandbox start generations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(server): restore sandbox launch sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): support authenticated runtime replacement

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): bind sandbox session successors

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): retry pending sandbox successors

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(vm): run the supervisor outside the guest workload

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): use sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): repair rebase integration

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): use unified build toolchain

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): use sandbox runtime terminology

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): own guest network bootstrap

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): expose guest init version

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): select native supervisor artifacts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): guard guest init Linux symbols

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): scope Linux test imports

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): avoid guest interface casts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): reconcile admitted sandbox identity

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): share resolved sandbox identity

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): surface host supervisor failures

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): include guest logs on supervisor exit

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): rotate and clean runtime generations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): make sandbox starts generation-aware

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): rotate restored sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): keep shared paths in the base layer

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): bind sandbox session lineage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(docker): isolate workloads behind the host supervisor

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(docker): rotate launch-scoped authentication

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): use sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): use host networking for supervisor

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): preserve host gateway alias resolution

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(docker): use separate sandbox and supervisor images

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): restore startup validation after rebase

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): name the sandbox runtime directly

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): narrow supervisor CA runtime storage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): close companion isolation gaps

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(docker): align mediated network expectations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(docker): exercise mediated network paths

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): attach supervisor to managed network

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): defer supervisor recovery until gateway is ready

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): make sandbox starts generation-aware

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): rotate restored sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): preserve workloads during session rotation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): remove unrelated configuration RFC changes

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): bind sandbox session lineage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(kubernetes): add proxy-pod isolation topology

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(kubernetes): use sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): use stable sandbox service authority

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(kubernetes): split sandbox and supervisor images

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): adapt proxy pods to current runtime APIs

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(kubernetes): describe the single runtime placement

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(kubernetes): simplify sandbox orchestration

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): validate deployment prerequisites

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): update Trivy Helm profile inventory

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(kubernetes): update Trivy scan inventory count

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): reuse preloaded runtime images in e2e

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): type and clean runtime resources

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): make sandbox restarts recoverable

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): rotate restored sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): preserve supervisor egress

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): bind sandbox session lineage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(podman): adopt isolated sandbox and supervisor containers

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): stage bootstrap archives at named volume destinations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(podman): rotate launch-scoped authentication

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(podman): use sandbox backend protocol

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(podman): use host networking for supervisor

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(podman): split sandbox and supervisor images

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): repair rebase integration

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(podman): name the sandbox runtime directly

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): provision supervisor CA runtime storage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): address isolation review findings

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): inspect Debian supervisor provenance

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): use libpod-compatible tmpfs options

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): bind verified sandbox runtime binary

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): provide external driver data directory

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): start sandbox before joining user namespace

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): separate supervisor user namespace

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): make sandbox starts generation-aware

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): rotate restored sandbox sessions

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): bind sandbox session lineage

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* perf(isolation): add TCP and DNS benchmark harnesses

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(perf): align benchmark timing and supported protocols

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(perf): report TCP benchmark metrics accurately

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(perf): cancel failed worker startup

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): build matching local supervisor image

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): make local sandbox smoke test runnable

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): wire local sandbox runtime image

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(kubernetes): narrow sandbox service RBAC

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(ci): validate split runtime artifacts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): harden runtime session handling

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(supervisor): add standalone network proxy role

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(rfc): remove implementation companion notes

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(vm): standardize runtime release name

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(vm): pin renamed runtime artifacts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): persist sandbox runtime identity

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(runtime): restore branch validation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(isolation): reconcile main after rebase

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(network): close unframed HTTP 1.0 responses

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* chore(isolation): preserve upstream OCSF updates

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(security): close credential and TLS replay paths

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(auth): make sandbox refresh retries idempotent

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

---------

Signed-off-by: Drew Newberry <385+drew@users.noreply.github.com>
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-09-16 00:49:14 +00:00

538 lines
17 KiB
Rust

// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
#![cfg(feature = "e2e-local-container-driver")]
use std::fs;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use bollard::Docker;
use bollard::models::{ContainerCreateBody, HostConfig, Mount, MountTypeEnum, VolumeCreateRequest};
use bollard::query_parameters::{
CreateContainerOptionsBuilder, CreateImageOptionsBuilder, LogsOptions, RemoveContainerOptions,
RemoveVolumeOptionsBuilder, StartContainerOptions, WaitContainerOptions,
};
use futures_util::TryStreamExt;
use openshell_e2e::harness::container::{ImageGuard, e2e_driver};
use openshell_e2e::harness::sandbox::SandboxGuard;
use serde_json::{Map, Value};
const TEST_IMAGE: &str = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest";
const VOLUME_TARGET: &str = "/sandbox/e2e-volume";
const BIND_TARGET: &str = "/sandbox/e2e-bind";
#[cfg(feature = "e2e-docker")]
const OCI_VOLUME_TARGET: &str = "/workspace/project/e2e-volume";
#[cfg(feature = "e2e-docker")]
const OCI_USER_DOCKERFILE: &str = r#"FROM public.ecr.aws/docker/library/python:3.13-slim
RUN apt-get update && apt-get install -y --no-install-recommends iproute2 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace/project
RUN chown 2234:2235 .
USER 2234:2235
CMD ["sleep", "infinity"]
"#;
static NEXT_VOLUME_ID: AtomicU64 = AtomicU64::new(0);
struct VolumeGuard {
docker: Docker,
name: String,
}
impl VolumeGuard {
async fn create(driver: &str) -> Result<Self, String> {
let name = unique_volume_name(driver);
let docker = connect_container_api(driver).await?;
docker
.create_volume(VolumeCreateRequest {
name: Some(name.clone()),
..Default::default()
})
.await
.map_err(|err| format!("create {driver} volume {name}: {err}"))?;
Ok(Self { docker, name })
}
}
impl Drop for VolumeGuard {
fn drop(&mut self) {
let docker = self.docker.clone();
let name = self.name.clone();
tokio::spawn(async move {
let _ = docker
.remove_volume(
&name,
Some(RemoveVolumeOptionsBuilder::new().force(true).build()),
)
.await;
});
}
}
#[tokio::test]
async fn sandbox_mounts_existing_driver_config_volume() {
let driver = e2e_driver().expect("OPENSHELL_E2E_DRIVER must be set by the e2e wrapper");
assert!(
matches!(driver.as_str(), "docker" | "podman"),
"driver_config volume e2e requires docker or podman, got {driver}"
);
let volume = VolumeGuard::create(&driver)
.await
.expect("create named test volume");
seed_volume(&volume).await.expect("seed named test volume");
let driver_config = format!(
r#"{{"{driver}":{{"mounts":[{{"type":"volume","source":"{}","target":"{VOLUME_TARGET}","read_only":false}}]}}}}"#,
volume.name
);
let mut sandbox = SandboxGuard::create(&[
"--driver-config-json",
&driver_config,
"--",
"sh",
"-lc",
"set -eu; test \"$(cat /sandbox/e2e-volume/input.txt)\" = host-volume-ok; printf sandbox-volume-ok > /sandbox/e2e-volume/output.txt; cat /sandbox/e2e-volume/output.txt",
])
.await
.expect("sandbox create with driver-config volume");
assert!(
sandbox.create_output.contains("sandbox-volume-ok"),
"sandbox should read and write the mounted volume:\n{}",
sandbox.create_output
);
sandbox.cleanup().await;
verify_volume(&volume)
.await
.expect("verify sandbox wrote to named test volume");
}
#[tokio::test]
#[cfg(feature = "e2e-docker")]
async fn oci_workspace_preparation_skips_nested_volume_ownership() {
let driver = e2e_driver().expect("OPENSHELL_E2E_DRIVER must be set by the e2e wrapper");
assert!(
driver == "docker",
"OCI workspace mount e2e requires docker, got {driver}"
);
let volume = VolumeGuard::create(&driver)
.await
.expect("create named test volume");
seed_volume(&volume).await.expect("seed named test volume");
let image_context = tempfile::tempdir().expect("create OCI image context");
let dockerfile = image_context.path().join("Dockerfile");
fs::write(&dockerfile, OCI_USER_DOCKERFILE).expect("write OCI image Dockerfile");
let image = ImageGuard::build(
&format!("{driver}-oci-user"),
&dockerfile,
image_context.path(),
)
.expect("build OCI-user image with selected container engine");
let driver_config = format!(
r#"{{"{driver}":{{"mounts":[{{"type":"volume","source":"{}","target":"{OCI_VOLUME_TARGET}","read_only":false}}]}}}}"#,
volume.name
);
let mut sandbox = SandboxGuard::create_keep_with_args(
&[
"--from",
image.tag(),
"--driver-config-json",
&driver_config,
"--no-tty",
],
&[
"sh",
"-lc",
"set -eu; test \"$(id -u):$(id -g)\" = 2234:2235; \
test \"$(pwd -P)\" = /workspace/project; test \"$HOME\" = /workspace/project; \
test \"$(stat -c %u:%g /workspace/project/e2e-volume/input.txt)\" = 0:0; \
touch direct-write; echo Ready; sleep infinity",
],
"Ready",
)
.await
.expect("create OCI-user sandbox with nested volume");
let ssh_output = sandbox
.exec(&[
"sh",
"-lc",
"set -eu; test \"$(pwd -P)\" = /workspace/project; \
test \"$HOME\" = /workspace/project; \
test \"$(stat -c %u:%g /workspace/project/e2e-volume/input.txt)\" = 0:0; \
touch ssh-write; echo nested-mount-owner-ok",
])
.await
.expect("SSH child should preserve nested volume ownership");
assert!(
ssh_output.contains("nested-mount-owner-ok"),
"expected nested mount ownership marker:\n{ssh_output}"
);
sandbox.cleanup().await;
verify_volume_ownership(&volume)
.await
.expect("nested volume ownership should remain unchanged");
}
#[tokio::test]
async fn sandbox_mounts_enabled_driver_config_bind() {
let driver = e2e_driver().expect("OPENSHELL_E2E_DRIVER must be set by the e2e wrapper");
assert!(
matches!(driver.as_str(), "docker" | "podman"),
"driver_config bind e2e requires docker or podman, got {driver}"
);
let cwd = std::env::current_dir().expect("resolve current dir");
let host_dir = tempfile::Builder::new()
.prefix("openshell-e2e-driver-config-bind-")
.tempdir_in(cwd)
.expect("create bind mount host dir");
fs::set_permissions(host_dir.path(), fs::Permissions::from_mode(0o777))
.expect("make bind mount host dir writable by sandbox user");
let input_path = host_dir.path().join("input.txt");
fs::write(&input_path, "host-bind-ok").expect("seed bind mount host dir");
fs::set_permissions(&input_path, fs::Permissions::from_mode(0o666))
.expect("make bind mount input readable by sandbox user");
let bind_source = bind_mount_source_path(&driver, host_dir.path());
let bind_mount = serde_json::json!({
"type": "bind",
"source": bind_source,
"target": BIND_TARGET,
"read_only": false,
"selinux_label": "private"
});
let driver_config = driver_config_mount_json(&driver, &bind_mount);
// Host bind mounts are explicitly unsafe: this test validates driver mount
// wiring, not Landlock enforcement over Docker Desktop's fakeowner mounts.
let policy = write_bind_mount_policy().expect("write bind mount policy");
let policy_path = policy.path().to_str().expect("policy path must be utf-8");
let mut sandbox = SandboxGuard::create(&[
"--policy",
policy_path,
"--driver-config-json",
&driver_config,
"--",
"sh",
"-lc",
"set -eu; test \"$(cat /sandbox/e2e-bind/input.txt)\" = host-bind-ok; printf sandbox-bind-ok > /sandbox/e2e-bind/output.txt; cat /sandbox/e2e-bind/output.txt",
])
.await
.expect("sandbox create with driver-config bind mount");
assert!(
sandbox.create_output.contains("sandbox-bind-ok"),
"sandbox should read and write the bind mount:\n{}",
sandbox.create_output
);
sandbox.cleanup().await;
let output = fs::read_to_string(host_dir.path().join("output.txt"))
.expect("read sandbox output from bind mount host dir");
assert_eq!(output, "sandbox-bind-ok");
}
fn write_bind_mount_policy() -> Result<tempfile::NamedTempFile, String> {
let mut file =
tempfile::NamedTempFile::new().map_err(|err| format!("create bind policy: {err}"))?;
file.write_all(
br#"version: 1
filesystem_policy:
include_workdir: false
read_only:
- "/bin"
- "/dev"
- "/etc"
- "/lib"
- "/proc"
- "/usr"
read_write:
- "/sandbox/e2e-bind"
- "/tmp"
landlock:
compatibility: best_effort
process:
run_as_user: sandbox
run_as_group: sandbox
"#,
)
.map_err(|err| format!("write bind policy: {err}"))?;
Ok(file)
}
async fn seed_volume(volume: &VolumeGuard) -> Result<(), String> {
run_volume_container(
volume,
"seed",
false,
"set -eu; chmod 0777 /vol; printf host-volume-ok > /vol/input.txt",
)
.await?;
Ok(())
}
async fn verify_volume(volume: &VolumeGuard) -> Result<(), String> {
let output = run_volume_container(
volume,
"verify",
true,
"set -eu; test \"$(cat /vol/input.txt)\" = host-volume-ok; test \"$(cat /vol/output.txt)\" = sandbox-volume-ok; echo volume-ok",
)
.await?;
if !output.contains("volume-ok") {
return Err(format!(
"volume verification did not print expected marker:\n{output}"
));
}
Ok(())
}
#[cfg(feature = "e2e-docker")]
async fn verify_volume_ownership(volume: &VolumeGuard) -> Result<(), String> {
let output = run_volume_container(
volume,
"verify-owner",
true,
"set -eu; test \"$(stat -c %u:%g /vol/input.txt)\" = 0:0; echo owner-ok",
)
.await?;
if !output.contains("owner-ok") {
return Err(format!(
"volume ownership verification did not print expected marker:\n{output}"
));
}
Ok(())
}
async fn run_volume_container(
volume: &VolumeGuard,
purpose: &str,
read_only: bool,
script: &str,
) -> Result<String, String> {
ensure_test_image(&volume.docker).await?;
let container_name = format!("{}-{purpose}", volume.name);
let create_options = CreateContainerOptionsBuilder::new()
.name(&container_name)
.build();
let host_config = HostConfig {
mounts: Some(vec![Mount {
target: Some("/vol".to_string()),
source: Some(volume.name.clone()),
typ: Some(MountTypeEnum::VOLUME),
read_only: Some(read_only),
..Default::default()
}]),
..Default::default()
};
volume
.docker
.create_container(
Some(create_options),
ContainerCreateBody {
image: Some(TEST_IMAGE.to_string()),
user: Some("0:0".to_string()),
entrypoint: Some(vec!["sh".to_string()]),
cmd: Some(vec!["-lc".to_string(), script.to_string()]),
attach_stdout: Some(true),
attach_stderr: Some(true),
host_config: Some(host_config),
..Default::default()
},
)
.await
.map_err(|err| format!("create helper container {container_name}: {err}"))?;
let result = run_created_container(volume, &container_name).await;
let remove_result = volume
.docker
.remove_container(&container_name, None::<RemoveContainerOptions>)
.await;
match (result, remove_result) {
(Ok(output), Ok(())) => Ok(output),
(Ok(_), Err(err)) => Err(format!("remove helper container {container_name}: {err}")),
(Err(err), Ok(())) => Err(err),
(Err(err), Err(remove_err)) => Err(format!(
"{err}\nremove helper container {container_name}: {remove_err}"
)),
}
}
async fn ensure_test_image(docker: &Docker) -> Result<(), String> {
if docker.inspect_image(TEST_IMAGE).await.is_ok() {
return Ok(());
}
let pull_events = docker
.create_image(
Some(
CreateImageOptionsBuilder::new()
.from_image(TEST_IMAGE)
.build(),
),
None,
None,
)
.try_collect::<Vec<_>>()
.await
.map_err(|err| format!("pull helper image {TEST_IMAGE}: {err}"))?;
let pull_errors = pull_events
.iter()
.filter_map(|event| {
event
.error_detail
.as_ref()
.and_then(|detail| detail.message.as_deref())
})
.collect::<Vec<_>>();
if pull_errors.is_empty() {
return Ok(());
}
Err(format!(
"pull helper image {TEST_IMAGE} failed:\n{}",
pull_errors.join("\n")
))
}
async fn run_created_container(
volume: &VolumeGuard,
container_name: &str,
) -> Result<String, String> {
volume
.docker
.start_container(container_name, None::<StartContainerOptions>)
.await
.map_err(|err| format!("start helper container {container_name}: {err}"))?;
let wait_result = volume
.docker
.wait_container(container_name, None::<WaitContainerOptions>)
.try_collect::<Vec<_>>()
.await;
let logs = volume
.docker
.logs(
container_name,
Some(LogsOptions {
stdout: true,
stderr: true,
tail: "all".to_string(),
..Default::default()
}),
)
.try_collect::<Vec<_>>()
.await
.map(|chunks| {
chunks
.into_iter()
.map(|chunk| chunk.to_string())
.collect::<String>()
});
match (wait_result, logs) {
(Ok(_), Ok(output)) => Ok(output),
(Ok(_), Err(err)) => Err(format!(
"read helper container {container_name} logs: {err}"
)),
(Err(err), Ok(output)) => Err(format!(
"helper container {container_name} failed: {err}\n{output}"
)),
(Err(err), Err(log_err)) => Err(format!(
"helper container {container_name} failed: {err}\nread logs failed: {log_err}"
)),
}
}
async fn connect_container_api(driver: &str) -> Result<Docker, String> {
let docker = match driver {
"docker" => Docker::connect_with_local_defaults()
.map_err(|err| format!("connect to Docker API: {err}"))?,
"podman" => {
let socket = podman_socket_path()?;
let socket_display = socket.display().to_string();
Docker::connect_with_unix(
socket
.to_str()
.ok_or_else(|| format!("podman socket path is not UTF-8: {socket_display}"))?,
120,
bollard::API_DEFAULT_VERSION,
)
.map_err(|err| format!("connect to Podman Docker-compatible API: {err}"))?
}
other => return Err(format!("unsupported e2e driver for volume API: {other}")),
};
docker
.ping()
.await
.map_err(|err| format!("ping {driver} Docker-compatible API: {err}"))?;
Ok(docker)
}
fn podman_socket_path() -> Result<PathBuf, String> {
let path = std::env::var_os("OPENSHELL_PODMAN_SOCKET").ok_or_else(|| {
"OPENSHELL_PODMAN_SOCKET must be set by e2e/with-podman-gateway.sh".to_string()
})?;
Ok(PathBuf::from(path))
}
fn unique_volume_name(driver: &str) -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock should be after Unix epoch")
.as_nanos();
let sequence = NEXT_VOLUME_ID.fetch_add(1, Ordering::Relaxed);
format!(
"openshell-e2e-driver-config-volume-{driver}-{}-{nanos}-{sequence}",
std::process::id()
)
}
fn driver_config_mount_json(driver: &str, mount: &Value) -> String {
let mut root = Map::new();
root.insert(
driver.to_string(),
serde_json::json!({
"mounts": [mount]
}),
);
Value::Object(root).to_string()
}
fn bind_mount_source_path(driver: &str, path: &Path) -> PathBuf {
if driver == "docker" {
github_actions_host_work_path(path).unwrap_or_else(|| path.to_path_buf())
} else {
path.to_path_buf()
}
}
fn github_actions_host_work_path(path: &Path) -> Option<PathBuf> {
if std::env::var("GITHUB_ACTIONS").ok().as_deref() != Some("true") {
return None;
}
let relative = path.strip_prefix("/__w").ok()?;
let mapped = Path::new("/home/runner/_work").join(relative);
mapped.exists().then_some(mapped)
}