Skip to main content

arcbox_computer_runtime/config/
jailer.rs

1use std::path::{Path, PathBuf};
2
3use arcbox_vm_driver::{CgroupSpec, IsolationSpec};
4use serde::{Deserialize, Serialize};
5
6use crate::error::ComputerError;
7
8/// The per-VM isolation half of `[firecracker.jailer]`.
9///
10/// Who the VMM runs as, where its chroot lives, which namespaces and
11/// cgroup it enters — everything [`IsolationSpec`] carries. The jailer
12/// binary and the rlimits it applies are node-wide and belong to whoever
13/// builds the adapter that spawns it; they stay in the same TOML table,
14/// read by that composer.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct JailerConfig {
17    /// UID the Firecracker process runs as inside the jail.
18    pub uid: u32,
19    /// GID the Firecracker process runs as inside the jail.
20    pub gid: u32,
21    /// Base chroot directory (default: `/srv/jailer`).
22    pub chroot_base_dir: Option<String>,
23    /// Network namespace path (e.g., `/var/run/netns/myns`).
24    pub netns: Option<String>,
25    /// Create a new PID namespace.
26    #[serde(default)]
27    pub new_pid_ns: bool,
28    /// cgroup version (`"1"` or `"2"`).
29    pub cgroup_version: Option<String>,
30    /// Parent cgroup path.
31    pub parent_cgroup: Option<String>,
32}
33
34impl JailerConfig {
35    /// The jailer's default chroot base, used when the config names none.
36    pub const DEFAULT_CHROOT_BASE: &'static str = "/srv/jailer";
37
38    /// The chroot base the jailer actually uses: the configured directory,
39    /// or the jailer's own default.
40    pub fn chroot_base(&self) -> &Path {
41        Path::new(
42            self.chroot_base_dir
43                .as_deref()
44                .unwrap_or(Self::DEFAULT_CHROOT_BASE),
45        )
46    }
47}
48
49impl TryFrom<&JailerConfig> for IsolationSpec {
50    type Error = ComputerError;
51
52    /// Who the VMM runs as, where its chroot lives, which namespaces and
53    /// cgroup it enters.
54    ///
55    /// A `parent_cgroup` without a `cgroup_version` keeps the jailer's own
56    /// default, version 1, made explicit here because the spec's
57    /// [`CgroupSpec`] always names a version.
58    fn try_from(jc: &JailerConfig) -> Result<Self, ComputerError> {
59        let cgroup = match (jc.cgroup_version.as_deref(), jc.parent_cgroup.as_deref()) {
60            (None, None) => None,
61            (version, parent) => Some(CgroupSpec {
62                version: match version {
63                    None => 1,
64                    Some("1") => 1,
65                    Some("2") => 2,
66                    Some(other) => {
67                        return Err(ComputerError::Config(format!(
68                            "jailer cgroup_version must be \"1\" or \"2\", got {other:?}"
69                        )));
70                    }
71                },
72                parent: parent.map(str::to_owned),
73            }),
74        };
75        Ok(Self::Jailer {
76            uid: jc.uid,
77            gid: jc.gid,
78            chroot_base: jc.chroot_base().to_path_buf(),
79            netns: jc.netns.as_deref().map(PathBuf::from),
80            new_pid_ns: jc.new_pid_ns,
81            cgroup,
82        })
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    fn jailer() -> JailerConfig {
91        JailerConfig {
92            uid: 1000,
93            gid: 1000,
94            chroot_base_dir: None,
95            netns: None,
96            new_pid_ns: false,
97            cgroup_version: None,
98            parent_cgroup: None,
99        }
100    }
101
102    #[test]
103    fn isolation_defaults_the_chroot_base_and_carries_no_cgroup_when_unset() {
104        let spec = IsolationSpec::try_from(&jailer()).unwrap();
105        assert_eq!(
106            spec,
107            IsolationSpec::Jailer {
108                uid: 1000,
109                gid: 1000,
110                chroot_base: PathBuf::from("/srv/jailer"),
111                netns: None,
112                new_pid_ns: false,
113                cgroup: None,
114            }
115        );
116    }
117
118    #[test]
119    fn isolation_maps_the_cgroup_fields_and_the_jailer_default_version() {
120        let mut jc = jailer();
121        jc.chroot_base_dir = Some("/jail".into());
122        jc.netns = Some("/var/run/netns/x".into());
123        jc.new_pid_ns = true;
124        jc.cgroup_version = Some("2".into());
125        jc.parent_cgroup = Some("arcbox".into());
126        let IsolationSpec::Jailer {
127            chroot_base,
128            netns,
129            new_pid_ns,
130            cgroup,
131            ..
132        } = IsolationSpec::try_from(&jc).unwrap()
133        else {
134            panic!("jailer config converts to jailer isolation");
135        };
136        assert_eq!(chroot_base, PathBuf::from("/jail"));
137        assert_eq!(netns, Some(PathBuf::from("/var/run/netns/x")));
138        assert!(new_pid_ns);
139        assert_eq!(
140            cgroup,
141            Some(CgroupSpec {
142                version: 2,
143                parent: Some("arcbox".into())
144            })
145        );
146
147        // A parent alone rides on the jailer's default cgroup version.
148        jc.cgroup_version = None;
149        let IsolationSpec::Jailer { cgroup, .. } = IsolationSpec::try_from(&jc).unwrap() else {
150            unreachable!()
151        };
152        assert_eq!(cgroup.map(|c| c.version), Some(1));
153
154        jc.cgroup_version = Some("v2".into());
155        assert!(matches!(
156            IsolationSpec::try_from(&jc),
157            Err(ComputerError::Config(_))
158        ));
159    }
160}