@@ -70,7 +70,7 @@ impl CdiSpecDirectory {
7070pub struct CdiDerivedRequirements {
7171 pub read_only_paths : Vec < String > ,
7272 pub read_write_paths : Vec < String > ,
73- pub supplemental_gids : Vec < u32 > ,
73+ pub additional_gids : Vec < u32 > ,
7474}
7575
7676#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
@@ -104,22 +104,12 @@ pub enum CdiError {
104104 } ,
105105 #[ error( "unsupported CDI context version {0}" ) ]
106106 UnsupportedContextVersion ( u32 ) ,
107- #[ error( "CDI context selected_devices must not be empty" ) ]
108- EmptySelection ,
109- #[ error( "CDI context spec_dirs must not be empty" ) ]
110- EmptySpecDirs ,
111- #[ error( "invalid CDI selected device ID '{0}'; expected '<kind>=<name>'" ) ]
112- InvalidSelectedDevice ( String ) ,
113107 #[ error( "CDI spec dir '{path}' from source '{diagnostic_source}' is unsafe: {reason}" ) ]
114108 UnsafeSpecDir {
115109 path : String ,
116110 diagnostic_source : String ,
117111 reason : & ' static str ,
118112 } ,
119- #[ error( "no CDI spec files were found in the mounted spec dirs" ) ]
120- NoSpecFiles ,
121- #[ error( "failed to refresh CDI cache from mounted spec dirs: {error}" ) ]
122- CacheRefresh { error : String } ,
123113 #[ error( "selected CDI device '{0}' was not found in mounted CDI specs" ) ]
124114 MissingDevice ( String ) ,
125115 #[ error( "failed to merge CDI edits for '{device}': {error}" ) ]
@@ -140,8 +130,6 @@ pub enum CdiError {
140130 WritableMountNotFile { path : String , kind : String } ,
141131 #[ error( "CDI mount '{path}' has conflicting ro/rw options" ) ]
142132 ConflictingMountOptions { path : String } ,
143- #[ error( "CDI additionalGids must not include root GID 0" ) ]
144- RootAdditionalGid ,
145133}
146134
147135// Temporary view over upstream-resolved CDI edits. The Rust CDI crate currently
@@ -180,10 +168,22 @@ enum CdiAccess {
180168#[ derive( Debug , Default ) ]
181169struct RequirementAccumulator {
182170 paths : BTreeMap < String , CdiAccess > ,
183- gids : BTreeSet < u32 > ,
171+ writable_mount_paths : BTreeSet < String > ,
172+ additional_gids : BTreeSet < u32 > ,
184173}
185174
186175impl RequirementAccumulator {
176+ fn add_device_node ( & mut self , path : String ) -> Result < ( ) , CdiError > {
177+ self . add_path ( path, CdiAccess :: ReadWrite )
178+ }
179+
180+ fn add_mount ( & mut self , path : String , access : CdiAccess ) -> Result < ( ) , CdiError > {
181+ if access == CdiAccess :: ReadWrite {
182+ self . writable_mount_paths . insert ( path. clone ( ) ) ;
183+ }
184+ self . add_path ( path, access)
185+ }
186+
187187 fn add_path ( & mut self , path : String , access : CdiAccess ) -> Result < ( ) , CdiError > {
188188 match self . paths . get ( & path) . copied ( ) {
189189 Some ( existing) if existing != access => Err ( CdiError :: ConflictingAccess { path } ) ,
@@ -195,11 +195,34 @@ impl RequirementAccumulator {
195195 }
196196 }
197197
198- fn add_gid ( & mut self , gid : u32 ) -> Result < ( ) , CdiError > {
198+ fn add_gid ( & mut self , gid : u32 ) {
199199 if gid == 0 {
200- return Err ( CdiError :: RootAdditionalGid ) ;
200+ tracing:: debug!( "Skipping CDI additionalGids entry for root GID 0" ) ;
201+ return ;
202+ }
203+ self . additional_gids . insert ( gid) ;
204+ }
205+
206+ fn validate_writable_mounts < F > (
207+ & self ,
208+ normalized_writable_file_allowlist : & HashSet < String > ,
209+ path_kind : & F ,
210+ ) -> Result < ( ) , CdiError >
211+ where
212+ F : Fn ( & str ) -> Option < CdiPathKind > ,
213+ {
214+ for path in & self . writable_mount_paths {
215+ if !normalized_writable_file_allowlist. contains ( path) {
216+ return Err ( CdiError :: WritableMountNotAllowed { path : path. clone ( ) } ) ;
217+ }
218+ let kind = path_kind ( path) ;
219+ if kind != Some ( CdiPathKind :: File ) {
220+ return Err ( CdiError :: WritableMountNotFile {
221+ path : path. clone ( ) ,
222+ kind : kind. map_or_else ( || "missing" . to_string ( ) , |kind| kind. to_string ( ) ) ,
223+ } ) ;
224+ }
201225 }
202- self . gids . insert ( gid) ;
203226 Ok ( ( ) )
204227 }
205228
@@ -211,7 +234,7 @@ impl RequirementAccumulator {
211234 CdiAccess :: ReadWrite => requirements. read_write_paths . push ( path) ,
212235 }
213236 }
214- requirements. supplemental_gids = self . gids . into_iter ( ) . collect ( ) ;
237+ requirements. additional_gids = self . additional_gids . into_iter ( ) . collect ( ) ;
215238 requirements
216239 }
217240}
@@ -245,28 +268,26 @@ where
245268 S : BuildHasher ,
246269{
247270 validate_context ( context) ?;
248- let selected_devices = parse_selected_devices ( & context. selected_devices ) ?;
271+ let selected_devices = selected_cdi_devices ( & context. selected_devices ) ;
272+ if selected_devices. is_empty ( ) || context. spec_dirs . is_empty ( ) {
273+ return Ok ( CdiDerivedRequirements :: default ( ) ) ;
274+ }
275+
249276 let normalized_allowlist = writable_file_allowlist
250277 . iter ( )
251278 . map ( |path| normalize_path ( path) )
252279 . collect :: < HashSet < _ > > ( ) ;
253280
254281 let edits = resolve_container_edits ( context, & selected_devices) ?;
255282 let mut accumulator = RequirementAccumulator :: default ( ) ;
256- apply_edits ( & edits, & normalized_allowlist, & path_kind, & mut accumulator) ?;
283+ accumulate_requirements ( & edits, & normalized_allowlist, & path_kind, & mut accumulator) ?;
257284 Ok ( accumulator. build ( ) )
258285}
259286
260287fn validate_context ( context : & CdiContext ) -> Result < ( ) , CdiError > {
261288 if context. version != CDI_CONTEXT_VERSION {
262289 return Err ( CdiError :: UnsupportedContextVersion ( context. version ) ) ;
263290 }
264- if context. selected_devices . is_empty ( ) {
265- return Err ( CdiError :: EmptySelection ) ;
266- }
267- if context. spec_dirs . is_empty ( ) {
268- return Err ( CdiError :: EmptySpecDirs ) ;
269- }
270291 for spec_dir in & context. spec_dirs {
271292 validate_absolute_no_parent ( & spec_dir. path ) . map_err ( |reason| CdiError :: UnsafeSpecDir {
272293 path : spec_dir. path . clone ( ) ,
@@ -277,29 +298,26 @@ fn validate_context(context: &CdiContext) -> Result<(), CdiError> {
277298 Ok ( ( ) )
278299}
279300
280- fn parse_selected_devices ( device_ids : & [ String ] ) -> Result < Vec < String > , CdiError > {
301+ fn selected_cdi_devices ( device_ids : & [ String ] ) -> Vec < String > {
281302 let mut seen = HashSet :: new ( ) ;
282303 let mut parsed = Vec :: new ( ) ;
283304 for raw in device_ids {
284305 let raw = raw. trim ( ) ;
285- let Some ( ( kind, name) ) = raw. rsplit_once ( '=' ) else {
286- return Err ( CdiError :: InvalidSelectedDevice ( raw. to_string ( ) ) ) ;
287- } ;
288- if kind. trim ( ) . is_empty ( ) || name. trim ( ) . is_empty ( ) {
289- return Err ( CdiError :: InvalidSelectedDevice ( raw. to_string ( ) ) ) ;
306+ if raw. is_empty ( ) {
307+ continue ;
290308 }
291309 if seen. insert ( raw. to_string ( ) ) {
292310 parsed. push ( raw. to_string ( ) ) ;
293311 }
294312 }
295- Ok ( parsed)
313+ parsed
296314}
297315
298316fn resolve_container_edits (
299317 context : & CdiContext ,
300318 selected_devices : & [ String ] ,
301319) -> Result < CdiContainerEdits , CdiError > {
302- let mut cache = build_cache ( & context. spec_dirs ) ? ;
320+ let mut cache = build_cache ( & context. spec_dirs ) ;
303321 let mut merged = UpstreamContainerEdits :: new ( ) ;
304322 let mut applied_specs = BTreeSet :: new ( ) ;
305323
@@ -333,7 +351,7 @@ fn resolve_container_edits(
333351 serde_json:: from_value ( value) . map_err ( |source| CdiError :: EditDecode { source } )
334352}
335353
336- fn build_cache ( spec_dirs : & [ CdiSpecDirectory ] ) -> Result < Cache , CdiError > {
354+ fn build_cache ( spec_dirs : & [ CdiSpecDirectory ] ) -> Cache {
337355 let spec_dir_paths = spec_dirs
338356 . iter ( )
339357 . map ( |spec_dir| spec_dir. path . as_str ( ) )
@@ -343,16 +361,16 @@ fn build_cache(spec_dirs: &[CdiSpecDirectory]) -> Result<Cache, CdiError> {
343361 with_spec_dirs( & spec_dir_paths) ,
344362 with_auto_refresh( false ) ,
345363 ] ) ;
346- cache. refresh ( ) . map_err ( |err| CdiError :: CacheRefresh {
347- error : err . to_string ( ) ,
348- } ) ? ;
349- if cache. list_devices ( ) . is_empty ( ) {
350- return Err ( CdiError :: NoSpecFiles ) ;
364+ if let Err ( err ) = cache. refresh ( ) {
365+ tracing :: debug! (
366+ error = %err ,
367+ "Ignoring CDI cache refresh error; requested device lookup will determine availability"
368+ ) ;
351369 }
352- Ok ( cache)
370+ cache
353371}
354372
355- fn apply_edits < F > (
373+ fn accumulate_requirements < F > (
356374 edits : & CdiContainerEdits ,
357375 normalized_writable_file_allowlist : & HashSet < String > ,
358376 path_kind : & F ,
@@ -363,33 +381,20 @@ where
363381{
364382 for device_node in & edits. device_nodes {
365383 let path = normalize_policy_path ( & device_node. path ) ?;
366- accumulator. add_path ( path, CdiAccess :: ReadWrite ) ?;
384+ accumulator. add_device_node ( path) ?;
367385 }
368386
369387 for mount in & edits. mounts {
370388 let path = normalize_policy_path ( & mount. container_path ) ?;
371- match mount_access ( & path, & mount. options ) ? {
372- CdiAccess :: ReadOnly => accumulator. add_path ( path, CdiAccess :: ReadOnly ) ?,
373- CdiAccess :: ReadWrite => {
374- if !normalized_writable_file_allowlist. contains ( & path) {
375- return Err ( CdiError :: WritableMountNotAllowed { path } ) ;
376- }
377- let kind = path_kind ( & path) ;
378- if kind != Some ( CdiPathKind :: File ) {
379- return Err ( CdiError :: WritableMountNotFile {
380- path,
381- kind : kind. map_or_else ( || "missing" . to_string ( ) , |kind| kind. to_string ( ) ) ,
382- } ) ;
383- }
384- accumulator. add_path ( path, CdiAccess :: ReadWrite ) ?;
385- }
386- }
389+ let access = mount_access ( & path, & mount. options ) ?;
390+ accumulator. add_mount ( path, access) ?;
387391 }
388392
389393 for gid in & edits. additional_gids {
390- accumulator. add_gid ( * gid) ? ;
394+ accumulator. add_gid ( * gid) ;
391395 }
392396
397+ accumulator. validate_writable_mounts ( normalized_writable_file_allowlist, path_kind) ?;
393398 Ok ( ( ) )
394399}
395400
@@ -400,6 +405,9 @@ fn mount_access(path: &str, options: &[String]) -> Result<CdiAccess, CdiError> {
400405 let read_write_requested = options
401406 . iter ( )
402407 . any ( |option| option. eq_ignore_ascii_case ( "rw" ) ) ;
408+ // CDI mount options are stringly typed and runtime-specific normalization
409+ // can differ. Treat simultaneous ro/rw as malformed instead of guessing
410+ // which option a later mount syscall would effectively apply.
403411 if read_only_requested && read_write_requested {
404412 return Err ( CdiError :: ConflictingMountOptions {
405413 path : path. to_string ( ) ,
@@ -624,7 +632,7 @@ devices:
624632 )
625633 . unwrap ( ) ;
626634
627- assert_eq ! ( requirements. supplemental_gids , vec![ 44 ] ) ;
635+ assert_eq ! ( requirements. additional_gids , vec![ 44 ] ) ;
628636 assert_eq ! (
629637 requirements. read_write_paths,
630638 vec![ "/dev/nvhost-gpu" , "/dev/nvmap" ]
@@ -717,7 +725,7 @@ devices:
717725 }
718726
719727 #[ test]
720- fn rejects_malformed_spec ( ) {
728+ fn defers_malformed_spec_errors_to_device_resolution ( ) {
721729 let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
722730 write_spec ( dir. path ( ) , "broken.yaml" , "kind: [" ) ;
723731
@@ -728,7 +736,49 @@ devices:
728736 )
729737 . unwrap_err ( ) ;
730738
731- assert ! ( matches!( err, CdiError :: CacheRefresh { .. } ) ) ;
739+ assert ! ( matches!( err, CdiError :: MissingDevice ( device) if device == "nvidia.com/gpu=0" ) ) ;
740+ }
741+
742+ #[ test]
743+ fn empty_selection_is_noop ( ) {
744+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
745+ let requirements = resolve_with_kind ( & context ( dir. path ( ) , & [ ] ) , & [ ] , always_missing)
746+ . expect ( "empty selection should resolve to empty requirements" ) ;
747+
748+ assert_eq ! ( requirements, CdiDerivedRequirements :: default ( ) ) ;
749+ }
750+
751+ #[ test]
752+ fn empty_spec_dirs_are_noop ( ) {
753+ let context = CdiContext :: new ( "test" , vec ! [ "nvidia.com/gpu=0" . to_string( ) ] , Vec :: new ( ) ) ;
754+
755+ let requirements = resolve_with_kind ( & context, & [ ] , always_missing)
756+ . expect ( "empty spec dirs should resolve to empty requirements" ) ;
757+
758+ assert_eq ! ( requirements, CdiDerivedRequirements :: default ( ) ) ;
759+ }
760+
761+ #[ test]
762+ fn defers_device_id_shape_to_upstream_resolution ( ) {
763+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
764+ write_spec (
765+ dir. path ( ) ,
766+ "nvidia.yaml" ,
767+ r#"
768+ cdiVersion: 1.1.0
769+ kind: nvidia.com/gpu
770+ devices:
771+ - name: "0"
772+ containerEdits:
773+ env:
774+ - OPEN_SHELL_TEST=1
775+ "# ,
776+ ) ;
777+
778+ let err = resolve_with_kind ( & context ( dir. path ( ) , & [ "not-a-cdi-id" ] ) , & [ ] , always_missing)
779+ . unwrap_err ( ) ;
780+
781+ assert ! ( matches!( err, CdiError :: MissingDevice ( device) if device == "not-a-cdi-id" ) ) ;
732782 }
733783
734784 #[ test]
@@ -799,7 +849,7 @@ devices:
799849 }
800850
801851 #[ test]
802- fn rejects_root_additional_gid ( ) {
852+ fn skips_root_additional_gid ( ) {
803853 let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
804854 write_spec (
805855 dir. path ( ) ,
@@ -810,17 +860,17 @@ kind: nvidia.com/gpu
810860devices:
811861 - name: "0"
812862 containerEdits:
813- additionalGids: [0]
863+ additionalGids: [0, 44 ]
814864"# ,
815865 ) ;
816866
817- let err = resolve_with_kind (
867+ let requirements = resolve_with_kind (
818868 & context ( dir. path ( ) , & [ "nvidia.com/gpu=0" ] ) ,
819869 & [ ] ,
820870 always_missing,
821871 )
822- . unwrap_err ( ) ;
872+ . unwrap ( ) ;
823873
824- assert ! ( matches! ( err , CdiError :: RootAdditionalGid ) ) ;
874+ assert_eq ! ( requirements . additional_gids , vec! [ 44 ] ) ;
825875 }
826876}
0 commit comments