Skip to content

Commit 386bbb5

Browse files
committed
fix(core): reject unsafe CDI requirements
Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent ab01f71 commit 386bbb5

2 files changed

Lines changed: 61 additions & 8 deletions

File tree

crates/openshell-core/src/cdi.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ pub enum CdiError {
8282
device: String,
8383
refresh_error: String,
8484
},
85+
#[error("selected CDI device '{0}' is defined by multiple mounted CDI specs")]
86+
DuplicateDevice(String),
8587
#[error("failed to merge CDI edits for '{device}': {error}")]
8688
EditMerge { device: String, error: String },
8789
#[error("failed to encode resolved CDI edits: {source}")]
@@ -100,6 +102,8 @@ pub enum CdiError {
100102
WritableMountNotFile { path: String, kind: String },
101103
#[error("CDI device node '{path}' must target a character or block device, found {kind}")]
102104
DeviceNodeNotDevice { path: String, kind: String },
105+
#[error("CDI additionalGids must not contain root GID 0")]
106+
RootAdditionalGid,
103107
#[error("CDI mount '{path}' has conflicting ro/rw options")]
104108
ConflictingMountOptions { path: String },
105109
}

crates/openshell-core/src/cdi_linux.rs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,12 @@ impl RequirementAccumulator {
116116
}
117117
}
118118

119-
fn add_gid(&mut self, gid: u32) {
119+
fn add_gid(&mut self, gid: u32) -> Result<(), CdiError> {
120120
if gid == 0 {
121-
tracing::debug!("Skipping CDI additionalGids entry for root GID 0");
122-
return;
121+
return Err(CdiError::RootAdditionalGid);
123122
}
124123
self.additional_gids.insert(gid);
124+
Ok(())
125125
}
126126

127127
fn validate_writable_mounts<F>(
@@ -235,6 +235,9 @@ fn resolve_container_edits(
235235
selected_devices: &[String],
236236
) -> Result<CdiContainerEdits, CdiError> {
237237
let (mut cache, refresh_error) = build_cache(&context.spec_dirs);
238+
if let Some(device) = conflicting_selected_device(refresh_error.as_deref(), selected_devices) {
239+
return Err(CdiError::DuplicateDevice(device));
240+
}
238241
let mut merged = UpstreamContainerEdits::new();
239242
let mut applied_specs = BTreeSet::new();
240243

@@ -268,6 +271,21 @@ fn resolve_container_edits(
268271
serde_json::from_value(value).map_err(|source| CdiError::EditDecode { source })
269272
}
270273

274+
// Temporary compatibility check for the Rust CDI cache revision pinned by
275+
// OpenShell. That revision records same-priority device conflicts but leaves
276+
// the conflicted device resolvable. Remove this once OpenShell uses the
277+
// upstream fix that excludes conflicts from the cache's device map.
278+
fn conflicting_selected_device(
279+
refresh_error: Option<&str>,
280+
selected_devices: &[String],
281+
) -> Option<String> {
282+
let refresh_error = refresh_error?;
283+
selected_devices
284+
.iter()
285+
.find(|device| refresh_error.contains(&format!("conflicting device {device} (specs ")))
286+
.cloned()
287+
}
288+
271289
fn missing_device_error(device: &str, refresh_error: Option<&str>) -> CdiError {
272290
refresh_error.map_or_else(
273291
|| CdiError::MissingDevice(device.to_string()),
@@ -319,7 +337,7 @@ where
319337
}
320338

321339
for gid in &edits.additional_gids {
322-
accumulator.add_gid(*gid);
340+
accumulator.add_gid(*gid)?;
323341
}
324342

325343
accumulator.validate_writable_mounts(normalized_writable_file_allowlist, path_kind)?;
@@ -684,6 +702,37 @@ devices:
684702
);
685703
}
686704

705+
#[test]
706+
fn rejects_duplicate_cdi_device_names() {
707+
let dir = tempfile::tempdir().unwrap();
708+
for (name, value) in [("first.yaml", "FIRST"), ("second.yaml", "SECOND")] {
709+
write_spec(
710+
dir.path(),
711+
name,
712+
&format!(
713+
r#"
714+
cdiVersion: 1.1.0
715+
kind: nvidia.com/gpu
716+
devices:
717+
- name: "0"
718+
containerEdits:
719+
env:
720+
- OPEN_SHELL_TEST={value}
721+
"#
722+
),
723+
);
724+
}
725+
726+
let err = resolve_with_kind(
727+
&context(dir.path(), &["nvidia.com/gpu=0"]),
728+
&[],
729+
always_missing,
730+
)
731+
.unwrap_err();
732+
733+
assert!(matches!(err, CdiError::DuplicateDevice(device) if device == "nvidia.com/gpu=0"));
734+
}
735+
687736
#[test]
688737
fn empty_selection_is_noop() {
689738
let dir = tempfile::tempdir().unwrap();
@@ -794,7 +843,7 @@ devices:
794843
}
795844

796845
#[test]
797-
fn skips_root_additional_gid() {
846+
fn rejects_root_additional_gid() {
798847
let dir = tempfile::tempdir().unwrap();
799848
write_spec(
800849
dir.path(),
@@ -809,14 +858,14 @@ devices:
809858
"#,
810859
);
811860

812-
let requirements = resolve_with_kind(
861+
let err = resolve_with_kind(
813862
&context(dir.path(), &["nvidia.com/gpu=0"]),
814863
&[],
815864
always_missing,
816865
)
817-
.unwrap();
866+
.unwrap_err();
818867

819-
assert_eq!(requirements.additional_gids, vec![44]);
868+
assert!(matches!(err, CdiError::RootAdditionalGid));
820869
}
821870

822871
#[test]

0 commit comments

Comments
 (0)