Skip to main content

petri/
initrd.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Boot-mode-independent preparation of Linux initrds.
5
6use crate::PetriInitrd;
7use crate::TempOrPersistentPath;
8use anyhow::Context;
9use std::io::Write;
10use std::path::Path;
11use std::sync::Arc;
12
13/// A file to append to a base initrd, replacing any earlier entry of that name.
14pub struct InitrdFile<'a> {
15    /// Relative path inside the initrd.
16    pub name: &'a str,
17    /// Complete file contents.
18    pub contents: &'a [u8],
19    /// Permissions, without the CPIO file-type bits.
20    pub permissions: u32,
21}
22
23impl PetriInitrd {
24    /// Prepare a temporary initrd from explicit inputs, without inspecting the
25    /// host environment or depending on a VM builder or boot mode.
26    ///
27    /// The base is a gzip-compressed newc CPIO archive and is never modified.
28    /// The caller supplies all files (agent, script, certificates, etc.) and
29    /// the absolute `rdinit` path. An explicit scratch directory must already
30    /// exist; otherwise the system temporary directory is used.
31    /// The output is removed when its last owner is dropped.
32    pub fn prepare(
33        base: &Path,
34        files: &[InitrdFile<'_>],
35        rdinit: &str,
36        scratch_dir: Option<&Path>,
37    ) -> anyhow::Result<Self> {
38        anyhow::ensure!(
39            rdinit.starts_with('/') && !rdinit.chars().any(|c| c.is_control() || c.is_whitespace()),
40            "rdinit must be an absolute path without whitespace or control characters"
41        );
42        let mut data = fs_err::read(base).context("reading base initrd")?;
43        for file in files {
44            anyhow::ensure!(
45                !file.name.is_empty()
46                    && !file.name.starts_with('/')
47                    && !file.name.contains(['\0', '\\'])
48                    && file
49                        .name
50                        .split('/')
51                        .all(|part| !matches!(part, "" | "." | "..")),
52                "invalid initrd file path: {:?}",
53                file.name
54            );
55            anyhow::ensure!(
56                file.permissions & !0o7777 == 0,
57                "invalid initrd file permissions"
58            );
59            data = initrd_cpio::inject_into_initrd(
60                &data,
61                file.name,
62                file.contents,
63                0o100000 | file.permissions,
64            )
65            .with_context(|| format!("injecting {} into initrd", file.name))?;
66        }
67        let mut builder = tempfile::Builder::new();
68        builder.prefix(".petri-initrd-").suffix(".gz");
69        let mut output = match scratch_dir {
70            Some(dir) => builder.tempfile_in(dir),
71            None => builder.tempfile(),
72        }
73        .context("creating prepared initrd")?;
74        output.write_all(&data).context("writing prepared initrd")?;
75        Ok(Self {
76            path: TempOrPersistentPath::Temp(Arc::new(output.into_temp_path())),
77            rdinit_param: rdinit.into(),
78        })
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use std::io::Read;
86    use test_with_tracing::test;
87
88    #[test]
89    fn prepares_explicit_files_and_owns_temporary_output() -> anyhow::Result<()> {
90        let temp = tempfile::tempdir()?;
91        let base = temp.path().join("base.gz");
92        let mut trailer = format!(
93            "070701{}0000000b00000000TRAILER!!!\0",
94            "00000000".repeat(11)
95        )
96        .into_bytes();
97        trailer.resize(trailer.len().div_ceil(4) * 4, 0);
98        let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
99        encoder.write_all(&trailer)?;
100        let original = encoder.finish()?;
101        fs_err::write(&base, &original)?;
102        let files = [
103            InitrdFile {
104                name: "pipette",
105                contents: b"agent",
106                permissions: 0o755,
107            },
108            InitrdFile {
109                name: "init.sh",
110                contents: b"exec /pipette",
111                permissions: 0o755,
112            },
113            InitrdFile {
114                name: "ca.pem",
115                contents: b"explicit CA bundle",
116                permissions: 0o644,
117            },
118        ];
119        let first = PetriInitrd::prepare(&base, &files, "/init.sh", Some(temp.path()))?;
120        let second = PetriInitrd::prepare(&base, &files, "/init.sh", None)?;
121        assert_eq!(first.path.as_ref().parent(), Some(temp.path()));
122        assert_eq!(
123            second.path.as_ref().parent(),
124            Some(std::env::temp_dir().as_path())
125        );
126        assert_ne!(first.path.as_ref(), second.path.as_ref());
127        assert_eq!(first.rdinit_param, "/init.sh");
128        assert_eq!(fs_err::read(&base)?, original);
129        let mut archive = Vec::new();
130        flate2::read::GzDecoder::new(fs_err::File::open(first.path.as_ref())?)
131            .read_to_end(&mut archive)?;
132        let mut offset = 0;
133        for file in files {
134            assert_eq!(&archive[offset..offset + 6], b"070701");
135            let field = |start, end| {
136                u32::from_str_radix(
137                    std::str::from_utf8(&archive[offset + start..offset + end]).unwrap(),
138                    16,
139                )
140                .unwrap() as usize
141            };
142            assert_eq!(field(14, 22), (0o100000 | file.permissions) as usize);
143            let size = field(54, 62);
144            let name_size = field(94, 102);
145            assert_eq!(
146                &archive[offset + 110..offset + 110 + name_size - 1],
147                file.name.as_bytes()
148            );
149            let content_offset = offset + (110 + name_size).div_ceil(4) * 4;
150            assert_eq!(
151                &archive[content_offset..content_offset + size],
152                file.contents
153            );
154            offset = content_offset + size.div_ceil(4) * 4;
155        }
156        let path = first.path.as_ref().to_path_buf();
157        let owner = first.clone();
158        drop(first);
159        assert!(path.exists());
160        drop(owner);
161        assert!(!path.exists());
162        for name in ["../escape", "/absolute", "bad\0name"] {
163            assert!(
164                PetriInitrd::prepare(
165                    &base,
166                    &[InitrdFile {
167                        name,
168                        contents: b"",
169                        permissions: 0o644
170                    }],
171                    "/init.sh",
172                    Some(temp.path()),
173                )
174                .is_err()
175            );
176        }
177        Ok(())
178    }
179}