Skip to main content

rvf_kernel/
initramfs.rs

1//! Real initramfs builder producing valid cpio archives (newc format).
2//!
3//! The cpio "newc" (SVR4 with no CRC) format is what the Linux kernel expects
4//! for initramfs archives. Each entry consists of a 110-byte ASCII header
5//! followed by the filename (NUL-terminated, padded to 4-byte boundary),
6//! followed by the file data (padded to 4-byte boundary).
7//!
8//! The archive is terminated by a trailer entry with the name "TRAILER!!!".
9
10use flate2::write::GzEncoder;
11use flate2::Compression;
12use std::io::Write;
13
14use crate::error::KernelError;
15
16/// Magic number for cpio newc format headers.
17const CPIO_NEWC_MAGIC: &str = "070701";
18
19/// Default /init script that mounts essential filesystems, configures
20/// networking, and starts services.
21pub fn default_init_script(services: &[&str]) -> String {
22    let mut script = String::from(
23        r#"#!/bin/sh
24# RVF initramfs init script
25# Generated by rvf-kernel
26
27set -e
28
29echo "RVF initramfs booting..."
30
31# Mount essential filesystems
32mount -t proc proc /proc
33mount -t sysfs sysfs /sys
34mount -t devtmpfs devtmpfs /dev
35mkdir -p /dev/pts /dev/shm
36mount -t devpts devpts /dev/pts
37mount -t tmpfs tmpfs /dev/shm
38mount -t tmpfs tmpfs /tmp
39mount -t tmpfs tmpfs /run
40
41# Set hostname
42hostname rvf
43
44# Configure loopback
45ip link set lo up
46
47# Configure primary network interface
48for iface in eth0 ens3 enp0s3; do
49    if [ -d "/sys/class/net/$iface" ]; then
50        ip link set "$iface" up
51        # Try DHCP if udhcpc is available
52        if command -v udhcpc >/dev/null 2>&1; then
53            udhcpc -i "$iface" -s /etc/udhcpc/simple.script -q &
54        fi
55        break
56    fi
57done
58
59# Set up minimal /etc
60echo "root:x:0:0:root:/root:/bin/sh" > /etc/passwd
61echo "root:x:0:" > /etc/group
62echo "nameserver 8.8.8.8" > /etc/resolv.conf
63echo "rvf" > /etc/hostname
64
65"#,
66    );
67
68    // Start requested services
69    for service in services {
70        script.push_str(&format!(
71            "# Start service: {service}\n\
72             echo \"Starting {service}...\"\n"
73        ));
74        match *service {
75            "sshd" | "dropbear" => {
76                script.push_str(
77                    "mkdir -p /etc/dropbear\n\
78                     if command -v dropbear >/dev/null 2>&1; then\n\
79                         dropbear -R -F -E -p 2222 &\n\
80                     elif command -v sshd >/dev/null 2>&1; then\n\
81                         mkdir -p /etc/ssh\n\
82                         ssh-keygen -A 2>/dev/null || true\n\
83                         /usr/sbin/sshd -p 2222\n\
84                     fi\n",
85                );
86            }
87            "rvf-server" => {
88                script.push_str(
89                    "if command -v rvf-server >/dev/null 2>&1; then\n\
90                         rvf-server --listen 0.0.0.0:8080 &\n\
91                     fi\n",
92                );
93            }
94            other => {
95                script.push_str(&format!(
96                    "if command -v {other} >/dev/null 2>&1; then\n\
97                         {other} &\n\
98                     fi\n"
99                ));
100            }
101        }
102        script.push('\n');
103    }
104
105    script.push_str(
106        "echo \"RVF initramfs ready.\"\n\
107         \n\
108         # Drop to shell or wait\n\
109         exec /bin/sh\n",
110    );
111
112    script
113}
114
115/// A builder for creating cpio newc archives suitable for Linux initramfs.
116pub struct CpioBuilder {
117    /// Accumulated cpio archive bytes (uncompressed).
118    data: Vec<u8>,
119    /// Monotonically increasing inode counter.
120    next_ino: u32,
121}
122
123impl CpioBuilder {
124    /// Create a new, empty cpio archive builder.
125    pub fn new() -> Self {
126        Self {
127            data: Vec::with_capacity(64 * 1024),
128            next_ino: 1,
129        }
130    }
131
132    /// Add a directory entry to the archive.
133    ///
134    /// `path` must not include a leading `/` in the archive (e.g., "bin", "etc").
135    /// Mode 0o755 is used for directories.
136    pub fn add_dir(&mut self, path: &str) {
137        self.add_entry(path, 0o040755, &[]);
138    }
139
140    /// Add a regular file to the archive.
141    ///
142    /// `path` is the archive-internal path (e.g., "init", "bin/busybox").
143    /// `mode` is the Unix file mode (e.g., 0o100755 for executable).
144    pub fn add_file(&mut self, path: &str, mode: u32, content: &[u8]) {
145        self.add_entry(path, mode, content);
146    }
147
148    /// Add a symlink to the archive.
149    ///
150    /// The symlink `path` will point to `target`.
151    pub fn add_symlink(&mut self, path: &str, target: &str) {
152        self.add_entry(path, 0o120777, target.as_bytes());
153    }
154
155    /// Add a device node (character or block).
156    ///
157    /// `mode` should include the device type bits (e.g., 0o020666 for char device).
158    /// `devmajor` and `devminor` are the device major/minor numbers.
159    pub fn add_device(&mut self, path: &str, mode: u32, devmajor: u32, devminor: u32) {
160        let ino = self.next_ino;
161        self.next_ino += 1;
162
163        let name_with_nul = format!("{path}\0");
164        let name_len = name_with_nul.len() as u32;
165        let filesize = 0u32;
166
167        let header = format!(
168            "{CPIO_NEWC_MAGIC}\
169             {ino:08X}\
170             {mode:08X}\
171             {uid:08X}\
172             {gid:08X}\
173             {nlink:08X}\
174             {mtime:08X}\
175             {filesize:08X}\
176             {devmajor:08X}\
177             {devminor:08X}\
178             {rdevmajor:08X}\
179             {rdevminor:08X}\
180             {namesize:08X}\
181             {check:08X}",
182            uid = 0u32,
183            gid = 0u32,
184            nlink = 1u32,
185            mtime = 0u32,
186            rdevmajor = devmajor,
187            rdevminor = devminor,
188            namesize = name_len,
189            check = 0u32,
190        );
191
192        self.data.extend_from_slice(header.as_bytes());
193        self.data.extend_from_slice(name_with_nul.as_bytes());
194        pad4(&mut self.data);
195        // No file data for device nodes
196    }
197
198    /// Finalize the archive by appending the TRAILER!!! entry.
199    ///
200    /// Returns the raw (uncompressed) cpio archive bytes.
201    pub fn finish(mut self) -> Vec<u8> {
202        self.add_entry("TRAILER!!!", 0, &[]);
203        self.data
204    }
205
206    /// Finalize and gzip-compress the archive.
207    ///
208    /// Returns the gzipped cpio archive suitable for use as a Linux initramfs.
209    pub fn finish_gzipped(self) -> Result<Vec<u8>, KernelError> {
210        let raw = self.finish();
211        let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
212        encoder
213            .write_all(&raw)
214            .map_err(|e| KernelError::CompressionFailed(e.to_string()))?;
215        encoder
216            .finish()
217            .map_err(|e| KernelError::CompressionFailed(e.to_string()))
218    }
219
220    fn add_entry(&mut self, path: &str, mode: u32, content: &[u8]) {
221        let ino = self.next_ino;
222        self.next_ino += 1;
223
224        let name_with_nul = format!("{path}\0");
225        let name_len = name_with_nul.len() as u32;
226        let filesize = content.len() as u32;
227
228        // cpio newc header is 110 bytes of ASCII hex fields
229        let header = format!(
230            "{CPIO_NEWC_MAGIC}\
231             {ino:08X}\
232             {mode:08X}\
233             {uid:08X}\
234             {gid:08X}\
235             {nlink:08X}\
236             {mtime:08X}\
237             {filesize:08X}\
238             {devmajor:08X}\
239             {devminor:08X}\
240             {rdevmajor:08X}\
241             {rdevminor:08X}\
242             {namesize:08X}\
243             {check:08X}",
244            uid = 0u32,
245            gid = 0u32,
246            nlink = if mode & 0o040000 != 0 { 2u32 } else { 1u32 },
247            mtime = 0u32,
248            devmajor = 0u32,
249            devminor = 0u32,
250            rdevmajor = 0u32,
251            rdevminor = 0u32,
252            namesize = name_len,
253            check = 0u32,
254        );
255
256        debug_assert_eq!(header.len(), 110);
257
258        self.data.extend_from_slice(header.as_bytes());
259        self.data.extend_from_slice(name_with_nul.as_bytes());
260        pad4(&mut self.data);
261
262        if !content.is_empty() {
263            self.data.extend_from_slice(content);
264            pad4(&mut self.data);
265        }
266    }
267}
268
269impl Default for CpioBuilder {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275/// Pad `data` to the next 4-byte boundary with NUL bytes.
276fn pad4(data: &mut Vec<u8>) {
277    let rem = data.len() % 4;
278    if rem != 0 {
279        let padding = 4 - rem;
280        data.extend(std::iter::repeat_n(0u8, padding));
281    }
282}
283
284/// Build a complete initramfs with standard directory structure and an /init script.
285///
286/// `services` are the names of services to start in the init script
287/// (e.g., "sshd", "rvf-server").
288///
289/// `extra_binaries` are (archive_path, content) pairs for additional binaries
290/// to include (e.g., ("bin/busybox", &busybox_bytes)).
291///
292/// Returns a gzipped cpio archive.
293pub fn build_initramfs(
294    services: &[&str],
295    extra_binaries: &[(&str, &[u8])],
296) -> Result<Vec<u8>, KernelError> {
297    let mut cpio = CpioBuilder::new();
298
299    // Create directory structure
300    let dirs = [
301        ".", "bin", "sbin", "etc", "etc/udhcpc", "dev", "proc", "sys",
302        "tmp", "var", "var/log", "var/run", "run", "root", "lib",
303        "usr", "usr/bin", "usr/sbin", "usr/lib", "mnt", "opt",
304    ];
305    for dir in &dirs {
306        cpio.add_dir(dir);
307    }
308
309    // Create essential device nodes
310    cpio.add_device("dev/console", 0o020600, 5, 1);
311    cpio.add_device("dev/ttyS0", 0o020660, 4, 64);
312    cpio.add_device("dev/null", 0o020666, 1, 3);
313    cpio.add_device("dev/zero", 0o020666, 1, 5);
314    cpio.add_device("dev/urandom", 0o020444, 1, 9);
315
316    // Create /init script
317    let init_script = default_init_script(services);
318    cpio.add_file("init", 0o100755, init_script.as_bytes());
319
320    // Create a minimal udhcpc script
321    let udhcpc_script = r#"#!/bin/sh
322case "$1" in
323    bound|renew)
324        ip addr add "$ip/$mask" dev "$interface"
325        if [ -n "$router" ]; then
326            ip route add default via "$router"
327        fi
328        if [ -n "$dns" ]; then
329            echo "nameserver $dns" > /etc/resolv.conf
330        fi
331        ;;
332esac
333"#;
334    cpio.add_file(
335        "etc/udhcpc/simple.script",
336        0o100755,
337        udhcpc_script.as_bytes(),
338    );
339
340    // Add extra binaries
341    for (path, content) in extra_binaries {
342        cpio.add_file(path, 0o100755, content);
343    }
344
345    cpio.finish_gzipped()
346}
347
348/// Parse a cpio newc archive and return the list of entries.
349///
350/// Each entry is returned as (path, mode, filesize, data_offset_in_archive).
351/// This is primarily used for testing/verification.
352pub fn parse_cpio_entries(data: &[u8]) -> Result<Vec<(String, u32, u32)>, KernelError> {
353    let mut entries = Vec::new();
354    let mut offset = 0;
355
356    loop {
357        if offset + 110 > data.len() {
358            break;
359        }
360
361        let header = &data[offset..offset + 110];
362        let header_str = std::str::from_utf8(header)
363            .map_err(|_| KernelError::InitramfsBuildFailed("invalid cpio header encoding".into()))?;
364
365        // Verify magic
366        if &header_str[..6] != CPIO_NEWC_MAGIC {
367            return Err(KernelError::InitramfsBuildFailed(format!(
368                "invalid cpio magic at offset {offset}: {:?}",
369                &header_str[..6]
370            )));
371        }
372
373        let mode = u32::from_str_radix(&header_str[14..22], 16)
374            .map_err(|_| KernelError::InitramfsBuildFailed("invalid mode".into()))?;
375        let filesize = u32::from_str_radix(&header_str[54..62], 16)
376            .map_err(|_| KernelError::InitramfsBuildFailed("invalid filesize".into()))?;
377        let namesize = u32::from_str_radix(&header_str[94..102], 16)
378            .map_err(|_| KernelError::InitramfsBuildFailed("invalid namesize".into()))?;
379
380        // Name starts right after the 110-byte header
381        let name_start = offset + 110;
382        let name_end = name_start + namesize as usize;
383        if name_end > data.len() {
384            break;
385        }
386
387        let name_bytes = &data[name_start..name_end];
388        // Strip trailing NUL
389        let name = std::str::from_utf8(name_bytes)
390            .map_err(|_| KernelError::InitramfsBuildFailed("invalid name encoding".into()))?
391            .trim_end_matches('\0')
392            .to_string();
393
394        if name == "TRAILER!!!" {
395            break;
396        }
397
398        // Advance past header + name (padded to 4 bytes)
399        let after_name = align4(name_end);
400        // Advance past data (padded to 4 bytes)
401        let data_end = after_name + filesize as usize;
402        let next_entry = if filesize > 0 {
403            align4(data_end)
404        } else {
405            after_name
406        };
407
408        entries.push((name, mode, filesize));
409        offset = next_entry;
410    }
411
412    Ok(entries)
413}
414
415fn align4(val: usize) -> usize {
416    (val + 3) & !3
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    #[test]
424    fn cpio_builder_produces_valid_archive() {
425        let mut cpio = CpioBuilder::new();
426        cpio.add_dir("bin");
427        cpio.add_file("init", 0o100755, b"#!/bin/sh\necho hello\n");
428        cpio.add_symlink("bin/sh", "/bin/busybox");
429        let archive = cpio.finish();
430
431        // Verify it starts with the cpio magic
432        assert!(archive.starts_with(b"070701"));
433
434        // Parse it back
435        let entries = parse_cpio_entries(&archive).expect("parse should succeed");
436        assert_eq!(entries.len(), 3);
437
438        // Check directory
439        assert_eq!(entries[0].0, "bin");
440        assert_eq!(entries[0].1 & 0o040000, 0o040000); // is a directory
441
442        // Check file
443        assert_eq!(entries[1].0, "init");
444        assert_eq!(entries[1].1 & 0o100000, 0o100000); // is a regular file
445        assert_eq!(entries[1].1 & 0o755, 0o755); // executable
446        assert_eq!(entries[1].2, 21); // file size: "#!/bin/sh\necho hello\n"
447
448        // Check symlink
449        assert_eq!(entries[2].0, "bin/sh");
450        assert_eq!(entries[2].1 & 0o120000, 0o120000); // is a symlink
451    }
452
453    #[test]
454    fn cpio_archive_ends_with_trailer() {
455        let cpio = CpioBuilder::new();
456        let archive = cpio.finish();
457        let as_str = String::from_utf8_lossy(&archive);
458        assert!(as_str.contains("TRAILER!!!"));
459    }
460
461    #[test]
462    fn build_initramfs_produces_gzipped_cpio() {
463        let result = build_initramfs(&["sshd"], &[]).expect("build should succeed");
464
465        // gzip magic bytes
466        assert_eq!(result[0], 0x1F);
467        assert_eq!(result[1], 0x8B);
468
469        // Decompress and verify
470        use flate2::read::GzDecoder;
471        use std::io::Read;
472        let mut decoder = GzDecoder::new(&result[..]);
473        let mut decompressed = Vec::new();
474        decoder
475            .read_to_end(&mut decompressed)
476            .expect("decompress should succeed");
477
478        // Verify it's a valid cpio archive
479        let entries = parse_cpio_entries(&decompressed).expect("parse should succeed");
480
481        // Should have directories + devices + init + udhcpc script
482        assert!(entries.len() >= 20, "expected at least 20 entries, got {}", entries.len());
483
484        // Check that /init exists
485        let init_entry = entries.iter().find(|(name, _, _)| name == "init");
486        assert!(init_entry.is_some(), "must have /init");
487
488        // Check that directories exist
489        let dir_names: Vec<&str> = entries
490            .iter()
491            .filter(|(_, mode, _)| mode & 0o040000 != 0)
492            .map(|(name, _, _)| name.as_str())
493            .collect();
494        assert!(dir_names.contains(&"bin"));
495        assert!(dir_names.contains(&"etc"));
496        assert!(dir_names.contains(&"proc"));
497        assert!(dir_names.contains(&"sys"));
498        assert!(dir_names.contains(&"dev"));
499    }
500
501    #[test]
502    fn build_initramfs_with_extra_binaries() {
503        let fake_binary = b"\x7FELF fake binary content";
504        let result = build_initramfs(
505            &["rvf-server"],
506            &[("bin/rvf-server", fake_binary)],
507        )
508        .expect("build should succeed");
509
510        // Decompress
511        use flate2::read::GzDecoder;
512        use std::io::Read;
513        let mut decoder = GzDecoder::new(&result[..]);
514        let mut decompressed = Vec::new();
515        decoder.read_to_end(&mut decompressed).unwrap();
516
517        let entries = parse_cpio_entries(&decompressed).unwrap();
518        let binary_entry = entries.iter().find(|(name, _, _)| name == "bin/rvf-server");
519        assert!(binary_entry.is_some(), "must have bin/rvf-server");
520        assert_eq!(binary_entry.unwrap().2, fake_binary.len() as u32);
521    }
522
523    #[test]
524    fn default_init_script_mounts_filesystems() {
525        let script = default_init_script(&[]);
526        assert!(script.contains("mount -t proc proc /proc"));
527        assert!(script.contains("mount -t sysfs sysfs /sys"));
528        assert!(script.contains("mount -t devtmpfs devtmpfs /dev"));
529    }
530
531    #[test]
532    fn default_init_script_includes_services() {
533        let script = default_init_script(&["sshd", "rvf-server"]);
534        assert!(script.contains("dropbear") || script.contains("sshd"));
535        assert!(script.contains("rvf-server"));
536    }
537
538    #[test]
539    fn cpio_header_is_110_bytes() {
540        let mut cpio = CpioBuilder::new();
541        cpio.add_file("x", 0o100644, b"");
542        let archive = cpio.finish();
543        // First entry header should be exactly 110 ASCII chars
544        let header_str = std::str::from_utf8(&archive[..110]).unwrap();
545        assert!(header_str.starts_with(CPIO_NEWC_MAGIC));
546    }
547
548    #[test]
549    fn device_nodes_are_parseable() {
550        let mut cpio = CpioBuilder::new();
551        cpio.add_device("dev/null", 0o020666, 1, 3);
552        let archive = cpio.finish();
553        let entries = parse_cpio_entries(&archive).unwrap();
554        assert_eq!(entries.len(), 1);
555        assert_eq!(entries[0].0, "dev/null");
556        // Character device bit
557        assert_eq!(entries[0].1 & 0o020000, 0o020000);
558    }
559}