Skip to main content

container_device_interface/
container_edits.rs

1use std::{collections::HashSet, str::FromStr};
2
3use anyhow::{anyhow, Context, Error, Result};
4use oci_spec::runtime::{self as oci, LinuxDeviceType};
5
6use crate::{
7    container_edits_unix::{device_info_from_path, DeviceType},
8    generate::config::Generator,
9    specs::config::{
10        ContainerEdits as CDIContainerEdits, DeviceNode as CDIDeviceNode, Hook as CDIHook,
11        IntelRdt as CDIIntelRdt, LinuxNetDevice, Mount as CDIMount,
12    },
13    utils::merge,
14};
15
16const NO_PERMISSIONS: &str = "none";
17
18pub trait Validate {
19    fn validate(&self) -> Result<()>;
20}
21
22fn validate_envs(envs: &[String]) -> Result<()> {
23    if let Some(env) = envs
24        .iter()
25        .find(|v| v.split_once('=').is_none_or(|(name, _)| name.is_empty()))
26    {
27        return Err(anyhow!(
28            "invalid environment variable {:?}: missing '=' or empty variable name",
29            env
30        ));
31    }
32    Ok(())
33}
34
35// ContainerEdits represent updates to be applied to an OCI Spec.
36// These updates can be specific to a CDI device, or they can be
37// specific to a CDI Spec. In the former case these edits should
38// be applied to all OCI Specs where the corresponding CDI device
39// is injected. In the latter case, these edits should be applied
40// to all OCI Specs where at least one devices from the CDI Spec
41// is injected.
42#[derive(Clone, Default, Debug, Eq, PartialEq, Hash)]
43pub struct ContainerEdits {
44    pub container_edits: CDIContainerEdits,
45}
46
47impl ContainerEdits {
48    pub fn new() -> Self {
49        Self {
50            container_edits: CDIContainerEdits {
51                ..Default::default()
52            },
53        }
54    }
55
56    // apply edits to the given OCI Spec. Updates the OCI Spec in place.
57    // Returns an error if the update fails.
58    pub fn apply(&mut self, oci_spec: &mut oci::Spec) -> Result<()> {
59        let mut spec_gen: Generator = Generator::spec_gen(Some(oci_spec.clone()));
60
61        if let Some(envs) = &self.container_edits.env {
62            if !envs.is_empty() {
63                spec_gen.add_multiple_process_env(envs);
64            }
65        }
66
67        if let Some(device_nodes) = &self.container_edits.device_nodes {
68            for d in device_nodes {
69                let mut dn: DeviceNode = DeviceNode { node: d.clone() };
70
71                dn.fill_missing_info()
72                    .context("filling missing info failed.")?;
73
74                let d = &dn.node;
75                let mut dev = dn.node.to_oci()?;
76                if dev.uid().is_none() {
77                    if let Some(process) = oci_spec.process() {
78                        let uid = process.user().uid();
79                        if uid > 0 {
80                            dev.set_uid(Some(uid));
81                        }
82                    }
83                }
84                if dev.gid().is_none() {
85                    if let Some(process) = oci_spec.process() {
86                        let gid = process.user().gid();
87                        if gid > 0 {
88                            dev.set_gid(Some(gid));
89                        }
90                    }
91                }
92
93                let dev_typ = dev.typ();
94                let typs = [LinuxDeviceType::B, LinuxDeviceType::C];
95                if typs.contains(&dev_typ) {
96                    let dev_access = match d.permissions.as_deref() {
97                        None | Some("") => Some("rwm".to_string()),
98                        Some(NO_PERMISSIONS) => Some(String::new()),
99                        Some(permissions) => Some(permissions.to_string()),
100                    };
101
102                    let major = dev.major();
103                    let minor = dev.minor();
104                    spec_gen.add_linux_resources_device(
105                        true,
106                        dev_typ,
107                        Some(major),
108                        Some(minor),
109                        dev_access,
110                    );
111                }
112
113                spec_gen.remove_device(&dev.path().display().to_string());
114                spec_gen.add_device(dev.clone());
115            }
116        }
117
118        if let Some(net_devices) = &self.container_edits.net_devices {
119            for net_device in net_devices {
120                spec_gen.add_linux_net_device(
121                    net_device.host_interface_name.clone(),
122                    net_device.to_oci()?,
123                );
124            }
125        }
126
127        if let Some(mounts) = &self.container_edits.mounts {
128            for m in mounts {
129                spec_gen.remove_mount(&m.container_path);
130                spec_gen.add_mount(m.to_oci()?);
131            }
132        }
133
134        if let Some(hooks) = &self.container_edits.hooks {
135            for h in hooks {
136                let hook_name = HookName::from_str(&h.hook_name)
137                    .context(format!("no such hook with name: {:?}", &h.hook_name))?;
138                match hook_name {
139                    HookName::Prestart => spec_gen.add_prestart_hook(h.to_oci()?),
140                    HookName::CreateRuntime => spec_gen.add_createruntime_hook(h.to_oci()?),
141                    HookName::CreateContainer => spec_gen.add_createcontainer_hook(h.to_oci()?),
142                    HookName::StartContainer => spec_gen.add_startcontainer_hook(h.to_oci()?),
143                    HookName::Poststart => spec_gen.add_poststart_hook(h.to_oci()?),
144                    HookName::Poststop => spec_gen.add_poststop_hook(h.to_oci()?),
145                }
146            }
147        }
148
149        if let Some(intel_rdt) = &self.container_edits.intel_rdt {
150            spec_gen.set_linux_intel_rdt(intel_rdt.to_oci()?);
151        }
152
153        if let Some(additional_gids) = &self.container_edits.additional_gids {
154            for gid in additional_gids {
155                if *gid > 0 {
156                    spec_gen.add_process_additional_gid(*gid);
157                }
158            }
159        }
160
161        if let Some(ref spec) = spec_gen.config {
162            oci_spec.set_linux(spec.linux().clone());
163            oci_spec.set_mounts(spec.mounts().clone());
164            oci_spec.set_annotations(spec.annotations().clone());
165            oci_spec.set_hooks(spec.hooks().clone());
166            oci_spec.set_process(spec.process().clone());
167        }
168
169        Ok(())
170    }
171
172    // append other edits into this one.
173    pub fn append(&mut self, o: ContainerEdits) -> Result<()> {
174        let intel_rdt = o
175            .container_edits
176            .intel_rdt
177            .or_else(|| self.container_edits.intel_rdt.take());
178
179        let ce = CDIContainerEdits {
180            env: merge(&mut self.container_edits.env, &o.container_edits.env),
181            device_nodes: merge(
182                &mut self.container_edits.device_nodes,
183                &o.container_edits.device_nodes,
184            ),
185            net_devices: merge(
186                &mut self.container_edits.net_devices,
187                &o.container_edits.net_devices,
188            ),
189            hooks: merge(&mut self.container_edits.hooks, &o.container_edits.hooks),
190            mounts: merge(&mut self.container_edits.mounts, &o.container_edits.mounts),
191            intel_rdt,
192            additional_gids: merge(
193                &mut self.container_edits.additional_gids,
194                &o.container_edits.additional_gids,
195            ),
196        };
197
198        self.container_edits = ce;
199
200        Ok(())
201    }
202
203    pub(crate) fn is_empty(&self) -> bool {
204        option_vec_empty(&self.container_edits.env)
205            && option_vec_empty(&self.container_edits.device_nodes)
206            && option_vec_empty(&self.container_edits.net_devices)
207            && option_vec_empty(&self.container_edits.hooks)
208            && option_vec_empty(&self.container_edits.mounts)
209            && self.container_edits.intel_rdt.is_none()
210            && option_vec_empty(&self.container_edits.additional_gids)
211    }
212}
213
214fn option_vec_empty<T>(value: &Option<Vec<T>>) -> bool {
215    value.as_ref().is_none_or(Vec::is_empty)
216}
217
218// Validate container edits.
219impl Validate for ContainerEdits {
220    fn validate(&self) -> Result<()> {
221        if let Some(envs) = &self.container_edits.env {
222            validate_envs(envs)
223                .context(format!("invalid container edits with envs: {:?}", envs))?;
224        }
225        if let Some(devices) = &self.container_edits.device_nodes {
226            for d in devices {
227                let dn = DeviceNode { node: d.clone() };
228                dn.validate()
229                    .context(format!("invalid container edits with device: {:?}", &d))?;
230            }
231        }
232        if let Some(hooks) = &self.container_edits.hooks {
233            for h in hooks {
234                let hook = Hook { hook: h.clone() };
235                hook.validate()
236                    .context(format!("invalid container edits with hook: {:?}", &h))?;
237            }
238        }
239        if let Some(mounts) = &self.container_edits.mounts {
240            for m in mounts {
241                let mnt = Mount { mount: m.clone() };
242                mnt.validate()
243                    .context(format!("invalid container edits with mount: {:?}", &m))?;
244            }
245        }
246        if let Some(irdt) = &self.container_edits.intel_rdt {
247            let i_rdt = IntelRdt {
248                intel_rdt: irdt.clone(),
249            };
250            i_rdt
251                .validate()
252                .context(format!("invalid container edits with mount: {:?}", irdt))?;
253        }
254        if let Some(net_devices) = &self.container_edits.net_devices {
255            validate_net_devices(net_devices)?;
256        }
257
258        Ok(())
259    }
260}
261
262fn validate_net_devices(devices: &[LinuxNetDevice]) -> Result<()> {
263    let mut host_seen = HashSet::new();
264    let mut name_seen = HashSet::new();
265
266    for dev in devices {
267        if dev.host_interface_name.is_empty() {
268            return Err(anyhow!("invalid linux net device, empty HostInterfaceName"));
269        }
270        if dev.name.is_empty() {
271            return Err(anyhow!("invalid linux net device, empty Name"));
272        }
273        if !host_seen.insert(dev.host_interface_name.clone()) {
274            return Err(anyhow!(
275                "invalid linux net device, duplicate HostInterfaceName {:?}",
276                dev.host_interface_name
277            ));
278        }
279        if !name_seen.insert(dev.name.clone()) {
280            return Err(anyhow!(
281                "invalid linux net device, duplicate Name {:?}",
282                dev.name
283            ));
284        }
285    }
286
287    Ok(())
288}
289
290// DeviceNode is a CDI Spec DeviceNode wrapper, used for validating DeviceNodes.
291pub struct DeviceNode {
292    pub node: CDIDeviceNode,
293}
294
295impl DeviceNode {
296    pub fn fill_missing_info(&mut self) -> Result<()> {
297        let host_path = self
298            .node
299            .host_path
300            .as_deref()
301            .unwrap_or_else(|| &self.node.path);
302
303        if self.node.r#type.as_deref() == Some("") {
304            self.node.r#type = None;
305        }
306
307        if let Some(device_type) = self.node.r#type.as_deref() {
308            if self.node.major.is_some() || device_type == DeviceType::Fifo.to_string() {
309                return Ok(());
310            }
311        }
312
313        let (dev_type, major, minor) = device_info_from_path(host_path)?;
314        match self.node.r#type.as_deref() {
315            None => self.node.r#type = Some(dev_type),
316            Some(node_type) if node_type != dev_type => {
317                return Err(anyhow!(
318                    "CDI device ({}, {}), host type mismatch ({}, {})",
319                    self.node.path,
320                    host_path,
321                    node_type,
322                    dev_type
323                ));
324            }
325            _ => {}
326        }
327
328        if self.node.major.is_none()
329            && self.node.r#type.as_deref() != Some(&DeviceType::Fifo.to_string())
330        {
331            self.node.major = Some(major);
332            self.node.minor = Some(minor);
333        }
334
335        Ok(())
336    }
337}
338
339impl Validate for DeviceNode {
340    fn validate(&self) -> Result<()> {
341        let typs = vec!["b", "c", "u", "p", ""];
342        let valid_typs: HashSet<&str> = typs.into_iter().collect();
343
344        if self.node.path.is_empty() {
345            return Err(anyhow!("invalid (empty) device path"));
346        }
347
348        if let Some(typ) = &self.node.r#type {
349            if !valid_typs.contains(typ.as_str()) {
350                return Err(anyhow!(
351                    "device {:?}: invalid type {:?}",
352                    self.node.path,
353                    typ
354                ));
355            }
356        }
357
358        if let Some(perms) = &self.node.permissions {
359            match perms.as_str() {
360                "" | NO_PERMISSIONS => {}
361                _ if perms.chars().all(|c| matches!(c, 'r' | 'w' | 'm')) => {}
362                _ => {
363                    return Err(anyhow!(
364                        "device {}: invalid permissions {}",
365                        self.node.path,
366                        perms
367                    ));
368                }
369            }
370        }
371
372        Ok(())
373    }
374}
375
376#[derive(Debug, PartialEq, Eq, Hash)]
377enum HookName {
378    Prestart,
379    CreateRuntime,
380    CreateContainer,
381    StartContainer,
382    Poststart,
383    Poststop,
384}
385
386impl FromStr for HookName {
387    type Err = Error;
388    fn from_str(s: &str) -> Result<Self, Self::Err> {
389        match s {
390            "prestart" => Ok(Self::Prestart),
391            "createRuntime" => Ok(Self::CreateRuntime),
392            "createContainer" => Ok(Self::CreateContainer),
393            "startContainer" => Ok(Self::StartContainer),
394            "poststart" => Ok(Self::Poststart),
395            "poststop" => Ok(Self::Poststop),
396            _ => Err(anyhow!("no such hook")),
397        }
398    }
399}
400
401struct Hook {
402    hook: CDIHook,
403}
404
405impl Validate for Hook {
406    fn validate(&self) -> Result<()> {
407        HookName::from_str(&self.hook.hook_name)
408            .context(anyhow!("invalid hook name: {:?}", self.hook.hook_name))?;
409
410        if self.hook.path.is_empty() {
411            return Err(anyhow!(
412                "invalid hook {:?} with empty path",
413                self.hook.hook_name
414            ));
415        }
416        if let Some(envs) = &self.hook.env {
417            validate_envs(envs)
418                .context(anyhow!("hook {:?} with invalid env", &self.hook.hook_name))?;
419        }
420
421        Ok(())
422    }
423}
424
425struct Mount {
426    mount: CDIMount,
427}
428
429impl Validate for Mount {
430    fn validate(&self) -> Result<()> {
431        if self.mount.host_path.is_empty() {
432            return Err(anyhow!("invalid mount, empty host path"));
433        }
434
435        if self.mount.container_path.is_empty() {
436            return Err(anyhow!("invalid mount, empty container path"));
437        }
438
439        Ok(())
440    }
441}
442
443struct IntelRdt {
444    intel_rdt: CDIIntelRdt,
445}
446
447impl Validate for IntelRdt {
448    fn validate(&self) -> Result<()> {
449        if let Some(ref clos_id) = self.intel_rdt.clos_id {
450            if clos_id.len() >= 4096
451                || clos_id == "."
452                || clos_id == ".."
453                || clos_id.contains(&['/', '\n'][..])
454            {
455                return Err(anyhow!("invalid clos id".to_string()));
456            }
457        }
458
459        Ok(())
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466    use crate::specs::config::{
467        ContainerEdits as CDIContainerEdits, DeviceNode as CDIDeviceNode, IntelRdt, LinuxNetDevice,
468    };
469    use oci_spec::runtime::{Process, Spec};
470
471    #[test]
472    fn validates_env_requires_name_and_separator() {
473        validate_envs(&["FOO=bar".to_string(), "EMPTY=".to_string()]).unwrap();
474
475        let missing_separator = validate_envs(&["FOO".to_string()]).unwrap_err();
476        assert!(missing_separator.to_string().contains("missing '='"));
477
478        let empty_name = validate_envs(&["=bar".to_string()]).unwrap_err();
479        assert!(empty_name.to_string().contains("empty variable name"));
480    }
481
482    #[test]
483    fn validates_device_type_and_permissions() {
484        let valid = CDIDeviceNode {
485            path: "/dev/example".to_string(),
486            r#type: Some("c".to_string()),
487            permissions: Some("none".to_string()),
488            ..Default::default()
489        };
490        assert!(DeviceNode { node: valid }.validate().is_ok());
491
492        let invalid_type = CDIDeviceNode {
493            path: "/dev/example".to_string(),
494            r#type: Some("bad".to_string()),
495            ..Default::default()
496        };
497        assert!(DeviceNode { node: invalid_type }.validate().is_err());
498    }
499
500    #[test]
501    #[cfg_attr(
502        miri,
503        ignore = "miri's stat shim does not populate rdev; /dev/null major/minor read as 0"
504    )]
505    fn fill_missing_info_treats_empty_device_type_as_unset() {
506        let mut device = DeviceNode {
507            node: CDIDeviceNode {
508                path: "/dev/null".to_string(),
509                r#type: Some(String::new()),
510                ..Default::default()
511            },
512        };
513
514        device.fill_missing_info().unwrap();
515
516        assert_eq!(Some("c"), device.node.r#type.as_deref());
517        assert_eq!(Some(1), device.node.major);
518        assert_eq!(Some(3), device.node.minor);
519    }
520
521    #[test]
522    fn validates_net_device_duplicates() {
523        let edits = ContainerEdits {
524            container_edits: CDIContainerEdits {
525                net_devices: Some(vec![
526                    LinuxNetDevice {
527                        host_interface_name: "eth0".to_string(),
528                        name: "container_eth0".to_string(),
529                    },
530                    LinuxNetDevice {
531                        host_interface_name: "eth0".to_string(),
532                        name: "container_eth1".to_string(),
533                    },
534                ]),
535                ..Default::default()
536            },
537        };
538
539        assert!(edits.validate().is_err());
540    }
541
542    #[test]
543    fn apply_sets_net_devices_and_full_intel_rdt() {
544        let mut spec = Spec::default();
545        let mut edits = ContainerEdits {
546            container_edits: CDIContainerEdits {
547                net_devices: Some(vec![LinuxNetDevice {
548                    host_interface_name: "eth0".to_string(),
549                    name: "container_eth0".to_string(),
550                }]),
551                intel_rdt: Some(IntelRdt {
552                    clos_id: Some("class-a".to_string()),
553                    schemata: Some(vec!["L3:0=ffff".to_string()]),
554                    enable_monitoring: Some(true),
555                    ..Default::default()
556                }),
557                ..Default::default()
558            },
559        };
560
561        edits.apply(&mut spec).unwrap();
562
563        let linux = spec.linux().as_ref().unwrap();
564        assert_eq!(
565            linux
566                .net_devices()
567                .as_ref()
568                .unwrap()
569                .get("eth0")
570                .unwrap()
571                .name()
572                .as_ref()
573                .unwrap(),
574            "container_eth0"
575        );
576        let rdt = linux.intel_rdt().as_ref().unwrap();
577        assert_eq!(Some(&"class-a".to_string()), rdt.clos_id().as_ref());
578        assert_eq!(
579            Some(&vec!["L3:0=ffff".to_string()]),
580            rdt.schemata().as_ref()
581        );
582        assert_eq!(Some(true), *rdt.enable_monitoring());
583    }
584
585    #[test]
586    fn apply_uses_process_uid_and_gid_only_when_device_owner_unset() {
587        let mut spec = Spec::default();
588        spec.set_process(Some(Process::default()));
589        spec.process_mut()
590            .as_mut()
591            .unwrap()
592            .user_mut()
593            .set_uid(1234);
594        spec.process_mut()
595            .as_mut()
596            .unwrap()
597            .user_mut()
598            .set_gid(5678);
599
600        let mut edits = ContainerEdits {
601            container_edits: CDIContainerEdits {
602                device_nodes: Some(vec![CDIDeviceNode {
603                    path: "/dev/null".to_string(),
604                    r#type: Some("c".to_string()),
605                    major: Some(1),
606                    minor: Some(3),
607                    ..Default::default()
608                }]),
609                ..Default::default()
610            },
611        };
612
613        edits.apply(&mut spec).unwrap();
614        let dev = &spec.linux().as_ref().unwrap().devices().as_ref().unwrap()[0];
615        assert_eq!(Some(1234), dev.uid());
616        assert_eq!(Some(5678), dev.gid());
617    }
618
619    fn named_device_node(path: &str, perms: Option<&str>) -> CDIDeviceNode {
620        CDIDeviceNode {
621            path: path.to_string(),
622            r#type: Some("c".to_string()),
623            major: Some(1),
624            minor: Some(3),
625            permissions: perms.map(str::to_string),
626            ..Default::default()
627        }
628    }
629
630    #[test]
631    fn apply_inherits_process_uid_gid_and_maps_permissions() {
632        let mut edits = ContainerEdits::new();
633        edits.container_edits.env = Some(vec![]); // empty env list: no-op branch
634        edits.container_edits.device_nodes = Some(vec![
635            named_device_node("/dev/a", None),         // -> rwm
636            named_device_node("/dev/b", Some("none")), // -> ""
637            named_device_node("/dev/c", Some("rw")),   // -> rw
638        ]);
639
640        let mut spec = oci::Spec::default();
641        if let Some(process) = spec.process_mut() {
642            process.user_mut().set_uid(1000);
643            process.user_mut().set_gid(2000);
644        }
645        edits.apply(&mut spec).unwrap();
646
647        let linux = spec.linux().as_ref().unwrap();
648        let dev = &linux.devices().as_ref().unwrap()[0];
649        assert_eq!((Some(1000), Some(2000)), (dev.uid(), dev.gid()));
650
651        let access: Vec<_> = linux
652            .resources()
653            .as_ref()
654            .unwrap()
655            .devices()
656            .as_ref()
657            .unwrap()
658            .iter()
659            .map(|d| d.access().clone().unwrap_or_default())
660            .collect();
661        assert_eq!(access, vec!["rwm", "", "rw"]);
662    }
663
664    #[test]
665    fn apply_places_every_hook_kind() {
666        let kinds = [
667            "prestart",
668            "createRuntime",
669            "createContainer",
670            "startContainer",
671            "poststart",
672            "poststop",
673        ];
674        let mut edits = ContainerEdits::new();
675        edits.container_edits.hooks = Some(
676            kinds
677                .iter()
678                .map(|k| CDIHook {
679                    hook_name: k.to_string(),
680                    path: "/bin/true".to_string(),
681                    ..Default::default()
682                })
683                .collect(),
684        );
685
686        let mut spec = oci::Spec::default();
687        edits.apply(&mut spec).unwrap();
688
689        let hooks = spec.hooks().as_ref().unwrap();
690        assert_eq!(hooks.prestart().as_ref().unwrap().len(), 1);
691        assert_eq!(hooks.create_runtime().as_ref().unwrap().len(), 1);
692        assert_eq!(hooks.create_container().as_ref().unwrap().len(), 1);
693        assert_eq!(hooks.start_container().as_ref().unwrap().len(), 1);
694        assert_eq!(hooks.poststart().as_ref().unwrap().len(), 1);
695        assert_eq!(hooks.poststop().as_ref().unwrap().len(), 1);
696    }
697
698    #[test]
699    fn apply_rejects_unknown_hook_names() {
700        let mut edits = ContainerEdits::new();
701        edits.container_edits.hooks = Some(vec![CDIHook {
702            hook_name: "bogus".to_string(),
703            path: "/bin/true".to_string(),
704            ..Default::default()
705        }]);
706        let err = edits.apply(&mut oci::Spec::default()).unwrap_err();
707        assert!(err.to_string().contains("no such hook"));
708    }
709
710    #[test]
711    fn append_merges_intel_rdt_from_other() {
712        let mut base = ContainerEdits::new();
713        base.container_edits.env = Some(vec!["A=1".to_string()]);
714        let mut other = ContainerEdits::new();
715        other.container_edits.intel_rdt = Some(CDIIntelRdt {
716            clos_id: Some("class".to_string()),
717            ..Default::default()
718        });
719        other.container_edits.env = Some(vec!["B=2".to_string()]);
720
721        base.append(other).unwrap();
722
723        assert_eq!(
724            base.container_edits.intel_rdt.as_ref().unwrap().clos_id,
725            Some("class".to_string())
726        );
727        assert_eq!(
728            base.container_edits.env,
729            Some(vec!["A=1".to_string(), "B=2".to_string()])
730        );
731    }
732
733    #[test]
734    fn validate_rejects_each_invalid_component() {
735        let case = |mutate: &dyn Fn(&mut CDIContainerEdits), needle: &str| {
736            let mut edits = ContainerEdits::new();
737            mutate(&mut edits.container_edits);
738            let err = edits.validate().unwrap_err();
739            assert!(
740                format!("{err:?}").contains(needle),
741                "expected {needle:?} in {err:?}"
742            );
743        };
744
745        case(
746            &|e| e.env = Some(vec!["NOEQUALS".into()]),
747            "invalid environment variable",
748        );
749        case(
750            &|e| {
751                e.device_nodes = Some(vec![CDIDeviceNode {
752                    path: String::new(),
753                    ..Default::default()
754                }])
755            },
756            "invalid (empty) device path",
757        );
758        case(
759            &|e| {
760                e.device_nodes = Some(vec![CDIDeviceNode {
761                    path: "/dev/x".into(),
762                    permissions: Some("rwx".into()),
763                    ..Default::default()
764                }])
765            },
766            "invalid permissions",
767        );
768        case(
769            &|e| {
770                e.hooks = Some(vec![CDIHook {
771                    hook_name: "bogus".into(),
772                    path: "/bin/true".into(),
773                    ..Default::default()
774                }])
775            },
776            "invalid hook name",
777        );
778        case(
779            &|e| {
780                e.hooks = Some(vec![CDIHook {
781                    hook_name: "prestart".into(),
782                    path: String::new(),
783                    ..Default::default()
784                }])
785            },
786            "empty path",
787        );
788        case(
789            &|e| {
790                e.hooks = Some(vec![CDIHook {
791                    hook_name: "prestart".into(),
792                    path: "/bin/true".into(),
793                    env: Some(vec!["BAD".into()]),
794                    ..Default::default()
795                }])
796            },
797            "invalid env",
798        );
799        case(
800            &|e| {
801                e.mounts = Some(vec![CDIMount {
802                    host_path: String::new(),
803                    container_path: "/c".into(),
804                    ..Default::default()
805                }])
806            },
807            "empty host path",
808        );
809        case(
810            &|e| {
811                e.mounts = Some(vec![CDIMount {
812                    host_path: "/h".into(),
813                    container_path: String::new(),
814                    ..Default::default()
815                }])
816            },
817            "empty container path",
818        );
819        case(
820            &|e| {
821                e.intel_rdt = Some(CDIIntelRdt {
822                    clos_id: Some("a/b".into()),
823                    ..Default::default()
824                })
825            },
826            "invalid clos id",
827        );
828    }
829
830    #[test]
831    fn validate_rejects_bad_net_devices() {
832        let netdev = |host: &str, name: &str| crate::specs::config::LinuxNetDevice {
833            host_interface_name: host.to_string(),
834            name: name.to_string(),
835        };
836        let case = |devices: Vec<crate::specs::config::LinuxNetDevice>, needle: &str| {
837            let mut edits = ContainerEdits::new();
838            edits.container_edits.net_devices = Some(devices);
839            let err = edits.validate().unwrap_err();
840            assert!(format!("{err:?}").contains(needle), "missing {needle:?}");
841        };
842
843        case(vec![netdev("", "eth0")], "empty HostInterfaceName");
844        case(vec![netdev("eth0", "")], "empty Name");
845        case(
846            vec![netdev("eth0", "a"), netdev("eth0", "b")],
847            "duplicate HostInterfaceName",
848        );
849        case(
850            vec![netdev("eth0", "a"), netdev("eth1", "a")],
851            "duplicate Name",
852        );
853    }
854
855    #[test]
856    #[cfg_attr(miri, ignore = "miri's stat shim does not populate rdev for /dev/null")]
857    fn fill_missing_info_rejects_host_type_mismatch() {
858        let mut dn = DeviceNode {
859            node: CDIDeviceNode {
860                path: "/dev/whatever".to_string(),
861                host_path: Some("/dev/null".to_string()),
862                r#type: Some("b".to_string()),
863                ..Default::default()
864            },
865        };
866        let err = dn.fill_missing_info().unwrap_err();
867        assert!(err.to_string().contains("host type mismatch"));
868    }
869
870    #[test]
871    fn fill_missing_info_returns_early_when_complete() {
872        let mut dn = DeviceNode {
873            node: named_device_node("/proc/does-not-matter", None),
874        };
875        // type + major present: no filesystem access happens at all
876        dn.fill_missing_info().unwrap();
877        assert_eq!(dn.node.major, Some(1));
878    }
879}