crosvm/crosvm/sys/linux/
config.rs

1// Copyright 2022 The ChromiumOS Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use std::path::PathBuf;
6use std::str::FromStr;
7
8use anyhow::anyhow;
9use anyhow::bail;
10use anyhow::Context;
11use devices::IommuDevType;
12use devices::PciAddress;
13use devices::SerialParameters;
14use libc::getegid;
15use libc::geteuid;
16use serde::Deserialize;
17use serde::Serialize;
18use serde_keyvalue::from_key_values;
19use serde_keyvalue::FromKeyValues;
20
21use crate::crosvm::config::Config;
22
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromKeyValues)]
24#[serde(deny_unknown_fields, rename_all = "kebab-case")]
25pub enum HypervisorKind {
26    Kvm {
27        device: Option<PathBuf>,
28    },
29    #[cfg(target_arch = "aarch64")]
30    #[cfg(feature = "halla")]
31    Halla {
32        device: Option<PathBuf>,
33    },
34    #[cfg(all(target_arch = "aarch64", feature = "geniezone"))]
35    Geniezone {
36        device: Option<PathBuf>,
37    },
38    #[cfg(all(target_arch = "aarch64", feature = "gunyah"))]
39    Gunyah {
40        device: Option<PathBuf>,
41        qcom_trusted_vm_id: Option<u16>,
42        qcom_trusted_vm_pas_id: Option<u32>,
43    },
44}
45
46// Doesn't do anything on unix.
47pub fn check_serial_params(_serial_params: &SerialParameters) -> Result<(), String> {
48    Ok(())
49}
50
51pub fn validate_config(_cfg: &mut Config) -> std::result::Result<(), String> {
52    Ok(())
53}
54
55/// VFIO device structure for creating a new instance based on command line options.
56#[derive(Serialize, Deserialize, FromKeyValues)]
57#[serde(deny_unknown_fields, rename_all = "kebab-case")]
58pub struct VfioOption {
59    /// Path to the VFIO device.
60    pub path: PathBuf,
61
62    /// IOMMU type to use for this VFIO device.
63    #[serde(default)]
64    pub iommu: IommuDevType,
65
66    /// PCI address to use for the VFIO device in the guest.
67    /// If not specified, defaults to mirroring the host PCI address.
68    pub guest_address: Option<PciAddress>,
69
70    /// The symbol that labels the overlay device tree node which corresponds to this
71    /// VFIO device.
72    pub dt_symbol: Option<String>,
73}
74
75#[derive(Default, Eq, PartialEq, Serialize, Deserialize)]
76pub enum SharedDirKind {
77    FS,
78    #[default]
79    P9,
80}
81
82impl FromStr for SharedDirKind {
83    type Err = anyhow::Error;
84
85    fn from_str(s: &str) -> Result<Self, Self::Err> {
86        use SharedDirKind::*;
87        match s {
88            "fs" | "FS" => Ok(FS),
89            "9p" | "9P" | "p9" | "P9" => Ok(P9),
90            _ => {
91                bail!("invalid file system type");
92            }
93        }
94    }
95}
96
97pub struct SharedDir {
98    pub src: PathBuf,
99    pub tag: String,
100    pub kind: SharedDirKind,
101    pub ugid: (Option<u32>, Option<u32>),
102    pub uid_map: String,
103    pub gid_map: String,
104    pub fs_cfg: devices::virtio::fs::Config,
105    pub p9_cfg: p9::Config,
106}
107
108impl Default for SharedDir {
109    fn default() -> SharedDir {
110        SharedDir {
111            src: Default::default(),
112            tag: Default::default(),
113            kind: Default::default(),
114            ugid: (None, None),
115            // SAFETY: trivially safe
116            uid_map: format!("0 {} 1", unsafe { geteuid() }),
117            // SAFETY: trivially safe
118            gid_map: format!("0 {} 1", unsafe { getegid() }),
119            fs_cfg: Default::default(),
120            p9_cfg: Default::default(),
121        }
122    }
123}
124
125struct UgidConfig {
126    uid: Option<u32>,
127    gid: Option<u32>,
128    uid_map: String,
129    gid_map: String,
130}
131
132impl Default for UgidConfig {
133    fn default() -> Self {
134        Self {
135            uid: None,
136            gid: None,
137            // SAFETY: geteuid never fails.
138            uid_map: format!("0 {} 1", unsafe { geteuid() }),
139            // SAFETY: getegid never fails.
140            gid_map: format!("0 {} 1", unsafe { getegid() }),
141        }
142    }
143}
144
145impl UgidConfig {
146    /// Parse a key-value pair of ugid config to update `UgidConfig`.
147    /// Returns whether `self` was updated or not.
148    fn parse_ugid_config(&mut self, kind: &str, value: &str) -> anyhow::Result<bool> {
149        match kind {
150            "uid" => {
151                self.uid = Some(value.parse().context("`uid` must be an integer")?);
152            }
153            "gid" => {
154                self.gid = Some(value.parse().context("`gid` must be an integer")?);
155            }
156            "uidmap" => self.uid_map = value.into(),
157            "gidmap" => self.gid_map = value.into(),
158            _ => {
159                return Ok(false);
160            }
161        }
162        Ok(true)
163    }
164}
165
166impl FromStr for SharedDir {
167    type Err = anyhow::Error;
168
169    fn from_str(param: &str) -> Result<Self, Self::Err> {
170        // This is formatted as multiple fields, each separated by ":". The first 2 fields are
171        // fixed (src:tag).  The rest may appear in any order:
172        //
173        // * type=TYPE - must be one of "p9" or "fs" (default: p9)
174        // * uidmap=UIDMAP - a uid map in the format "inner outer count[,inner outer count]"
175        //   (default: "0 <current euid> 1")
176        // * gidmap=GIDMAP - a gid map in the same format as uidmap (default: "0 <current egid> 1")
177        // * privileged_quota_uids=UIDS - Space-separated list of privileged uid values. When
178        //   performing quota-related operations, these UIDs are treated as if they have CAP_FOWNER.
179        // * timeout=TIMEOUT - a timeout value in seconds, which indicates how long attributes and
180        //   directory contents should be considered valid (default: 5)
181        // * cache=CACHE - one of "never", "always", or "auto" (default: auto)
182        // * writeback=BOOL - indicates whether writeback caching should be enabled (default: false)
183        // * uid=UID - uid of the device process in the user namespace created by minijail.
184        //   (default: 0)
185        // * gid=GID - gid of the device process in the user namespace created by minijail.
186        //   (default: 0)
187        // * max_dynamic_perm=uint - number of maximum number of dynamic permissions paths (default:
188        //   0) This feature is arc_quota specific feature.
189        // * max_dynamic_xattr=uint - number of maximum number of dynamic xattr paths (default: 0).
190        //   This feature is arc_quota specific feature.
191        // * security_ctx=BOOL - indicates whether use FUSE_SECURITY_CONTEXT feature or not.
192        //
193        // These two options (uid/gid) are useful when the crosvm process has no
194        // CAP_SETGID/CAP_SETUID but an identity mapping of the current user/group
195        // between the VM and the host is required.
196        // Say the current user and the crosvm process has uid 5000, a user can use
197        // "uid=5000" and "uidmap=5000 5000 1" such that files owned by user 5000
198        // still appear to be owned by user 5000 in the VM. These 2 options are
199        // useful only when there is 1 user in the VM accessing shared files.
200        // If multiple users want to access the shared file, gid/uid options are
201        // useless. It'd be better to create a new user namespace and give
202        // CAP_SETUID/CAP_SETGID to the crosvm.
203        let mut components = param.split(':');
204        let src = PathBuf::from(
205            components
206                .next()
207                .context("missing source path for `shared-dir`")?,
208        );
209        let tag = components
210            .next()
211            .context("missing tag for `shared-dir`")?
212            .to_owned();
213
214        if !src.is_dir() {
215            bail!("source path for `shared-dir` must be a directory");
216        }
217
218        let mut shared_dir = SharedDir {
219            src,
220            tag,
221            ..Default::default()
222        };
223        let mut type_opts = vec![];
224        let mut ugid_cfg = UgidConfig::default();
225        for opt in components {
226            let mut o = opt.splitn(2, '=');
227            let kind = o.next().context("`shared-dir` options must not be empty")?;
228            let value = o
229                .next()
230                .context("`shared-dir` options must be of the form `kind=value`")?;
231
232            if !ugid_cfg
233                .parse_ugid_config(kind, value)
234                .context("failed to parse ugid config")?
235            {
236                match kind {
237                    "type" => {
238                        shared_dir.kind = value.parse().with_context(|| {
239                            anyhow!("`type` must be one of `fs` or `9p` but {value}")
240                        })?
241                    }
242                    _ => type_opts.push(opt),
243                }
244            }
245        }
246        shared_dir.ugid = (ugid_cfg.uid, ugid_cfg.gid);
247        shared_dir.uid_map = ugid_cfg.uid_map;
248        shared_dir.gid_map = ugid_cfg.gid_map;
249
250        match shared_dir.kind {
251            SharedDirKind::FS => {
252                shared_dir.fs_cfg = from_key_values(&type_opts.join(","))
253                    .map_err(|e| anyhow!("failed to parse fs config '{:?}': {e}", type_opts))?;
254
255                if shared_dir.fs_cfg.ascii_casefold && !shared_dir.fs_cfg.negative_timeout.is_zero()
256                {
257                    // Disallow the combination of `ascii_casefold` and `negative_timeout` because
258                    // negative dentry caches doesn't wort well in scenarios like the following:
259                    // 1. Lookup "foo", an non-existing file. Negative dentry is cached on the
260                    //    guest.
261                    // 2. Create "FOO".
262                    // 3. Lookup "foo". This needs to be successful on the casefold directory, but
263                    //    the lookup can fail due the negative cache created at 1.
264                    bail!("'negative_timeout' cannot be used with 'ascii_casefold'");
265                }
266            }
267            SharedDirKind::P9 => {
268                shared_dir.p9_cfg = type_opts
269                    .join(":")
270                    .parse()
271                    .map_err(|e| anyhow!("failed to parse 9p config '{:?}': {e}", type_opts))?;
272            }
273        }
274        Ok(shared_dir)
275    }
276}
277
278#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
279#[serde(deny_unknown_fields)]
280pub struct PmemExt2Option {
281    pub path: PathBuf,
282    pub blocks_per_group: u32,
283    pub inodes_per_group: u32,
284    pub size: u32,
285    pub ugid: (Option<u32>, Option<u32>),
286    pub uid_map: String,
287    pub gid_map: String,
288}
289
290impl Default for PmemExt2Option {
291    fn default() -> Self {
292        let blocks_per_group = 4096;
293        let inodes_per_group = 1024;
294        let size = ext2::BLOCK_SIZE as u32 * blocks_per_group; // only one block group
295        let ugid_cfg = UgidConfig::default();
296        Self {
297            path: Default::default(),
298            blocks_per_group,
299            inodes_per_group,
300            size,
301            ugid: (ugid_cfg.uid, ugid_cfg.gid),
302            uid_map: ugid_cfg.uid_map,
303            gid_map: ugid_cfg.gid_map,
304        }
305    }
306}
307
308pub fn parse_pmem_ext2_option(param: &str) -> Result<PmemExt2Option, String> {
309    let mut opt = PmemExt2Option::default();
310    let mut components = param.split(':');
311    opt.path = PathBuf::from(
312        components
313            .next()
314            .ok_or("missing source path for `pmem-ext2`")?,
315    );
316
317    let mut ugid_cfg = UgidConfig::default();
318    for c in components {
319        let mut o = c.splitn(2, '=');
320        let kind = o.next().ok_or("`pmem-ext2` options must not be empty")?;
321        let value = o
322            .next()
323            .ok_or("`pmem-ext2` options must be of the form `kind=value`")?;
324
325        if !ugid_cfg
326            .parse_ugid_config(kind, value)
327            .map_err(|e| format!("failed to parse ugid config for pmem-ext2: {e:#}"))?
328        {
329            match kind {
330                "blocks_per_group" => {
331                    opt.blocks_per_group = value.parse().map_err(|e| {
332                        format!("failed to parse blocks_per_groups '{value}': {e:#}")
333                    })?
334                }
335                "inodes_per_group" => {
336                    opt.inodes_per_group = value.parse().map_err(|e| {
337                        format!("failed to parse inodes_per_groups '{value}': {e:#}")
338                    })?
339                }
340                "size" => {
341                    opt.size = value
342                        .parse()
343                        .map_err(|e| format!("failed to parse memory size '{value}': {e:#}"))?
344                }
345                _ => return Err(format!("invalid `pmem-ext2` option: {kind}")),
346            }
347        }
348    }
349    opt.ugid = (ugid_cfg.uid, ugid_cfg.gid);
350    opt.uid_map = ugid_cfg.uid_map;
351    opt.gid_map = ugid_cfg.gid_map;
352
353    Ok(opt)
354}
355
356#[cfg(test)]
357mod tests {
358    use std::path::Path;
359    use std::path::PathBuf;
360    use std::time::Duration;
361
362    use argh::FromArgs;
363    use devices::virtio::fs::CachePolicy;
364
365    use super::*;
366    use crate::crosvm::config::from_key_values;
367
368    #[test]
369    fn parse_coiommu_options() {
370        use std::time::Duration;
371
372        use devices::CoIommuParameters;
373        use devices::CoIommuUnpinPolicy;
374
375        // unpin_policy
376        let coiommu_params = from_key_values::<CoIommuParameters>("unpin_policy=off").unwrap();
377        assert_eq!(
378            coiommu_params,
379            CoIommuParameters {
380                unpin_policy: CoIommuUnpinPolicy::Off,
381                ..Default::default()
382            }
383        );
384        let coiommu_params = from_key_values::<CoIommuParameters>("unpin_policy=lru").unwrap();
385        assert_eq!(
386            coiommu_params,
387            CoIommuParameters {
388                unpin_policy: CoIommuUnpinPolicy::Lru,
389                ..Default::default()
390            }
391        );
392        let coiommu_params = from_key_values::<CoIommuParameters>("unpin_policy=foo");
393        assert!(coiommu_params.is_err());
394
395        // unpin_interval
396        let coiommu_params = from_key_values::<CoIommuParameters>("unpin_interval=42").unwrap();
397        assert_eq!(
398            coiommu_params,
399            CoIommuParameters {
400                unpin_interval: Duration::from_secs(42),
401                ..Default::default()
402            }
403        );
404        let coiommu_params = from_key_values::<CoIommuParameters>("unpin_interval=foo");
405        assert!(coiommu_params.is_err());
406
407        // unpin_limit
408        let coiommu_params = from_key_values::<CoIommuParameters>("unpin_limit=256").unwrap();
409        assert_eq!(
410            coiommu_params,
411            CoIommuParameters {
412                unpin_limit: Some(256),
413                ..Default::default()
414            }
415        );
416        let coiommu_params = from_key_values::<CoIommuParameters>("unpin_limit=0");
417        assert!(coiommu_params.is_err());
418        let coiommu_params = from_key_values::<CoIommuParameters>("unpin_limit=foo");
419        assert!(coiommu_params.is_err());
420
421        // unpin_gen_threshold
422        let coiommu_params =
423            from_key_values::<CoIommuParameters>("unpin_gen_threshold=32").unwrap();
424        assert_eq!(
425            coiommu_params,
426            CoIommuParameters {
427                unpin_gen_threshold: 32,
428                ..Default::default()
429            }
430        );
431        let coiommu_params = from_key_values::<CoIommuParameters>("unpin_gen_threshold=foo");
432        assert!(coiommu_params.is_err());
433
434        // All together
435        let coiommu_params = from_key_values::<CoIommuParameters>(
436            "unpin_policy=lru,unpin_interval=90,unpin_limit=8,unpin_gen_threshold=64",
437        )
438        .unwrap();
439        assert_eq!(
440            coiommu_params,
441            CoIommuParameters {
442                unpin_policy: CoIommuUnpinPolicy::Lru,
443                unpin_interval: Duration::from_secs(90),
444                unpin_limit: Some(8),
445                unpin_gen_threshold: 64,
446            }
447        );
448
449        // invalid parameter
450        let coiommu_params = from_key_values::<CoIommuParameters>("unpin_invalid_param=0");
451        assert!(coiommu_params.is_err());
452    }
453
454    #[test]
455    fn vfio_pci_path() {
456        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
457            &[],
458            &["--vfio", "/path/to/dev", "/dev/null"],
459        )
460        .unwrap()
461        .try_into()
462        .unwrap();
463
464        let vfio = config.vfio.first().unwrap();
465
466        assert_eq!(vfio.path, PathBuf::from("/path/to/dev"));
467        assert_eq!(vfio.iommu, IommuDevType::NoIommu);
468        assert_eq!(vfio.guest_address, None);
469    }
470
471    #[test]
472    fn vfio_pci_path_coiommu() {
473        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
474            &[],
475            &["--vfio", "/path/to/dev,iommu=coiommu", "/dev/null"],
476        )
477        .unwrap()
478        .try_into()
479        .unwrap();
480
481        let vfio = config.vfio.first().unwrap();
482
483        assert_eq!(vfio.path, PathBuf::from("/path/to/dev"));
484        assert_eq!(vfio.iommu, IommuDevType::CoIommu);
485        assert_eq!(vfio.guest_address, None);
486    }
487
488    #[test]
489    fn vfio_pci_path_viommu_guest_address() {
490        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
491            &[],
492            &[
493                "--vfio",
494                "/path/to/dev,iommu=viommu,guest-address=42:15.4",
495                "/dev/null",
496            ],
497        )
498        .unwrap()
499        .try_into()
500        .unwrap();
501
502        let vfio = config.vfio.first().unwrap();
503
504        assert_eq!(vfio.path, PathBuf::from("/path/to/dev"));
505        assert_eq!(vfio.iommu, IommuDevType::VirtioIommu);
506        assert_eq!(
507            vfio.guest_address,
508            Some(PciAddress::new(0, 0x42, 0x15, 4).unwrap())
509        );
510    }
511
512    #[test]
513    fn vfio_platform() {
514        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
515            &[],
516            &["--vfio-platform", "/path/to/dev", "/dev/null"],
517        )
518        .unwrap()
519        .try_into()
520        .unwrap();
521
522        let vfio = config.vfio.first().unwrap();
523
524        assert_eq!(vfio.path, PathBuf::from("/path/to/dev"));
525    }
526
527    #[test]
528    fn hypervisor_default() {
529        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(&[], &["/dev/null"])
530            .unwrap()
531            .try_into()
532            .unwrap();
533
534        assert_eq!(config.hypervisor, None);
535    }
536
537    #[test]
538    fn hypervisor_kvm() {
539        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
540            &[],
541            &["--hypervisor", "kvm", "/dev/null"],
542        )
543        .unwrap()
544        .try_into()
545        .unwrap();
546
547        assert_eq!(
548            config.hypervisor,
549            Some(HypervisorKind::Kvm { device: None })
550        );
551    }
552
553    #[test]
554    fn hypervisor_kvm_device() {
555        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
556            &[],
557            &["--hypervisor", "kvm[device=/not/default]", "/dev/null"],
558        )
559        .unwrap()
560        .try_into()
561        .unwrap();
562
563        assert_eq!(
564            config.hypervisor,
565            Some(HypervisorKind::Kvm {
566                device: Some(PathBuf::from("/not/default"))
567            })
568        );
569    }
570
571    #[test]
572    #[cfg(all(target_arch = "aarch64", feature = "halla"))]
573    fn hypervisor_halla() {
574        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
575            &[],
576            &["--hypervisor", "halla", "/dev/null"],
577        )
578        .unwrap()
579        .try_into()
580        .unwrap();
581
582        assert_eq!(
583            config.hypervisor,
584            Some(HypervisorKind::Halla { device: None })
585        );
586    }
587
588    #[test]
589    #[cfg(all(target_arch = "aarch64", feature = "halla"))]
590    fn hypervisor_halla_device() {
591        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
592            &[],
593            &["--hypervisor", "halla[device=/not/default]", "/dev/null"],
594        )
595        .unwrap()
596        .try_into()
597        .unwrap();
598
599        assert_eq!(
600            config.hypervisor,
601            Some(HypervisorKind::Halla {
602                device: Some(PathBuf::from("/not/default"))
603            })
604        );
605    }
606
607    #[test]
608    #[cfg(all(target_arch = "aarch64", feature = "geniezone"))]
609    fn hypervisor_geniezone() {
610        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
611            &[],
612            &["--hypervisor", "geniezone", "/dev/null"],
613        )
614        .unwrap()
615        .try_into()
616        .unwrap();
617
618        assert_eq!(
619            config.hypervisor,
620            Some(HypervisorKind::Geniezone { device: None })
621        );
622    }
623
624    #[test]
625    #[cfg(all(target_arch = "aarch64", feature = "geniezone"))]
626    fn hypervisor_geniezone_device() {
627        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
628            &[],
629            &[
630                "--hypervisor",
631                "geniezone[device=/not/default]",
632                "/dev/null",
633            ],
634        )
635        .unwrap()
636        .try_into()
637        .unwrap();
638
639        assert_eq!(
640            config.hypervisor,
641            Some(HypervisorKind::Geniezone {
642                device: Some(PathBuf::from("/not/default"))
643            })
644        );
645    }
646
647    #[test]
648    #[cfg(all(target_arch = "aarch64", feature = "gunyah"))]
649    fn hypervisor_gunyah() {
650        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
651            &[],
652            &["--hypervisor", "gunyah", "/dev/null"],
653        )
654        .unwrap()
655        .try_into()
656        .unwrap();
657
658        assert_eq!(
659            config.hypervisor,
660            Some(HypervisorKind::Gunyah {
661                device: None,
662                qcom_trusted_vm_id: None,
663                qcom_trusted_vm_pas_id: None,
664            })
665        );
666    }
667
668    #[test]
669    #[cfg(all(target_arch = "aarch64", feature = "gunyah"))]
670    fn hypervisor_gunyah_device() {
671        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
672            &[],
673            &["--hypervisor", "gunyah[device=/not/default]", "/dev/null"],
674        )
675        .unwrap()
676        .try_into()
677        .unwrap();
678
679        assert_eq!(
680            config.hypervisor,
681            Some(HypervisorKind::Gunyah {
682                device: Some(PathBuf::from("/not/default")),
683                qcom_trusted_vm_id: None,
684                qcom_trusted_vm_pas_id: None,
685            })
686        );
687    }
688
689    #[test]
690    #[cfg(all(target_arch = "aarch64", feature = "gunyah"))]
691    fn hypervisor_gunyah_device_with_qtvm_ids() {
692        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
693            &[],
694            &[
695                "--hypervisor",
696                "gunyah[device=/not/default,qcom_trusted_vm_id=0,qcom_trusted_vm_pas_id=0]",
697                "/dev/null",
698            ],
699        )
700        .unwrap()
701        .try_into()
702        .unwrap();
703
704        assert_eq!(
705            config.hypervisor,
706            Some(HypervisorKind::Gunyah {
707                device: Some(PathBuf::from("/not/default")),
708                qcom_trusted_vm_id: Some(0),
709                qcom_trusted_vm_pas_id: Some(0),
710            })
711        );
712    }
713
714    #[test]
715    fn parse_shared_dir() {
716        // Although I want to test /usr/local/bin, Use / instead of
717        // /usr/local/bin, as /usr/local/bin doesn't always exist.
718        let s = "/:usr_local_bin:type=fs:cache=always:uidmap=0 655360 5000,5000 600 50,5050 660410 1994950:gidmap=0 655360 1065,1065 20119 1,1066 656426 3934,5000 600 50,5050 660410 1994950:timeout=3600:rewrite-security-xattrs=true:writeback=true";
719
720        let shared_dir: SharedDir = s.parse().unwrap();
721        assert_eq!(shared_dir.src, Path::new("/").to_path_buf());
722        assert_eq!(shared_dir.tag, "usr_local_bin");
723        assert!(shared_dir.kind == SharedDirKind::FS);
724        assert_eq!(
725            shared_dir.uid_map,
726            "0 655360 5000,5000 600 50,5050 660410 1994950"
727        );
728        assert_eq!(
729            shared_dir.gid_map,
730            "0 655360 1065,1065 20119 1,1066 656426 3934,5000 600 50,5050 660410 1994950"
731        );
732        assert_eq!(shared_dir.fs_cfg.ascii_casefold, false);
733        assert_eq!(shared_dir.fs_cfg.timeout, Duration::from_secs(3600));
734        assert_eq!(shared_dir.fs_cfg.negative_timeout, Duration::ZERO);
735        assert_eq!(shared_dir.fs_cfg.writeback, true);
736        assert_eq!(
737            shared_dir.fs_cfg.cache_policy,
738            devices::virtio::fs::CachePolicy::Always
739        );
740        assert_eq!(shared_dir.fs_cfg.rewrite_security_xattrs, true);
741        assert_eq!(shared_dir.fs_cfg.use_dax, false);
742        assert_eq!(shared_dir.fs_cfg.posix_acl, true);
743        assert_eq!(shared_dir.ugid, (None, None));
744    }
745
746    #[test]
747    fn parse_shared_dir_parses_ascii_casefold_and_posix_acl() {
748        // Although I want to test /usr/local/bin, Use / instead of
749        // /usr/local/bin, as /usr/local/bin doesn't always exist.
750        let s = "/:usr_local_bin:type=fs:ascii_casefold=true:posix_acl=false";
751
752        let shared_dir: SharedDir = s.parse().unwrap();
753        assert_eq!(shared_dir.fs_cfg.ascii_casefold, true);
754        assert_eq!(shared_dir.fs_cfg.posix_acl, false);
755    }
756
757    #[test]
758    fn parse_shared_dir_negative_timeout() {
759        // Although I want to test /usr/local/bin, Use / instead of
760        // /usr/local/bin, as /usr/local/bin doesn't always exist.
761        let s = "/:usr_local_bin:type=fs:cache=always:timeout=3600:negative_timeout=60";
762
763        let shared_dir: SharedDir = s.parse().unwrap();
764        assert_eq!(shared_dir.src, Path::new("/").to_path_buf());
765        assert_eq!(shared_dir.tag, "usr_local_bin");
766        assert!(shared_dir.kind == SharedDirKind::FS);
767        assert_eq!(
768            shared_dir.fs_cfg.cache_policy,
769            devices::virtio::fs::CachePolicy::Always
770        );
771        assert_eq!(shared_dir.fs_cfg.timeout, Duration::from_secs(3600));
772        assert_eq!(shared_dir.fs_cfg.negative_timeout, Duration::from_secs(60));
773    }
774
775    #[test]
776    fn parse_shared_dir_oem() {
777        let shared_dir: SharedDir = "/:oem_etc:type=fs:cache=always:uidmap=0 299 1, 5000 600 50:gidmap=0 300 1, 5000 600 50:timeout=3600:rewrite-security-xattrs=true".parse().unwrap();
778        assert_eq!(shared_dir.src, Path::new("/").to_path_buf());
779        assert_eq!(shared_dir.tag, "oem_etc");
780        assert!(shared_dir.kind == SharedDirKind::FS);
781        assert_eq!(shared_dir.uid_map, "0 299 1, 5000 600 50");
782        assert_eq!(shared_dir.gid_map, "0 300 1, 5000 600 50");
783        assert_eq!(shared_dir.fs_cfg.ascii_casefold, false);
784        assert_eq!(shared_dir.fs_cfg.timeout, Duration::from_secs(3600));
785        assert_eq!(shared_dir.fs_cfg.negative_timeout, Duration::ZERO);
786        assert_eq!(shared_dir.fs_cfg.writeback, false);
787        assert_eq!(
788            shared_dir.fs_cfg.cache_policy,
789            devices::virtio::fs::CachePolicy::Always
790        );
791        assert_eq!(shared_dir.fs_cfg.rewrite_security_xattrs, true);
792        assert_eq!(shared_dir.fs_cfg.use_dax, false);
793        assert_eq!(shared_dir.fs_cfg.posix_acl, true);
794        assert_eq!(shared_dir.ugid, (None, None));
795    }
796
797    #[test]
798    #[cfg(feature = "arc_quota")]
799    fn parse_shared_dir_arcvm_data() {
800        // Test an actual ARCVM argument for /data/, where the path is replaced with `/`.
801        let arcvm_arg = "/:_data:type=fs:cache=always:uidmap=0 655360 5000,5000 600 50,5050 660410 1994950:gidmap=0 655360 1065,1065 20119 1,1066 656426 3934,5000 600 50,5050 660410 1994950:timeout=3600:rewrite-security-xattrs=true:writeback=true:privileged_quota_uids=0";
802        assert_eq!(
803            arcvm_arg.parse::<SharedDir>().unwrap().fs_cfg,
804            devices::virtio::fs::Config {
805                cache_policy: CachePolicy::Always,
806                timeout: Duration::from_secs(3600),
807                rewrite_security_xattrs: true,
808                writeback: true,
809                privileged_quota_uids: vec![0],
810                ..Default::default()
811            }
812        );
813    }
814
815    #[test]
816    fn parse_shared_dir_ugid_set() {
817        let shared_dir: SharedDir =
818            "/:hostRoot:type=fs:uidmap=40417 40417 1:gidmap=5000 5000 1:uid=40417:gid=5000"
819                .parse()
820                .unwrap();
821        assert_eq!(shared_dir.src, Path::new("/").to_path_buf());
822        assert_eq!(shared_dir.tag, "hostRoot");
823        assert!(shared_dir.kind == SharedDirKind::FS);
824        assert_eq!(shared_dir.uid_map, "40417 40417 1");
825        assert_eq!(shared_dir.gid_map, "5000 5000 1");
826        assert_eq!(shared_dir.ugid, (Some(40417), Some(5000)));
827    }
828
829    #[test]
830    fn parse_shared_dir_vm_fio() {
831        // Tests shared-dir argurments used in ChromeOS's vm.Fio tast tests.
832
833        // --shared-dir for rootfs
834        let shared_dir: SharedDir =
835            "/:root:type=fs:cache=always:timeout=5:writeback=false:dax=false:ascii_casefold=false"
836                .parse()
837                .unwrap();
838        assert_eq!(
839            shared_dir.fs_cfg,
840            devices::virtio::fs::Config {
841                cache_policy: CachePolicy::Always,
842                timeout: Duration::from_secs(5),
843                writeback: false,
844                use_dax: false,
845                ascii_casefold: false,
846                ..Default::default()
847            }
848        );
849
850        // --shared-dir for vm.Fio.virtiofs_dax_*
851        let shared_dir: SharedDir =
852            "/:shared:type=fs:cache=auto:timeout=1:writeback=true:dax=true:ascii_casefold=false"
853                .parse()
854                .unwrap();
855        assert_eq!(
856            shared_dir.fs_cfg,
857            devices::virtio::fs::Config {
858                cache_policy: CachePolicy::Auto,
859                timeout: Duration::from_secs(1),
860                writeback: true,
861                use_dax: true,
862                ascii_casefold: false,
863                ..Default::default()
864            }
865        );
866    }
867
868    #[test]
869    fn parse_cache_policy() {
870        // The default policy is `auto`.
871        assert_eq!(
872            "/:_data:type=fs"
873                .parse::<SharedDir>()
874                .unwrap()
875                .fs_cfg
876                .cache_policy,
877            CachePolicy::Auto
878        );
879        assert_eq!(
880            "/:_data:type=fs:cache=always"
881                .parse::<SharedDir>()
882                .unwrap()
883                .fs_cfg
884                .cache_policy,
885            CachePolicy::Always
886        );
887        assert_eq!(
888            "/:_data:type=fs:cache=auto"
889                .parse::<SharedDir>()
890                .unwrap()
891                .fs_cfg
892                .cache_policy,
893            CachePolicy::Auto
894        );
895        assert_eq!(
896            "/:_data:type=fs:cache=never"
897                .parse::<SharedDir>()
898                .unwrap()
899                .fs_cfg
900                .cache_policy,
901            CachePolicy::Never
902        );
903
904        // cache policy is case-sensitive
905        assert!("/:_data:type=fs:cache=Always".parse::<SharedDir>().is_err());
906        assert!("/:_data:type=fs:cache=ALWAYS".parse::<SharedDir>().is_err());
907        assert!("/:_data:type=fs:cache=Auto".parse::<SharedDir>().is_err());
908        assert!("/:_data:type=fs:cache=AUTO".parse::<SharedDir>().is_err());
909        assert!("/:_data:type=fs:cache=Never".parse::<SharedDir>().is_err());
910        assert!("/:_data:type=fs:cache=NEVER".parse::<SharedDir>().is_err());
911
912        // we don't accept unknown policy
913        assert!("/:_data:type=fs:cache=foobar".parse::<SharedDir>().is_err());
914    }
915
916    #[cfg(feature = "arc_quota")]
917    #[test]
918    fn parse_privileged_quota_uids() {
919        assert_eq!(
920            "/:_data:type=fs:privileged_quota_uids=0"
921                .parse::<SharedDir>()
922                .unwrap()
923                .fs_cfg
924                .privileged_quota_uids,
925            vec![0]
926        );
927        assert_eq!(
928            "/:_data:type=fs:privileged_quota_uids=0 1 2 3 4"
929                .parse::<SharedDir>()
930                .unwrap()
931                .fs_cfg
932                .privileged_quota_uids,
933            vec![0, 1, 2, 3, 4]
934        );
935    }
936
937    #[test]
938    fn parse_dax() {
939        // DAX is disabled by default
940        assert!(
941            !"/:_data:type=fs"
942                .parse::<SharedDir>()
943                .unwrap()
944                .fs_cfg
945                .use_dax
946        );
947        assert!(
948            "/:_data:type=fs:dax=true"
949                .parse::<SharedDir>()
950                .unwrap()
951                .fs_cfg
952                .use_dax
953        );
954        assert!(
955            !"/:_data:type=fs:dax=false"
956                .parse::<SharedDir>()
957                .unwrap()
958                .fs_cfg
959                .use_dax
960        );
961    }
962
963    #[test]
964    fn parse_pmem_ext2() {
965        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
966            &[],
967            &["--pmem-ext2", "/path/to/dir", "/dev/null"],
968        )
969        .unwrap()
970        .try_into()
971        .unwrap();
972
973        let opt = config.pmem_ext2.first().unwrap();
974
975        assert_eq!(opt.path, PathBuf::from("/path/to/dir"));
976    }
977
978    #[test]
979    fn parse_pmem_ext2_size() {
980        let blocks_per_group = 2048;
981        let inodes_per_group = 1024;
982        let size = 4096 * blocks_per_group;
983
984        let config: Config = crate::crosvm::cmdline::RunCommand::from_args(
985            &[],
986            &[
987                "--pmem-ext2",
988                &format!("/path/to/dir:blocks_per_group={blocks_per_group}:inodes_per_group={inodes_per_group}:size={size}"),
989                "/dev/null",
990            ],
991        )
992        .unwrap()
993        .try_into()
994        .unwrap();
995
996        let opt = config.pmem_ext2.first().unwrap();
997
998        assert_eq!(opt.path, PathBuf::from("/path/to/dir"));
999        assert_eq!(opt.blocks_per_group, blocks_per_group);
1000        assert_eq!(opt.inodes_per_group, inodes_per_group);
1001        assert_eq!(opt.size, size);
1002    }
1003}