Skip to content

Commit a81daf0

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

9 files changed

Lines changed: 1272 additions & 2 deletions

File tree

‎Cargo.lock‎

Lines changed: 267 additions & 1 deletion
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
@@ -78,6 +78,7 @@ rustix = { version = "1.1", features = ["process"] }
7878
socket2 = "0.6"
7979

8080
# Serialization
81+
container-device-interface = { git = "https://github.com/cncf-tags/container-device-interface-rs", rev = "06644c1b167c8dcc247816590901e0a819636b01" }
8182
serde = { version = "1", features = ["derive"] }
8283
serde_json = "1"
8384
serde_yml = "0.0.12"

‎crates/openshell-core/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ tempfile = { version = "3", optional = true }
3636
[target.'cfg(unix)'.dependencies]
3737
nix = { workspace = true }
3838

39+
[target.'cfg(target_os = "linux")'.dependencies]
40+
container-device-interface = { workspace = true }
41+
3942
[features]
4043
default = ["telemetry"]
4144
## Compile in anonymous telemetry emission support. On by default; disable with

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

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13+
pub struct CdiContext {
14+
pub version: u32,
15+
pub selected_devices: Vec<String>,
16+
pub spec_dirs: Vec<CdiSpecDirectory>,
17+
}
18+
19+
impl CdiContext {
20+
#[must_use]
21+
pub fn new(selected_devices: Vec<String>, spec_dirs: Vec<CdiSpecDirectory>) -> Self {
22+
Self {
23+
version: CDI_CONTEXT_VERSION,
24+
selected_devices,
25+
spec_dirs,
26+
}
27+
}
28+
}
29+
30+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31+
pub struct CdiSpecDirectory {
32+
pub path: String,
33+
pub source: String,
34+
}
35+
36+
impl CdiSpecDirectory {
37+
#[must_use]
38+
pub fn new(path: impl Into<String>, source: impl Into<String>) -> Self {
39+
Self {
40+
path: path.into(),
41+
source: source.into(),
42+
}
43+
}
44+
}
45+
46+
#[derive(Debug, Clone, Default, PartialEq, Eq)]
47+
pub struct CdiDerivedRequirements {
48+
pub device_node_paths: Vec<String>,
49+
pub read_only_mount_paths: Vec<String>,
50+
pub read_write_mount_paths: Vec<String>,
51+
pub additional_gids: Vec<u32>,
52+
}
53+
54+
#[derive(Debug, thiserror::Error)]
55+
pub enum CdiError {
56+
#[error("CDI policy resolution is unavailable on this platform")]
57+
UnsupportedPlatform,
58+
#[error("failed to read CDI context '{}': {source}", path.display())]
59+
ContextRead {
60+
path: PathBuf,
61+
source: std::io::Error,
62+
},
63+
#[error("failed to parse CDI context '{}': {source}", path.display())]
64+
ContextParse {
65+
path: PathBuf,
66+
source: serde_json::Error,
67+
},
68+
#[error("unsupported CDI context version {0}")]
69+
UnsupportedContextVersion(u32),
70+
#[error("CDI spec dir '{path}' from source '{diagnostic_source}' is unsafe: {reason}")]
71+
UnsafeSpecDir {
72+
path: String,
73+
diagnostic_source: String,
74+
reason: &'static str,
75+
},
76+
#[error("selected CDI device '{0}' was not found in mounted CDI specs")]
77+
MissingDevice(String),
78+
#[error(
79+
"selected CDI device '{device}' was not found in mounted CDI specs after CDI spec refresh reported: {refresh_error}"
80+
)]
81+
MissingDeviceAfterRefresh {
82+
device: String,
83+
refresh_error: String,
84+
},
85+
#[error("failed to merge CDI edits for '{device}': {error}")]
86+
EditMerge { device: String, error: String },
87+
#[error("failed to encode resolved CDI edits: {source}")]
88+
EditEncode { source: serde_json::Error },
89+
#[error("failed to decode resolved CDI edits: {source}")]
90+
EditDecode { source: serde_json::Error },
91+
#[error("CDI-derived path '{path}' is unsafe: {reason}")]
92+
UnsafePolicyPath { path: String, reason: &'static str },
93+
#[error("CDI path '{path}' requested conflicting access modes")]
94+
ConflictingAccess { path: String },
95+
#[error(
96+
"CDI writable mount '{path}' is not explicitly listed in the sandbox policy read_write paths"
97+
)]
98+
WritableMountNotAllowed { path: String },
99+
#[error("CDI writable mount '{path}' must target a single file, found {kind}")]
100+
WritableMountNotFile { path: String, kind: String },
101+
#[error("CDI device node '{path}' must target a character or block device, found {kind}")]
102+
DeviceNodeNotDevice { path: String, kind: String },
103+
#[error("CDI mount '{path}' has conflicting ro/rw options")]
104+
ConflictingMountOptions { path: String },
105+
}
106+
107+
pub fn read_context(path: impl AsRef<Path>) -> Result<CdiContext, CdiError> {
108+
let path = path.as_ref();
109+
let json = std::fs::read_to_string(path).map_err(|source| CdiError::ContextRead {
110+
path: path.to_path_buf(),
111+
source,
112+
})?;
113+
serde_json::from_str(&json).map_err(|source| CdiError::ContextParse {
114+
path: path.to_path_buf(),
115+
source,
116+
})
117+
}
118+
119+
#[cfg(target_os = "linux")]
120+
#[path = "cdi_linux.rs"]
121+
mod cdi_linux;
122+
#[cfg(target_os = "linux")]
123+
pub use cdi_linux::resolve_cdi_context;
124+
125+
#[cfg(not(target_os = "linux"))]
126+
#[path = "cdi_stub.rs"]
127+
mod cdi_stub;
128+
#[cfg(not(target_os = "linux"))]
129+
pub use cdi_stub::resolve_cdi_context;

0 commit comments

Comments
 (0)