Files
wifi-densepose/vendor/ruvector/crates/rvf/rvf-kernel/src/initramfs.rs

672 lines
20 KiB
Rust

//! Real initramfs builder producing valid cpio archives (newc format).
//!
//! The cpio "newc" (SVR4 with no CRC) format is what the Linux kernel expects
//! for initramfs archives. Each entry consists of a 110-byte ASCII header
//! followed by the filename (NUL-terminated, padded to 4-byte boundary),
//! followed by the file data (padded to 4-byte boundary).
//!
//! The archive is terminated by a trailer entry with the name "TRAILER!!!".
use flate2::write::GzEncoder;
use flate2::Compression;
use std::io::Write;
use crate::error::KernelError;
/// Magic number for cpio newc format headers.
const CPIO_NEWC_MAGIC: &str = "070701";
/// Default /init script that mounts essential filesystems, configures
/// networking, and starts services.
pub fn default_init_script(services: &[&str]) -> String {
let mut script = String::from(
r#"#!/bin/sh
# RVF initramfs init script
# Generated by rvf-kernel
set -e
echo "RVF initramfs booting..."
# Mount essential filesystems
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
mkdir -p /dev/pts /dev/shm
mount -t devpts devpts /dev/pts
mount -t tmpfs tmpfs /dev/shm
mount -t tmpfs tmpfs /tmp
mount -t tmpfs tmpfs /run
# Set hostname
hostname rvf
# Configure loopback
ip link set lo up
# Configure primary network interface
for iface in eth0 ens3 enp0s3; do
if [ -d "/sys/class/net/$iface" ]; then
ip link set "$iface" up
# Try DHCP if udhcpc is available
if command -v udhcpc >/dev/null 2>&1; then
udhcpc -i "$iface" -s /etc/udhcpc/simple.script -q &
fi
break
fi
done
# Set up minimal /etc
echo "root:x:0:0:root:/root:/bin/sh" > /etc/passwd
echo "root:x:0:" > /etc/group
echo "nameserver 8.8.8.8" > /etc/resolv.conf
echo "rvf" > /etc/hostname
"#,
);
// Start requested services
for service in services {
script.push_str(&format!(
"# Start service: {service}\n\
echo \"Starting {service}...\"\n"
));
match *service {
"sshd" | "dropbear" => {
script.push_str(
"mkdir -p /etc/dropbear\n\
if command -v dropbear >/dev/null 2>&1; then\n\
dropbear -R -F -E -p 2222 &\n\
elif command -v sshd >/dev/null 2>&1; then\n\
mkdir -p /etc/ssh\n\
ssh-keygen -A 2>/dev/null || true\n\
/usr/sbin/sshd -p 2222\n\
fi\n",
);
}
"rvf-server" => {
script.push_str(
"if command -v rvf-server >/dev/null 2>&1; then\n\
rvf-server --listen 0.0.0.0:8080 &\n\
fi\n",
);
}
other => {
script.push_str(&format!(
"if command -v {other} >/dev/null 2>&1; then\n\
{other} &\n\
fi\n"
));
}
}
script.push('\n');
}
script.push_str(
"echo \"RVF initramfs ready.\"\n\
\n\
# Drop to shell or wait\n\
exec /bin/sh\n",
);
script
}
/// A builder for creating cpio newc archives suitable for Linux initramfs.
pub struct CpioBuilder {
/// Accumulated cpio archive bytes (uncompressed).
data: Vec<u8>,
/// Monotonically increasing inode counter.
next_ino: u32,
}
impl CpioBuilder {
/// Create a new, empty cpio archive builder.
pub fn new() -> Self {
Self {
data: Vec::with_capacity(64 * 1024),
next_ino: 1,
}
}
/// Add a directory entry to the archive.
///
/// `path` must not include a leading `/` in the archive (e.g., "bin", "etc").
/// Mode 0o755 is used for directories.
pub fn add_dir(&mut self, path: &str) {
self.add_entry(path, 0o040755, &[]);
}
/// Add a regular file to the archive.
///
/// `path` is the archive-internal path (e.g., "init", "bin/busybox").
/// `mode` is the Unix file mode (e.g., 0o100755 for executable).
pub fn add_file(&mut self, path: &str, mode: u32, content: &[u8]) {
self.add_entry(path, mode, content);
}
/// Add a symlink to the archive.
///
/// The symlink `path` will point to `target`.
pub fn add_symlink(&mut self, path: &str, target: &str) {
self.add_entry(path, 0o120777, target.as_bytes());
}
/// Add a device node (character or block).
///
/// `mode` should include the device type bits (e.g., 0o020666 for char device).
/// `devmajor` and `devminor` are the device major/minor numbers.
pub fn add_device(&mut self, path: &str, mode: u32, devmajor: u32, devminor: u32) {
let ino = self.next_ino;
self.next_ino += 1;
let name_with_nul = format!("{path}\0");
let name_len = name_with_nul.len() as u32;
let filesize = 0u32;
let header = format!(
"{CPIO_NEWC_MAGIC}\
{ino:08X}\
{mode:08X}\
{uid:08X}\
{gid:08X}\
{nlink:08X}\
{mtime:08X}\
{filesize:08X}\
{devmajor:08X}\
{devminor:08X}\
{rdevmajor:08X}\
{rdevminor:08X}\
{namesize:08X}\
{check:08X}",
uid = 0u32,
gid = 0u32,
nlink = 1u32,
mtime = 0u32,
rdevmajor = devmajor,
rdevminor = devminor,
namesize = name_len,
check = 0u32,
);
self.data.extend_from_slice(header.as_bytes());
self.data.extend_from_slice(name_with_nul.as_bytes());
pad4(&mut self.data);
// No file data for device nodes
}
/// Finalize the archive by appending the TRAILER!!! entry.
///
/// Returns the raw (uncompressed) cpio archive bytes.
pub fn finish(mut self) -> Vec<u8> {
self.add_entry("TRAILER!!!", 0, &[]);
self.data
}
/// Finalize and gzip-compress the archive.
///
/// Returns the gzipped cpio archive suitable for use as a Linux initramfs.
pub fn finish_gzipped(self) -> Result<Vec<u8>, KernelError> {
let raw = self.finish();
let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
encoder
.write_all(&raw)
.map_err(|e| KernelError::CompressionFailed(e.to_string()))?;
encoder
.finish()
.map_err(|e| KernelError::CompressionFailed(e.to_string()))
}
fn add_entry(&mut self, path: &str, mode: u32, content: &[u8]) {
let ino = self.next_ino;
self.next_ino += 1;
let name_with_nul = format!("{path}\0");
let name_len = name_with_nul.len() as u32;
let filesize = content.len() as u32;
// cpio newc header is 110 bytes of ASCII hex fields
let header = format!(
"{CPIO_NEWC_MAGIC}\
{ino:08X}\
{mode:08X}\
{uid:08X}\
{gid:08X}\
{nlink:08X}\
{mtime:08X}\
{filesize:08X}\
{devmajor:08X}\
{devminor:08X}\
{rdevmajor:08X}\
{rdevminor:08X}\
{namesize:08X}\
{check:08X}",
uid = 0u32,
gid = 0u32,
nlink = if mode & 0o040000 != 0 { 2u32 } else { 1u32 },
mtime = 0u32,
devmajor = 0u32,
devminor = 0u32,
rdevmajor = 0u32,
rdevminor = 0u32,
namesize = name_len,
check = 0u32,
);
debug_assert_eq!(header.len(), 110);
self.data.extend_from_slice(header.as_bytes());
self.data.extend_from_slice(name_with_nul.as_bytes());
pad4(&mut self.data);
if !content.is_empty() {
self.data.extend_from_slice(content);
pad4(&mut self.data);
}
}
}
impl Default for CpioBuilder {
fn default() -> Self {
Self::new()
}
}
/// Pad `data` to the next 4-byte boundary with NUL bytes.
fn pad4(data: &mut Vec<u8>) {
let rem = data.len() % 4;
if rem != 0 {
let padding = 4 - rem;
data.extend(std::iter::repeat_n(0u8, padding));
}
}
/// Build a complete initramfs with standard directory structure and an /init script.
///
/// `services` are the names of services to start in the init script
/// (e.g., "sshd", "rvf-server").
///
/// `extra_binaries` are (archive_path, content) pairs for additional binaries
/// to include (e.g., ("bin/busybox", &busybox_bytes)).
///
/// Returns a gzipped cpio archive.
pub fn build_initramfs(
services: &[&str],
extra_binaries: &[(&str, &[u8])],
) -> Result<Vec<u8>, KernelError> {
let mut cpio = CpioBuilder::new();
// Create directory structure
let dirs = [
".",
"bin",
"sbin",
"etc",
"etc/udhcpc",
"dev",
"proc",
"sys",
"tmp",
"var",
"var/log",
"var/run",
"run",
"root",
"lib",
"usr",
"usr/bin",
"usr/sbin",
"usr/lib",
"mnt",
"opt",
];
for dir in &dirs {
cpio.add_dir(dir);
}
// Create essential device nodes
cpio.add_device("dev/console", 0o020600, 5, 1);
cpio.add_device("dev/ttyS0", 0o020660, 4, 64);
cpio.add_device("dev/null", 0o020666, 1, 3);
cpio.add_device("dev/zero", 0o020666, 1, 5);
cpio.add_device("dev/urandom", 0o020444, 1, 9);
// Create /init script
let init_script = default_init_script(services);
cpio.add_file("init", 0o100755, init_script.as_bytes());
// Create a minimal udhcpc script
let udhcpc_script = r#"#!/bin/sh
case "$1" in
bound|renew)
ip addr add "$ip/$mask" dev "$interface"
if [ -n "$router" ]; then
ip route add default via "$router"
fi
if [ -n "$dns" ]; then
echo "nameserver $dns" > /etc/resolv.conf
fi
;;
esac
"#;
cpio.add_file(
"etc/udhcpc/simple.script",
0o100755,
udhcpc_script.as_bytes(),
);
// Add extra binaries
for (path, content) in extra_binaries {
cpio.add_file(path, 0o100755, content);
}
cpio.finish_gzipped()
}
/// Build an ultra-fast boot initramfs optimized for minimal startup time.
///
/// Compared to `build_initramfs`, this:
/// - Skips network interface enumeration/DHCP
/// - Mounts only /proc, /sys, /dev (no /dev/pts, /dev/shm, /tmp, /run)
/// - No /etc setup (no passwd, resolv.conf, hostname)
/// - Starts services immediately without probing
/// - Uses minimal directory structure
///
/// Target: kernel-to-service in under 50ms of userspace init time.
pub fn build_fast_initramfs(
services: &[&str],
extra_binaries: &[(&str, &[u8])],
) -> Result<Vec<u8>, KernelError> {
let mut cpio = CpioBuilder::new();
// Minimal directory structure
let dirs = [".", "bin", "sbin", "dev", "proc", "sys", "tmp", "run"];
for dir in &dirs {
cpio.add_dir(dir);
}
// Essential device nodes only
cpio.add_device("dev/console", 0o020600, 5, 1);
cpio.add_device("dev/ttyS0", 0o020660, 4, 64);
cpio.add_device("dev/null", 0o020666, 1, 3);
cpio.add_device("dev/urandom", 0o020444, 1, 9);
// Ultra-fast /init script
let mut script = String::from(
"#!/bin/sh\n\
mount -t proc proc /proc\n\
mount -t sysfs sysfs /sys\n\
mount -t devtmpfs devtmpfs /dev\n",
);
for service in services {
match *service {
"sshd" | "dropbear" => {
script.push_str(
"mkdir -p /etc/dropbear\n\
dropbear -R -F -E -p 2222 &\n",
);
}
"rvf-server" => {
script.push_str("rvf-server --listen 0.0.0.0:8080 &\n");
}
other => {
script.push_str(&format!("{other} &\n"));
}
}
}
script.push_str("exec /bin/sh\n");
cpio.add_file("init", 0o100755, script.as_bytes());
// Add extra binaries
for (path, content) in extra_binaries {
cpio.add_file(path, 0o100755, content);
}
cpio.finish_gzipped()
}
/// Parse a cpio newc archive and return the list of entries.
///
/// Each entry is returned as (path, mode, filesize, data_offset_in_archive).
/// This is primarily used for testing/verification.
pub fn parse_cpio_entries(data: &[u8]) -> Result<Vec<(String, u32, u32)>, KernelError> {
let mut entries = Vec::new();
let mut offset = 0;
loop {
if offset + 110 > data.len() {
break;
}
let header = &data[offset..offset + 110];
let header_str = std::str::from_utf8(header).map_err(|_| {
KernelError::InitramfsBuildFailed("invalid cpio header encoding".into())
})?;
// Verify magic
if &header_str[..6] != CPIO_NEWC_MAGIC {
return Err(KernelError::InitramfsBuildFailed(format!(
"invalid cpio magic at offset {offset}: {:?}",
&header_str[..6]
)));
}
let mode = u32::from_str_radix(&header_str[14..22], 16)
.map_err(|_| KernelError::InitramfsBuildFailed("invalid mode".into()))?;
let filesize = u32::from_str_radix(&header_str[54..62], 16)
.map_err(|_| KernelError::InitramfsBuildFailed("invalid filesize".into()))?;
let namesize = u32::from_str_radix(&header_str[94..102], 16)
.map_err(|_| KernelError::InitramfsBuildFailed("invalid namesize".into()))?;
// Name starts right after the 110-byte header
let name_start = offset + 110;
let name_end = name_start + namesize as usize;
if name_end > data.len() {
break;
}
let name_bytes = &data[name_start..name_end];
// Strip trailing NUL
let name = std::str::from_utf8(name_bytes)
.map_err(|_| KernelError::InitramfsBuildFailed("invalid name encoding".into()))?
.trim_end_matches('\0')
.to_string();
if name == "TRAILER!!!" {
break;
}
// Advance past header + name (padded to 4 bytes)
let after_name = align4(name_end);
// Advance past data (padded to 4 bytes)
let data_end = after_name + filesize as usize;
let next_entry = if filesize > 0 {
align4(data_end)
} else {
after_name
};
entries.push((name, mode, filesize));
offset = next_entry;
}
Ok(entries)
}
fn align4(val: usize) -> usize {
(val + 3) & !3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cpio_builder_produces_valid_archive() {
let mut cpio = CpioBuilder::new();
cpio.add_dir("bin");
cpio.add_file("init", 0o100755, b"#!/bin/sh\necho hello\n");
cpio.add_symlink("bin/sh", "/bin/busybox");
let archive = cpio.finish();
// Verify it starts with the cpio magic
assert!(archive.starts_with(b"070701"));
// Parse it back
let entries = parse_cpio_entries(&archive).expect("parse should succeed");
assert_eq!(entries.len(), 3);
// Check directory
assert_eq!(entries[0].0, "bin");
assert_eq!(entries[0].1 & 0o040000, 0o040000); // is a directory
// Check file
assert_eq!(entries[1].0, "init");
assert_eq!(entries[1].1 & 0o100000, 0o100000); // is a regular file
assert_eq!(entries[1].1 & 0o755, 0o755); // executable
assert_eq!(entries[1].2, 21); // file size: "#!/bin/sh\necho hello\n"
// Check symlink
assert_eq!(entries[2].0, "bin/sh");
assert_eq!(entries[2].1 & 0o120000, 0o120000); // is a symlink
}
#[test]
fn cpio_archive_ends_with_trailer() {
let cpio = CpioBuilder::new();
let archive = cpio.finish();
let as_str = String::from_utf8_lossy(&archive);
assert!(as_str.contains("TRAILER!!!"));
}
#[test]
fn build_initramfs_produces_gzipped_cpio() {
let result = build_initramfs(&["sshd"], &[]).expect("build should succeed");
// gzip magic bytes
assert_eq!(result[0], 0x1F);
assert_eq!(result[1], 0x8B);
// Decompress and verify
use flate2::read::GzDecoder;
use std::io::Read;
let mut decoder = GzDecoder::new(&result[..]);
let mut decompressed = Vec::new();
decoder
.read_to_end(&mut decompressed)
.expect("decompress should succeed");
// Verify it's a valid cpio archive
let entries = parse_cpio_entries(&decompressed).expect("parse should succeed");
// Should have directories + devices + init + udhcpc script
assert!(
entries.len() >= 20,
"expected at least 20 entries, got {}",
entries.len()
);
// Check that /init exists
let init_entry = entries.iter().find(|(name, _, _)| name == "init");
assert!(init_entry.is_some(), "must have /init");
// Check that directories exist
let dir_names: Vec<&str> = entries
.iter()
.filter(|(_, mode, _)| mode & 0o040000 != 0)
.map(|(name, _, _)| name.as_str())
.collect();
assert!(dir_names.contains(&"bin"));
assert!(dir_names.contains(&"etc"));
assert!(dir_names.contains(&"proc"));
assert!(dir_names.contains(&"sys"));
assert!(dir_names.contains(&"dev"));
}
#[test]
fn build_initramfs_with_extra_binaries() {
let fake_binary = b"\x7FELF fake binary content";
let result = build_initramfs(&["rvf-server"], &[("bin/rvf-server", fake_binary)])
.expect("build should succeed");
// Decompress
use flate2::read::GzDecoder;
use std::io::Read;
let mut decoder = GzDecoder::new(&result[..]);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed).unwrap();
let entries = parse_cpio_entries(&decompressed).unwrap();
let binary_entry = entries.iter().find(|(name, _, _)| name == "bin/rvf-server");
assert!(binary_entry.is_some(), "must have bin/rvf-server");
assert_eq!(binary_entry.unwrap().2, fake_binary.len() as u32);
}
#[test]
fn default_init_script_mounts_filesystems() {
let script = default_init_script(&[]);
assert!(script.contains("mount -t proc proc /proc"));
assert!(script.contains("mount -t sysfs sysfs /sys"));
assert!(script.contains("mount -t devtmpfs devtmpfs /dev"));
}
#[test]
fn default_init_script_includes_services() {
let script = default_init_script(&["sshd", "rvf-server"]);
assert!(script.contains("dropbear") || script.contains("sshd"));
assert!(script.contains("rvf-server"));
}
#[test]
fn cpio_header_is_110_bytes() {
let mut cpio = CpioBuilder::new();
cpio.add_file("x", 0o100644, b"");
let archive = cpio.finish();
// First entry header should be exactly 110 ASCII chars
let header_str = std::str::from_utf8(&archive[..110]).unwrap();
assert!(header_str.starts_with(CPIO_NEWC_MAGIC));
}
#[test]
fn build_fast_initramfs_is_smaller() {
let normal = build_initramfs(&["sshd", "rvf-server"], &[]).unwrap();
let fast = build_fast_initramfs(&["sshd", "rvf-server"], &[]).unwrap();
// Fast initramfs should be smaller (fewer dirs, shorter init script)
assert!(
fast.len() < normal.len(),
"fast ({}) should be smaller than normal ({})",
fast.len(),
normal.len()
);
// Both should be valid gzip
assert_eq!(fast[0], 0x1F);
assert_eq!(fast[1], 0x8B);
// Decompress and verify it has /init
use flate2::read::GzDecoder;
use std::io::Read;
let mut decoder = GzDecoder::new(&fast[..]);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed).unwrap();
let entries = parse_cpio_entries(&decompressed).unwrap();
let has_init = entries.iter().any(|(name, _, _)| name == "init");
assert!(has_init, "fast initramfs must have /init");
}
#[test]
fn device_nodes_are_parseable() {
let mut cpio = CpioBuilder::new();
cpio.add_device("dev/null", 0o020666, 1, 3);
let archive = cpio.finish();
let entries = parse_cpio_entries(&archive).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].0, "dev/null");
// Character device bit
assert_eq!(entries[0].1 & 0o020000, 0o020000);
}
}