Skip to content

Commit 4df50f8

Browse files
committed
fix(docker): preserve GPU access in sandbox runtime
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent 84429e4 commit 4df50f8

4 files changed

Lines changed: 144 additions & 9 deletions

File tree

‎crates/openshell-driver-docker/src/isolation.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::net::IpAddr;
1212
use std::path::PathBuf;
1313

1414
use openshell_isolation_interface::contract::{DriverFenceEvidence, ResolvedWorkloadIdentity};
15+
use openshell_sandbox_backend::GPU_RESOURCE_CLAIM;
1516
use openshell_sandbox_backend::boundary_protocol::{
1617
BoundaryConfig, BoundaryListener, GatewayVerificationKey, SandboxRuntimeDescriptor,
1718
SandboxTlsClientConfig, SandboxTlsServerConfig, SandboxTransport,
@@ -28,6 +29,7 @@ pub struct DockerBoundarySpec {
2829
pub verification_keys: Vec<GatewayVerificationKey>,
2930
pub container_id: String,
3031
pub image_identity: String,
32+
pub gpu_requested: bool,
3133
pub listener_socket: PathBuf,
3234
pub control_socket: PathBuf,
3335
pub sandbox_tls: SandboxTlsServerConfig,
@@ -48,10 +50,13 @@ impl DockerBoundarySpec {
4850
/// Docker coordinates so attach cannot bind a different container.
4951
#[must_use]
5052
pub fn provision(self) -> DockerBoundaryProvisioning {
51-
let resource_claims = BTreeMap::from([
53+
let mut resource_claims = BTreeMap::from([
5254
("docker.container_id".to_string(), self.container_id),
5355
("docker.image_identity".to_string(), self.image_identity),
5456
]);
57+
if self.gpu_requested {
58+
resource_claims.insert(GPU_RESOURCE_CLAIM.to_string(), "true".to_string());
59+
}
5560
let driver_fence = DriverFenceEvidence::Docker {
5661
container_id: resource_claims["docker.container_id"].clone(),
5762
network_mode: "none".to_string(),
@@ -116,6 +121,7 @@ mod tests {
116121
}],
117122
container_id: "sha256:container".to_string(),
118123
image_identity: "sha256:image".to_string(),
124+
gpu_requested: true,
119125
listener_socket: PathBuf::from("/run/openshell/boundary/control.sock"),
120126
control_socket: PathBuf::from("/host/control.sock"),
121127
sandbox_tls: SandboxTlsServerConfig {
@@ -147,6 +153,10 @@ mod tests {
147153
provisioned.runtime_descriptor.resource_claims["docker.container_id"],
148154
"sha256:container"
149155
);
156+
assert_eq!(
157+
provisioned.runtime_descriptor.resource_claims[GPU_RESOURCE_CLAIM],
158+
"true"
159+
);
150160
assert_eq!(
151161
provisioned.boundary_config.driver_fence,
152162
provisioned.runtime_descriptor.driver_fence

‎crates/openshell-driver-docker/src/lib.rs‎

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,6 +1617,7 @@ impl DockerComputeDriver {
16171617
&created.id,
16181618
&image,
16191619
&workload_identity,
1620+
gpu_devices.is_some(),
16201621
)
16211622
.await
16221623
{
@@ -4373,6 +4374,7 @@ async fn prepare_docker_boundary_files(
43734374
container_id: &str,
43744375
image: &DockerImageMetadata,
43754376
workload_identity: &ResolvedWorkloadIdentity,
4377+
gpu_requested: bool,
43764378
) -> Result<(), Status> {
43774379
let directory = docker_boundary_state_dir(sandbox, config)?;
43784380
let workspace_root = driver_mounts::resolve_oci_workspace_root(&image.working_dir)
@@ -4403,6 +4405,7 @@ async fn prepare_docker_boundary_files(
44034405
verification_keys,
44044406
container_id: container_id.to_string(),
44054407
image_identity: image.id.clone(),
4408+
gpu_requested,
44064409
listener_socket: PathBuf::from(BOUNDARY_SOCKET_MOUNT_PATH),
44074410
control_socket: PathBuf::from(BOUNDARY_SOCKET_MOUNT_PATH),
44084411
sandbox_tls: SandboxTlsServerConfig {
@@ -5044,6 +5047,7 @@ async fn spawn_docker_control_process(
50445047
let supervisor_id = created.id;
50455048
let monitored_supervisor_id = supervisor_id.clone();
50465049
let monitored_docker = failure_context.docker.clone();
5050+
let readiness_sandbox_id = failure_context.container_id.clone();
50475051
let task = tokio::spawn(async move {
50485052
let wait = async {
50495053
let mut stream = monitored_docker.wait_container(
@@ -5134,7 +5138,9 @@ async fn spawn_docker_control_process(
51345138
intentional_shutdown,
51355139
task,
51365140
};
5137-
if let Err(error) = wait_for_docker_supervisor_ready(docker, &supervisor_id).await {
5141+
if let Err(error) =
5142+
wait_for_docker_supervisor_ready(docker, &supervisor_id, &readiness_sandbox_id).await
5143+
{
51385144
stop_docker_control_process(process).await;
51395145
return Err(error);
51405146
}
@@ -5144,9 +5150,23 @@ async fn spawn_docker_control_process(
51445150
async fn wait_for_docker_supervisor_ready(
51455151
docker: &Docker,
51465152
supervisor_id: &str,
5153+
sandbox_id: &str,
51475154
) -> Result<(), Status> {
51485155
let wait = async {
51495156
loop {
5157+
let sandbox = docker
5158+
.inspect_container(sandbox_id, None)
5159+
.await
5160+
.map_err(|error| {
5161+
Status::internal(format!("inspect Docker sandbox container: {error}"))
5162+
})?;
5163+
if sandbox.state.unwrap_or_default().running == Some(false) {
5164+
let sandbox_log_tail = docker_container_log_tail(docker, sandbox_id).await;
5165+
return Err(Status::unavailable(format!(
5166+
"Docker sandbox exited before supervisor became ready{}",
5167+
format_named_log_tail("sandbox log tail", &sandbox_log_tail)
5168+
)));
5169+
}
51505170
let inspected = docker
51515171
.inspect_container(supervisor_id, None)
51525172
.await
@@ -5158,16 +5178,20 @@ async fn wait_for_docker_supervisor_ready(
51585178
Some(HealthStatusEnum::HEALTHY) => return Ok(()),
51595179
Some(HealthStatusEnum::UNHEALTHY) => {
51605180
let log_tail = docker_container_log_tail(docker, supervisor_id).await;
5181+
let sandbox_log_tail = docker_container_log_tail(docker, sandbox_id).await;
51615182
return Err(Status::unavailable(format!(
5162-
"Docker supervisor failed its readiness check{}",
5163-
format_log_tail(&log_tail)
5183+
"Docker supervisor failed its readiness check{}{}",
5184+
format_log_tail(&log_tail),
5185+
format_named_log_tail("sandbox log tail", &sandbox_log_tail)
51645186
)));
51655187
}
51665188
_ if state.running == Some(false) => {
51675189
let log_tail = docker_container_log_tail(docker, supervisor_id).await;
5190+
let sandbox_log_tail = docker_container_log_tail(docker, sandbox_id).await;
51685191
return Err(Status::unavailable(format!(
5169-
"Docker supervisor exited before becoming ready{}",
5170-
format_log_tail(&log_tail)
5192+
"Docker supervisor exited before becoming ready{}{}",
5193+
format_log_tail(&log_tail),
5194+
format_named_log_tail("sandbox log tail", &sandbox_log_tail)
51715195
)));
51725196
}
51735197
_ => tokio::time::sleep(Duration::from_millis(100)).await,
@@ -5179,19 +5203,25 @@ async fn wait_for_docker_supervisor_ready(
51795203
result
51805204
} else {
51815205
let log_tail = docker_container_log_tail(docker, supervisor_id).await;
5206+
let sandbox_log_tail = docker_container_log_tail(docker, sandbox_id).await;
51825207
Err(Status::deadline_exceeded(format!(
5183-
"Docker supervisor did not become ready within {} seconds{}",
5208+
"Docker supervisor did not become ready within {} seconds{}{}",
51845209
SUPERVISOR_READY_TIMEOUT.as_secs(),
5185-
format_log_tail(&log_tail)
5210+
format_log_tail(&log_tail),
5211+
format_named_log_tail("sandbox log tail", &sandbox_log_tail)
51865212
)))
51875213
}
51885214
}
51895215

51905216
fn format_log_tail(log_tail: &str) -> String {
5217+
format_named_log_tail("log tail", log_tail)
5218+
}
5219+
5220+
fn format_named_log_tail(label: &str, log_tail: &str) -> String {
51915221
if log_tail.is_empty() {
51925222
String::new()
51935223
} else {
5194-
format!("; log tail: {log_tail}")
5224+
format!("; {label}: {log_tail}")
51955225
}
51965226
}
51975227

‎crates/openshell-sandbox-backend/src/lib.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub use runtime::OpenShellRuntimeBackend;
1818
/// Stable isolation backend name implemented by `openshell-sandbox`.
1919
pub const BACKEND_NAME: &str = "openshell-sandbox";
2020

21+
/// Resource claim set by compute drivers when the workload requests GPU access.
22+
pub const GPU_RESOURCE_CLAIM: &str = "openshell.gpu";
23+
2124
/// Memory-backed parent used for supervisor CA material.
2225
pub const SUPERVISOR_CA_RUNTIME_ROOT: &str = "/run/openshell-supervisor-ca";
2326

‎crates/openshell-sandbox/src/boundary_server.rs‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ mod linux {
4040
CapabilityEvidence, ExecSession, LoopbackTarget, ResolvedWorkloadIdentity,
4141
SandboxConfirmEvidence,
4242
};
43+
use openshell_sandbox_backend::GPU_RESOURCE_CLAIM;
4344
use openshell_sandbox_backend::mediation::{
4445
self, DnsQueryWire, MediationFrame, MediationFrameKind,
4546
};
@@ -80,10 +81,86 @@ mod linux {
8081
const MAX_REPLAY_LEDGER_ENTRIES: usize = 4096;
8182
const MAX_RETAINED_EXEC_PROCESSES: usize = 64;
8283

84+
const GPU_BASELINE_READ_ONLY: &[&str] = &["/run/nvidia-persistenced", "/usr/lib/wsl"];
85+
const GPU_BASELINE_READ_WRITE: &[&str] = &[
86+
"/dev/nvidiactl",
87+
"/dev/nvidia-uvm",
88+
"/dev/nvidia-uvm-tools",
89+
"/dev/nvidia-modeset",
90+
"/dev/dxg",
91+
"/proc",
92+
];
93+
8394
fn duration_micros(duration: Duration) -> u64 {
8495
u64::try_from(duration.as_micros()).unwrap_or(u64::MAX)
8596
}
8697

98+
/// Add the filesystem paths required by GPU devices visible inside the
99+
/// workload container. The companion supervisor intentionally has no GPU
100+
/// devices, so it cannot discover these paths on the sandbox's behalf.
101+
fn enrich_gpu_filesystem_paths(
102+
policy: &mut openshell_core::policy::SandboxPolicy,
103+
gpu_requested: bool,
104+
) -> bool {
105+
if !gpu_requested {
106+
return false;
107+
}
108+
let has_gpu = Path::new("/dev/nvidiactl").exists() || Path::new("/dev/dxg").exists();
109+
if !has_gpu {
110+
return false;
111+
}
112+
113+
let mut read_write = GPU_BASELINE_READ_WRITE
114+
.iter()
115+
.copied()
116+
.map(std::path::PathBuf::from)
117+
.collect::<Vec<_>>();
118+
if let Ok(entries) = std::fs::read_dir("/dev") {
119+
read_write.extend(entries.flatten().filter_map(|entry| {
120+
let name = entry.file_name();
121+
let suffix = name.to_str()?.strip_prefix("nvidia")?;
122+
(!suffix.is_empty() && suffix.chars().all(|character| character.is_ascii_digit()))
123+
.then(|| entry.path())
124+
}));
125+
}
126+
127+
let mut modified = false;
128+
for path in GPU_BASELINE_READ_ONLY.iter().copied().map(Path::new) {
129+
if path.exists()
130+
&& !policy
131+
.filesystem
132+
.read_only
133+
.iter()
134+
.any(|allowed| allowed == path)
135+
&& !policy
136+
.filesystem
137+
.read_write
138+
.iter()
139+
.any(|allowed| allowed == path)
140+
{
141+
policy.filesystem.read_only.push(path.to_path_buf());
142+
modified = true;
143+
}
144+
}
145+
for path in read_write {
146+
if !path.exists() || policy.filesystem.read_write.contains(&path) {
147+
continue;
148+
}
149+
if policy.filesystem.read_only.contains(&path) {
150+
if path != Path::new("/proc") {
151+
continue;
152+
}
153+
policy
154+
.filesystem
155+
.read_only
156+
.retain(|allowed| allowed != &path);
157+
}
158+
policy.filesystem.read_write.push(path);
159+
modified = true;
160+
}
161+
modified
162+
}
163+
87164
struct ControlConnectionSlot(Arc<AtomicUsize>);
88165

89166
impl Drop for ControlConnectionSlot {
@@ -2247,6 +2324,21 @@ mod linux {
22472324
Err(error) => return guest_error(BoundaryErrorKind::Process, error),
22482325
};
22492326
let mut policy = policy.into();
2327+
let gpu_requested = self
2328+
.config
2329+
.resource_claims
2330+
.get(GPU_RESOURCE_CLAIM)
2331+
.is_some_and(|value| value == "true");
2332+
if enrich_gpu_filesystem_paths(&mut policy, gpu_requested) {
2333+
openshell_ocsf::ocsf_emit!(
2334+
openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
2335+
.severity(openshell_ocsf::SeverityId::Informational)
2336+
.status(openshell_ocsf::StatusId::Success)
2337+
.state(openshell_ocsf::StateId::Enabled, "enriched")
2338+
.message("Added workload-local GPU filesystem paths".to_string())
2339+
.build()
2340+
);
2341+
}
22502342
let driver_identity = DriverIdentity::Resolved {
22512343
uid: self.config.workload_identity.uid,
22522344
gid: self.config.workload_identity.gid,

0 commit comments

Comments
 (0)