Skip to main content

rvf_launch/
extract.rs

1//! Kernel and initramfs extraction from RVF files.
2//!
3//! Opens the RVF store read-only, locates the KERNEL_SEG, parses the
4//! KernelHeader, and writes the kernel image and optional initramfs to
5//! temporary files that persist until the returned handles are dropped.
6
7use std::io::Write;
8use std::path::{Path, PathBuf};
9
10use rvf_runtime::RvfStore;
11use rvf_types::kernel::KernelHeader;
12
13use crate::error::LaunchError;
14
15/// Extracted kernel artifacts ready for QEMU consumption.
16pub struct ExtractedKernel {
17    /// Path to the extracted kernel image (bzImage or equivalent).
18    pub kernel_path: PathBuf,
19    /// Path to the extracted initramfs, if present in the KERNEL_SEG.
20    pub initramfs_path: Option<PathBuf>,
21    /// The parsed KernelHeader.
22    pub header: KernelHeader,
23    /// The kernel command line string.
24    pub cmdline: String,
25    /// Temp directory holding the extracted files (kept alive via ownership).
26    _tempdir: tempfile::TempDir,
27}
28
29impl std::fmt::Debug for ExtractedKernel {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.debug_struct("ExtractedKernel")
32            .field("kernel_path", &self.kernel_path)
33            .field("initramfs_path", &self.initramfs_path)
34            .field("cmdline", &self.cmdline)
35            .finish_non_exhaustive()
36    }
37}
38
39/// Extract kernel and initramfs from an RVF file to temporary files.
40///
41/// The returned `ExtractedKernel` owns a `TempDir`; the files are cleaned
42/// up when it is dropped.
43pub fn extract_kernel(rvf_path: &Path) -> Result<ExtractedKernel, LaunchError> {
44    let store = RvfStore::open_readonly(rvf_path)
45        .map_err(|e| LaunchError::KernelExtraction(format!("failed to open store: {e:?}")))?;
46
47    let (header_bytes, remainder) = store
48        .extract_kernel()
49        .map_err(|e| LaunchError::KernelExtraction(format!("segment read error: {e:?}")))?
50        .ok_or_else(|| LaunchError::NoKernelSegment {
51            path: rvf_path.to_path_buf(),
52        })?;
53
54    if header_bytes.len() < 128 {
55        return Err(LaunchError::KernelExtraction(
56            "KernelHeader too short".into(),
57        ));
58    }
59
60    let mut hdr_array = [0u8; 128];
61    hdr_array.copy_from_slice(&header_bytes[..128]);
62    let header = KernelHeader::from_bytes(&hdr_array)
63        .map_err(|e| LaunchError::KernelExtraction(format!("bad KernelHeader: {e:?}")))?;
64
65    // The wire format after the 128-byte KernelHeader (which is already
66    // split off into `header_bytes`) is:
67    //
68    //   For simple embed_kernel (no binding):
69    //     kernel_image || cmdline
70    //
71    //   For embed_kernel_with_binding:
72    //     KernelBinding(128) || cmdline || kernel_image
73    //
74    // We determine the layout from header.image_size which tells us the
75    // kernel image length. The cmdline is header.cmdline_length bytes.
76
77    let image_size = header.image_size as usize;
78    let cmdline_length = header.cmdline_length as usize;
79
80    // Simple format: image comes first in the remainder, then cmdline
81    let (kernel_image, cmdline) = if image_size > 0 && image_size <= remainder.len() {
82        let img = &remainder[..image_size];
83        let cmd = if cmdline_length > 0 && image_size + cmdline_length <= remainder.len() {
84            String::from_utf8_lossy(&remainder[image_size..image_size + cmdline_length])
85                .into_owned()
86        } else {
87            String::new()
88        };
89        (img, cmd)
90    } else {
91        // Fallback: treat entire remainder as kernel image
92        (&remainder[..], String::new())
93    };
94
95    // Write to temp files
96    let tempdir = tempfile::tempdir().map_err(LaunchError::TempFile)?;
97
98    let kernel_file_path = tempdir.path().join("vmlinuz");
99    {
100        let mut f =
101            std::fs::File::create(&kernel_file_path).map_err(LaunchError::TempFile)?;
102        f.write_all(kernel_image).map_err(LaunchError::TempFile)?;
103        f.sync_all().map_err(LaunchError::TempFile)?;
104    }
105
106    // For now we do not split out a separate initramfs from the kernel
107    // image. A future version could detect an appended initramfs using
108    // the standard Linux trailer magic (0x6d65736800000000).
109    let initramfs_path = None;
110
111    Ok(ExtractedKernel {
112        kernel_path: kernel_file_path,
113        initramfs_path,
114        header,
115        cmdline,
116        _tempdir: tempdir,
117    })
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use rvf_runtime::options::RvfOptions;
124    use rvf_types::kernel::KernelArch;
125
126    #[test]
127    fn extract_from_store_with_kernel() {
128        let dir = tempfile::tempdir().unwrap();
129        let rvf_path = dir.path().join("test.rvf");
130
131        let opts = RvfOptions {
132            dimension: 4,
133            ..Default::default()
134        };
135        let mut store = RvfStore::create(&rvf_path, opts).unwrap();
136
137        let image = b"MZ\x00fake-kernel-image-for-testing";
138        store
139            .embed_kernel(
140                KernelArch::X86_64 as u8,
141                0x01,
142                0,
143                image,
144                8080,
145                Some("console=ttyS0"),
146            )
147            .unwrap();
148        store.close().unwrap();
149
150        let extracted = extract_kernel(&rvf_path).unwrap();
151        assert!(extracted.kernel_path.exists());
152
153        let on_disk = std::fs::read(&extracted.kernel_path).unwrap();
154        assert_eq!(on_disk, image);
155        assert_eq!(extracted.header.api_port, 8080);
156        assert_eq!(extracted.cmdline, "console=ttyS0");
157    }
158
159    #[test]
160    fn extract_kernel_no_cmdline() {
161        let dir = tempfile::tempdir().unwrap();
162        let rvf_path = dir.path().join("no_cmd.rvf");
163
164        let opts = RvfOptions {
165            dimension: 4,
166            ..Default::default()
167        };
168        let mut store = RvfStore::create(&rvf_path, opts).unwrap();
169
170        let image = b"fake-kernel";
171        store
172            .embed_kernel(
173                KernelArch::X86_64 as u8,
174                0x01,
175                0,
176                image,
177                9090,
178                None,
179            )
180            .unwrap();
181        store.close().unwrap();
182
183        let extracted = extract_kernel(&rvf_path).unwrap();
184        let on_disk = std::fs::read(&extracted.kernel_path).unwrap();
185        assert_eq!(on_disk, image);
186        assert!(extracted.cmdline.is_empty());
187    }
188
189    #[test]
190    fn extract_returns_error_when_no_kernel() {
191        let dir = tempfile::tempdir().unwrap();
192        let rvf_path = dir.path().join("no_kernel.rvf");
193
194        let opts = RvfOptions {
195            dimension: 4,
196            ..Default::default()
197        };
198        let store = RvfStore::create(&rvf_path, opts).unwrap();
199        store.close().unwrap();
200
201        let result = extract_kernel(&rvf_path);
202        assert!(result.is_err());
203        match result.unwrap_err() {
204            LaunchError::NoKernelSegment { .. } => {}
205            other => panic!("expected NoKernelSegment, got: {other}"),
206        }
207    }
208}