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#[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 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 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
218impl 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
290pub 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![]); edits.container_edits.device_nodes = Some(vec![
635 named_device_node("/dev/a", None), named_device_node("/dev/b", Some("none")), named_device_node("/dev/c", Some("rw")), ]);
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 dn.fill_missing_info().unwrap();
877 assert_eq!(dn.node.major, Some(1));
878 }
879}