Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions crates/openshell-core/src/gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,62 @@ fn cdi_nvidia_gpu_suffix(id: &str) -> Option<&str> {
id.strip_prefix(CDI_NVIDIA_GPU_PREFIX)
}

// Local equivalent of the upstream CDI parser until its public API is released:
// https://github.com/cncf-tags/container-device-interface-rs/pull/177
fn is_qualified_name(device: &str) -> bool {
let Some((kind, name)) = device.split_once('=') else {
return false;
};
let Some((vendor, class)) = kind.split_once('/') else {
return false;
};
let valid_component = |value: &str, device_name: bool| {
let bytes = value.as_bytes();
bytes.first().is_some_and(|byte| {
if device_name {
byte.is_ascii_alphanumeric()
} else {
byte.is_ascii_alphabetic()
}
}) && bytes.last().is_some_and(u8::is_ascii_alphanumeric)
&& bytes.iter().all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(byte, b'_' | b'-' | b'.')
|| (device_name && *byte == b':')
})
};
valid_component(vendor, false) && valid_component(class, false) && valid_component(name, true)
}

/// Validate vendor-agnostic CDI qualified names without resolving or authorizing devices.
///
/// # Errors
/// Returns an error for host paths or malformed `<vendor>/<class>=<name>` selectors.
pub fn validate_cdi_device_names(devices: &[String], field: &str) -> Result<(), String> {
if devices.iter().any(|device| !is_qualified_name(device)) {
return Err(format!(
"{field} must contain CDI qualified names (<vendor>/<class>=<name>)"
));
}
Ok(())
}

/// Validate explicit CDI selectors and their GPU request requirements.
///
/// Docker and Podman use this helper; drivers with other identifier formats use
/// [`validate_specific_gpu_device_request`] directly.
///
/// # Errors
/// Returns an error for malformed CDI names or invalid GPU request requirements.
pub fn validate_cdi_gpu_device_request(
gpu: Option<&DriverGpuResourceRequirements>,
devices: &[String],
field: &str,
) -> Result<(), String> {
validate_cdi_device_names(devices, field)?;
validate_specific_gpu_device_request(gpu, devices, field)
}

/// Validate a compute-driver GPU request against driver-owned specific devices.
///
/// Drivers call this when a sandbox request combines portable GPU requirements
Expand Down Expand Up @@ -347,6 +403,64 @@ pub fn validate_specific_gpu_device_request(
mod tests {
use super::*;

#[test]
fn cdi_device_names_accept_vendor_agnostic_selectors() {
for device in [
"nvidia.com/gpu=0",
"nvidia.com/gpu=all",
"nvidia.com/gpu=GPU-5b2d",
"nvidia.com/gpu=MIG-5b2d",
"nvidia.com/gpu=0:1",
"example.com/accelerator=card_0.1",
"intel.com/gpu=0",
"amd.com/gpu=0",
"v/c=0",
] {
let devices = vec![device.to_string()];
validate_cdi_device_names(&devices, "driver_config.cdi_devices")
.unwrap_or_else(|error| panic!("{device:?}: {error}"));
}
}

#[test]
fn cdi_device_names_reject_host_paths_and_malformed_selectors() {
for device in [
"",
"/dev/sda",
"/dev",
"/dev/sda:/dev/sda:rwm",
"dev/sda",
"../dev/sda",
"vendor/gpu",
"/gpu=0",
"vendor/=0",
"vendor/gpu=",
"vendor/gpu=/dev/sda",
"vendor/gpu=0/1",
"vendor/gpu=0=1",
"vendor/gpu=0,1",
"vendor/gpu=-0",
"vendor/gpu=0:",
"vendor/gpu=0 ",
" vendor/gpu=0",
"vendor/gpu=0\n",
"vendor/gpu=0\0",
"vendor/gpu=é",
"véndor/gpu=0",
"vendor/gpü=0",
"0vendor/gpu=0",
"vendor/0gpu=0",
"vendor./gpu=0",
"vendor/gpu-=0",
"vendor/gpu/other=0",
] {
let error =
validate_cdi_device_names(&[device.to_string()], "driver_config.cdi_devices")
.unwrap_err();
assert!(error.contains("CDI qualified names"), "{device:?}: {error}");
}
}

#[test]
fn effective_driver_gpu_count_normalizes_missing_count() {
let gpu = DriverGpuResourceRequirements { count: None };
Expand Down Expand Up @@ -547,6 +661,18 @@ mod tests {
);
}

#[test]
fn specific_gpu_device_request_preserves_non_cdi_identifiers() {
let gpu = DriverGpuResourceRequirements { count: Some(1) };
let devices = vec!["0000:2d:00.0".to_string()];
validate_specific_gpu_device_request(Some(&gpu), &devices, "driver_config.gpu_device_ids")
.expect("generic GPU request validation must accept PCI device identifiers");
let error =
validate_cdi_gpu_device_request(Some(&gpu), &devices, "driver_config.cdi_devices")
.expect_err("CDI drivers must reject non-CDI identifiers");
assert!(error.contains("CDI qualified names"));
}

#[test]
fn validate_specific_gpu_device_request_ignores_empty_devices() {
validate_specific_gpu_device_request(None, &[], "driver_config.cdi_devices")
Expand Down
18 changes: 9 additions & 9 deletions crates/openshell-driver-docker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use openshell_core::driver_utils::{
};
use openshell_core::gpu::{
CdiGpuDefaultSelector, CdiGpuInventory, CdiGpuSelectionError, driver_gpu_requirements,
effective_driver_gpu_count, validate_specific_gpu_device_request,
effective_driver_gpu_count, validate_cdi_device_names, validate_cdi_gpu_device_request,
};
use openshell_core::progress::{
PROGRESS_STEP_PULLING_IMAGE, PROGRESS_STEP_REQUESTING_SANDBOX, PROGRESS_STEP_STARTING_SANDBOX,
Expand Down Expand Up @@ -1126,7 +1126,7 @@ impl DockerComputeDriver {
}

if let Some(cdi_devices) = driver_config.cdi_devices.as_deref() {
validate_specific_gpu_device_request(
validate_cdi_gpu_device_request(
gpu_requirements,
cdi_devices,
"driver_config.cdi_devices",
Expand Down Expand Up @@ -1309,7 +1309,7 @@ impl DockerComputeDriver {
) -> Result<Vec<String>, CdiGpuSelectionError>,
) -> Result<Option<Vec<String>>, Status> {
if let Some(cdi_devices) = driver_config.cdi_devices.as_deref() {
validate_specific_gpu_device_request(
validate_cdi_gpu_device_request(
gpu_requirements,
cdi_devices,
"driver_config.cdi_devices",
Expand Down Expand Up @@ -5618,12 +5618,8 @@ fn build_container_create_body(
.as_ref()
.and_then(|spec| driver_gpu_requirements(spec.resource_requirements.as_ref()));
let cdi_devices = if let Some(cdi_devices) = driver_config.cdi_devices.as_ref() {
validate_specific_gpu_device_request(
gpu_requirements,
cdi_devices,
"driver_config.cdi_devices",
)
.map_err(Status::invalid_argument)?;
validate_cdi_gpu_device_request(gpu_requirements, cdi_devices, "driver_config.cdi_devices")
.map_err(Status::invalid_argument)?;
Some(cdi_devices.as_slice())
} else {
None
Expand Down Expand Up @@ -5674,6 +5670,10 @@ fn build_container_create_body_for_image(
image: &DockerImageMetadata,
workload_identity: &ResolvedWorkloadIdentity,
) -> Result<ContainerCreateBody, Status> {
if let Some(device_ids) = gpu_device_ids {
validate_cdi_device_names(device_ids, "driver_config.cdi_devices")
.map_err(Status::invalid_argument)?;
}
let spec = sandbox
.spec
.as_ref()
Expand Down
85 changes: 85 additions & 0 deletions crates/openshell-driver-docker/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2719,6 +2719,91 @@ fn build_container_create_body_omits_devices_without_resolved_default_cdi_device
);
}

#[test]
fn container_spec_accepts_non_nvidia_explicit_and_resolved_cdi_devices() {
let mut config = runtime_config();
config.gpu.cdi_supported = true;
let mut sandbox = test_sandbox();
sandbox.spec.as_mut().unwrap().resource_requirements = Some(gpu_resources(None));
for device in ["example.com/gpu=0", "intel.com/gpu=0", "amd.com/gpu=0"] {
sandbox
.spec
.as_mut()
.unwrap()
.template
.as_mut()
.unwrap()
.driver_config = Some(cdi_devices_config(&[device]));
let explicit = build_container_create_body(&sandbox, &config).unwrap();
sandbox
.spec
.as_mut()
.unwrap()
.template
.as_mut()
.unwrap()
.driver_config = None;
let resolved = build_container_create_body_with_gpu_devices(
&sandbox,
&config,
&DockerSandboxDriverConfig::default(),
Some(&[device.to_string()]),
)
.unwrap();
for body in [explicit, resolved] {
let requests = body.host_config.unwrap().device_requests.unwrap();
assert_eq!(requests[0].driver.as_deref(), Some("cdi"));
assert_eq!(
requests[0].device_ids.as_ref().unwrap(),
&[device.to_string()]
);
}
}
}

#[test]
fn container_spec_rejects_host_paths_in_explicit_and_resolved_cdi_devices() {
let mut config = runtime_config();
config.gpu.cdi_supported = true;
let mut sandbox = test_sandbox();
sandbox.spec.as_mut().unwrap().resource_requirements = Some(gpu_resources(None));
for device in [
"/dev/sda",
"/dev",
"/dev/sda:/dev/sda:rwm",
"vendor/gpu=0/1",
] {
sandbox
.spec
.as_mut()
.unwrap()
.template
.as_mut()
.unwrap()
.driver_config = Some(cdi_devices_config(&[device]));
let explicit_error = build_container_create_body(&sandbox, &config).unwrap_err();
sandbox
.spec
.as_mut()
.unwrap()
.template
.as_mut()
.unwrap()
.driver_config = None;
let resolved_error = build_container_create_body_with_gpu_devices(
&sandbox,
&config,
&DockerSandboxDriverConfig::default(),
Some(&[device.to_string()]),
)
.unwrap_err();
for error in [explicit_error, resolved_error] {
assert_eq!(error.code(), tonic::Code::InvalidArgument);
assert!(error.message().contains("CDI qualified names"));
}
}
}

#[test]
fn build_container_create_body_passes_explicit_cdi_device_id_through() {
let mut config = runtime_config();
Expand Down
Loading
Loading