Skip to content

Commit bb155ce

Browse files
committed
feat(core): add CDI policy resolver
Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent 0c29d8e commit bb155ce

16 files changed

Lines changed: 6510 additions & 123 deletions

File tree

‎Cargo.lock‎

Lines changed: 252 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ rustix = { version = "1.1", features = ["process"] }
8383
socket2 = "0.6"
8484

8585
# Serialization
86+
container-device-interface = { git = "https://github.com/cncf-tags/container-device-interface-rs", rev = "b8a056e92dbd159423c129cd306c93e410651b18" }
8687
serde = { version = "1", features = ["derive"] }
8788
serde_json = "1"
8889
serde_yml = { package = "noyalib", version = "0.0.28", default-features = false, features = ["std", "compat-serde-yaml"] }

‎crates/openshell-core/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ tempfile = { version = "3", optional = true }
4545
nix = { workspace = true }
4646
rustix = { workspace = true }
4747

48+
[target.'cfg(target_os = "linux")'.dependencies]
49+
container-device-interface = { workspace = true }
50+
4851
[features]
4952
default = ["telemetry"]
5053
## Compile in anonymous telemetry emission support. On by default; disable with

‎crates/openshell-core/src/cdi.rs‎

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Shared CDI context schema and resolver helpers.
5+
6+
use std::path::{Path, PathBuf};
7+
8+
use serde::{Deserialize, Serialize};
9+
10+
pub const CDI_CONTEXT_VERSION: u32 = 1;
11+
12+
/// Base workload-runtime path under which compute drivers mount CDI specification directories.
13+
pub const CDI_SPEC_DIR_BASE: &str = "/run/openshell/boundary/cdi-specs";
14+
15+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16+
pub struct CdiContext {
17+
pub version: u32,
18+
pub selected_devices: Vec<String>,
19+
pub spec_dirs: Vec<CdiSpecDirectory>,
20+
}
21+
22+
impl CdiContext {
23+
#[must_use]
24+
pub fn new(selected_devices: Vec<String>, spec_dirs: Vec<CdiSpecDirectory>) -> Self {
25+
Self {
26+
version: CDI_CONTEXT_VERSION,
27+
selected_devices,
28+
spec_dirs,
29+
}
30+
}
31+
}
32+
33+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34+
pub struct CdiSpecDirectory {
35+
pub path: String,
36+
pub source: String,
37+
}
38+
39+
impl CdiSpecDirectory {
40+
#[must_use]
41+
pub fn new(path: impl Into<String>, source: impl Into<String>) -> Self {
42+
Self {
43+
path: path.into(),
44+
source: source.into(),
45+
}
46+
}
47+
}
48+
49+
#[derive(Debug, Clone, Default, PartialEq, Eq)]
50+
pub struct CdiDerivedRequirements {
51+
pub device_node_paths: Vec<String>,
52+
/// Derived read-only policy paths. Shared-library mounts contribute their
53+
/// parent directory; other read-only mounts contribute their exact path.
54+
pub read_only_paths: Vec<String>,
55+
pub read_write_mount_paths: Vec<String>,
56+
pub additional_gids: Vec<u32>,
57+
}
58+
59+
#[derive(Debug, thiserror::Error)]
60+
pub enum CdiError {
61+
#[error("CDI policy resolution is unavailable on this platform")]
62+
UnsupportedPlatform,
63+
#[error("failed to read CDI context '{}': {source}", path.display())]
64+
ContextRead {
65+
path: PathBuf,
66+
source: std::io::Error,
67+
},
68+
#[error("failed to parse CDI context '{}': {source}", path.display())]
69+
ContextParse {
70+
path: PathBuf,
71+
source: serde_json::Error,
72+
},
73+
#[error("unsupported CDI context version {0}")]
74+
UnsupportedContextVersion(u32),
75+
#[error("CDI spec dir '{path}' from source '{diagnostic_source}' is unsafe: {reason}")]
76+
UnsafeSpecDir {
77+
path: String,
78+
diagnostic_source: String,
79+
reason: &'static str,
80+
},
81+
#[error("selected CDI device '{0}' was not found in mounted CDI specs")]
82+
MissingDevice(String),
83+
#[error(
84+
"selected CDI device '{device}' was not found in mounted CDI specs after CDI spec refresh reported: {refresh_error}"
85+
)]
86+
MissingDeviceAfterRefresh {
87+
device: String,
88+
refresh_error: String,
89+
},
90+
#[error("failed to merge CDI edits for '{device}': {error}")]
91+
EditMerge { device: String, error: String },
92+
#[error("failed to encode resolved CDI edits: {source}")]
93+
EditEncode { source: serde_json::Error },
94+
#[error("failed to decode resolved CDI edits: {source}")]
95+
EditDecode { source: serde_json::Error },
96+
#[error("CDI-derived path '{path}' is unsafe: {reason}")]
97+
UnsafePolicyPath { path: String, reason: &'static str },
98+
#[error("CDI path '{path}' requested conflicting access modes")]
99+
ConflictingAccess { path: String },
100+
#[error(
101+
"CDI writable mount '{path}' is not explicitly listed in the sandbox policy read_write paths"
102+
)]
103+
WritableMountNotAllowed { path: String },
104+
#[error("CDI writable mount '{path}' must target a single file, found {kind}")]
105+
WritableMountNotFile { path: String, kind: String },
106+
#[error("CDI device node '{path}' must target a character or block device, found {kind}")]
107+
DeviceNodeNotDevice { path: String, kind: String },
108+
#[error("CDI additionalGids must not contain root GID 0")]
109+
RootAdditionalGid,
110+
#[error("CDI mount '{path}' has conflicting ro/rw options")]
111+
ConflictingMountOptions { path: String },
112+
}
113+
114+
pub fn read_context(path: impl AsRef<Path>) -> Result<CdiContext, CdiError> {
115+
let path = path.as_ref();
116+
let json = std::fs::read_to_string(path).map_err(|source| CdiError::ContextRead {
117+
path: path.to_path_buf(),
118+
source,
119+
})?;
120+
serde_json::from_str(&json).map_err(|source| CdiError::ContextParse {
121+
path: path.to_path_buf(),
122+
source,
123+
})
124+
}
125+
126+
#[cfg(any(target_os = "linux", test))]
127+
fn shared_library_parent(path: &str) -> Option<String> {
128+
let path = Path::new(path);
129+
let name = path.file_name()?.to_str()?;
130+
let versioned_library = name.rsplit_once(".so.").is_some_and(|(_, version)| {
131+
version.split('.').all(|component| {
132+
!component.is_empty() && component.bytes().all(|byte| byte.is_ascii_digit())
133+
})
134+
});
135+
let unversioned_library = name
136+
.rsplit_once('.')
137+
.is_some_and(|(_, extension)| extension == "so");
138+
if !unversioned_library && !versioned_library {
139+
return None;
140+
}
141+
path.parent()?.to_str().map(ToOwned::to_owned)
142+
}
143+
144+
#[cfg(test)]
145+
mod tests {
146+
use super::shared_library_parent;
147+
148+
#[test]
149+
fn identifies_shared_library_parent_directories() {
150+
for path in [
151+
"/opt/nvidia/lib/libcuda.so",
152+
"/opt/nvidia/lib/libnvidia-ml.so.1",
153+
"/opt/nvidia/lib/libnvidia-eglcore.so.540.5.0",
154+
] {
155+
assert_eq!(
156+
shared_library_parent(path).as_deref(),
157+
Some("/opt/nvidia/lib")
158+
);
159+
}
160+
for path in [
161+
"/etc/ld.so.conf",
162+
"/opt/nvidia/lib/libcuda.so.debug",
163+
"/opt/nvidia/lib/libcuda.so.1beta",
164+
"/usr/bin/nvidia-smi",
165+
] {
166+
assert_eq!(shared_library_parent(path), None);
167+
}
168+
}
169+
}
170+
171+
#[cfg(target_os = "linux")]
172+
#[path = "cdi_linux.rs"]
173+
mod cdi_linux;
174+
#[cfg(target_os = "linux")]
175+
pub use cdi_linux::{resolve_cdi_context, validate_cdi_requirements};
176+
177+
#[cfg(not(target_os = "linux"))]
178+
#[path = "cdi_stub.rs"]
179+
mod cdi_stub;
180+
#[cfg(not(target_os = "linux"))]
181+
pub use cdi_stub::{resolve_cdi_context, validate_cdi_requirements};

0 commit comments

Comments
 (0)