Skip to main content

rvf_kernel/
lib.rs

1//! Real Linux microkernel builder for RVF computational containers.
2//!
3//! This crate provides the tools to build, verify, and embed real Linux
4//! kernel images inside RVF files. It supports:
5//!
6//! - **Prebuilt kernels**: Load a bzImage/ELF from disk and embed it
7//! - **Docker builds**: Reproducible kernel compilation from source
8//! - **Initramfs**: Build valid cpio/newc archives with real /init scripts
9//! - **Verification**: SHA3-256 hash verification of extracted kernels
10//!
11//! # Architecture
12//!
13//! ```text
14//! KernelBuilder
15//!   ├── from_prebuilt(path)  → reads bzImage/ELF from disk
16//!   ├── build_docker()       → builds kernel in Docker container
17//!   ├── build_initramfs()    → creates gzipped cpio archive
18//!   └── embed(store, kernel) → writes KERNEL_SEG to RVF file
19//!
20//! KernelVerifier
21//!   └── verify(header, image) → checks SHA3-256 hash
22//! ```
23
24pub mod config;
25pub mod docker;
26pub mod error;
27pub mod initramfs;
28
29use std::fs;
30use std::path::{Path, PathBuf};
31
32use sha3::{Digest, Sha3_256};
33
34use rvf_types::kernel::{KernelArch, KernelHeader, KernelType, KERNEL_MAGIC};
35use rvf_types::kernel_binding::KernelBinding;
36
37use crate::docker::DockerBuildContext;
38use crate::error::KernelError;
39
40/// Configuration for building or loading a kernel.
41#[derive(Clone, Debug)]
42pub struct KernelConfig {
43    /// Path to a prebuilt kernel image (bzImage or ELF).
44    pub prebuilt_path: Option<PathBuf>,
45    /// Docker build context directory (for building from source).
46    pub docker_context: Option<PathBuf>,
47    /// Kernel command line arguments.
48    pub cmdline: String,
49    /// Target CPU architecture.
50    pub arch: KernelArch,
51    /// Whether to include an initramfs.
52    pub with_initramfs: bool,
53    /// Services to start in the initramfs /init script.
54    pub services: Vec<String>,
55    /// Linux kernel version (for Docker builds).
56    pub kernel_version: Option<String>,
57}
58
59impl Default for KernelConfig {
60    fn default() -> Self {
61        Self {
62            prebuilt_path: None,
63            docker_context: None,
64            cmdline: "console=ttyS0 quiet".to_string(),
65            arch: KernelArch::X86_64,
66            with_initramfs: false,
67            services: Vec::new(),
68            kernel_version: None,
69        }
70    }
71}
72
73/// A built kernel ready to be embedded into an RVF store.
74#[derive(Clone, Debug)]
75pub struct BuiltKernel {
76    /// The raw kernel image bytes (bzImage or ELF).
77    pub bzimage: Vec<u8>,
78    /// Optional initramfs (gzipped cpio archive).
79    pub initramfs: Option<Vec<u8>>,
80    /// The config used to build this kernel.
81    pub config: KernelConfig,
82    /// SHA3-256 hash of the uncompressed kernel image.
83    pub image_hash: [u8; 32],
84    /// Size of the kernel image after compression (or raw size if uncompressed).
85    pub compressed_size: u64,
86}
87
88/// Builder for creating kernel images to embed in RVF files.
89pub struct KernelBuilder {
90    arch: KernelArch,
91    kernel_type: KernelType,
92    config: KernelConfig,
93}
94
95impl KernelBuilder {
96    /// Create a new KernelBuilder targeting the given architecture.
97    pub fn new(arch: KernelArch) -> Self {
98        Self {
99            arch,
100            kernel_type: KernelType::MicroLinux,
101            config: KernelConfig {
102                arch,
103                ..Default::default()
104            },
105        }
106    }
107
108    /// Set the kernel type.
109    pub fn kernel_type(mut self, kt: KernelType) -> Self {
110        self.kernel_type = kt;
111        self
112    }
113
114    /// Set the kernel command line.
115    pub fn cmdline(mut self, cmdline: &str) -> Self {
116        self.config.cmdline = cmdline.to_string();
117        self
118    }
119
120    /// Enable initramfs with the given services.
121    pub fn with_initramfs(mut self, services: &[&str]) -> Self {
122        self.config.with_initramfs = true;
123        self.config.services = services.iter().map(|s| s.to_string()).collect();
124        self
125    }
126
127    /// Set the kernel version (for Docker builds).
128    pub fn kernel_version(mut self, version: &str) -> Self {
129        self.config.kernel_version = Some(version.to_string());
130        self
131    }
132
133    /// Build a kernel from a prebuilt image file on disk.
134    ///
135    /// Supports:
136    /// - Linux bzImage (starts with boot sector magic or bzImage signature)
137    /// - ELF executables (starts with \x7FELF)
138    /// - Raw binary images
139    ///
140    /// The file must be at least 512 bytes (minimum boot sector size).
141    pub fn from_prebuilt(path: &Path) -> Result<BuiltKernel, KernelError> {
142        if !path.exists() {
143            return Err(KernelError::Io(std::io::Error::new(
144                std::io::ErrorKind::NotFound,
145                format!("kernel image not found: {}", path.display()),
146            )));
147        }
148
149        let metadata = fs::metadata(path)?;
150        if metadata.len() < 512 {
151            return Err(KernelError::ImageTooSmall {
152                size: metadata.len(),
153                min_size: 512,
154            });
155        }
156
157        let bzimage = fs::read(path)?;
158
159        // Validate: must start with ELF magic, bzImage setup, or be a raw binary
160        let is_elf = bzimage.len() >= 4 && &bzimage[..4] == b"\x7FELF";
161        let is_bzimage = bzimage.len() >= 514
162            && bzimage[510] == 0x55
163            && bzimage[511] == 0xAA;
164        let is_pe = bzimage.len() >= 2 && &bzimage[..2] == b"MZ";
165
166        if !is_elf && !is_bzimage && !is_pe && metadata.len() < 4096 {
167            return Err(KernelError::InvalidImage {
168                path: path.to_path_buf(),
169                reason: "not a recognized kernel format (ELF, bzImage, or PE)".into(),
170            });
171        }
172
173        let image_hash = sha3_256(&bzimage);
174        let compressed_size = bzimage.len() as u64;
175
176        Ok(BuiltKernel {
177            bzimage,
178            initramfs: None,
179            config: KernelConfig {
180                prebuilt_path: Some(path.to_path_buf()),
181                ..Default::default()
182            },
183            image_hash,
184            compressed_size,
185        })
186    }
187
188    /// Build a kernel using Docker (requires Docker installed).
189    ///
190    /// This downloads the Linux kernel source, applies the RVF microVM config,
191    /// and builds a bzImage inside a Docker container. The result is a real,
192    /// bootable kernel image.
193    ///
194    /// Set `docker_context` to a directory where the Dockerfile and config
195    /// will be written. If None, a temporary directory is used.
196    pub fn build_docker(
197        &self,
198        context_dir: &Path,
199    ) -> Result<BuiltKernel, KernelError> {
200        let version = self
201            .config
202            .kernel_version
203            .as_deref()
204            .unwrap_or(docker::DEFAULT_KERNEL_VERSION);
205
206        let ctx = DockerBuildContext::prepare(context_dir, Some(version))?;
207        let bzimage = ctx.build()?;
208
209        let image_hash = sha3_256(&bzimage);
210        let compressed_size = bzimage.len() as u64;
211
212        let initramfs = if self.config.with_initramfs {
213            let services: Vec<&str> = self.config.services.iter().map(|s| s.as_str()).collect();
214            Some(initramfs::build_initramfs(&services, &[])?)
215        } else {
216            None
217        };
218
219        Ok(BuiltKernel {
220            bzimage,
221            initramfs,
222            config: self.config.clone(),
223            image_hash,
224            compressed_size,
225        })
226    }
227
228    /// Build an initramfs (gzipped cpio archive) with the configured services.
229    ///
230    /// The initramfs contains:
231    /// - Standard directory structure (/bin, /sbin, /etc, /dev, /proc, /sys, ...)
232    /// - Device nodes (console, ttyS0, null, zero, urandom)
233    /// - /init script that mounts filesystems and starts services
234    /// - Any extra binaries passed in `extra_binaries`
235    pub fn build_initramfs(
236        &self,
237        services: &[&str],
238        extra_binaries: &[(&str, &[u8])],
239    ) -> Result<Vec<u8>, KernelError> {
240        initramfs::build_initramfs(services, extra_binaries)
241    }
242
243    /// Get the kernel flags based on the current configuration.
244    pub fn kernel_flags(&self) -> u32 {
245        use rvf_types::kernel::*;
246
247        let mut flags = KERNEL_FLAG_COMPRESSED;
248
249        // VirtIO drivers are always enabled in our config
250        flags |= KERNEL_FLAG_HAS_VIRTIO_NET;
251        flags |= KERNEL_FLAG_HAS_VIRTIO_BLK;
252        flags |= KERNEL_FLAG_HAS_VSOCK;
253        flags |= KERNEL_FLAG_HAS_NETWORKING;
254
255        // Check for service-specific capabilities
256        for svc in &self.config.services {
257            match svc.as_str() {
258                "rvf-server" => {
259                    flags |= KERNEL_FLAG_HAS_QUERY_API;
260                }
261                "sshd" | "dropbear" => {
262                    flags |= KERNEL_FLAG_HAS_ADMIN_API;
263                }
264                _ => {}
265            }
266        }
267
268        flags
269    }
270
271    /// Get the architecture as a `u8` for the KernelHeader.
272    pub fn arch_byte(&self) -> u8 {
273        self.arch as u8
274    }
275
276    /// Get the kernel type as a `u8` for the KernelHeader.
277    pub fn kernel_type_byte(&self) -> u8 {
278        self.kernel_type as u8
279    }
280}
281
282/// Verifier for kernel images extracted from RVF stores.
283pub struct KernelVerifier;
284
285impl KernelVerifier {
286    /// Verify that a kernel image matches the hash in its header.
287    ///
288    /// Parses the 128-byte KernelHeader from `header_bytes`, computes the
289    /// SHA3-256 hash of `image_bytes`, and checks it matches `image_hash`
290    /// in the header.
291    pub fn verify(
292        header_bytes: &[u8; 128],
293        image_bytes: &[u8],
294    ) -> Result<VerifiedKernel, KernelError> {
295        let header = KernelHeader::from_bytes(header_bytes).map_err(|e| {
296            KernelError::InvalidImage {
297                path: PathBuf::from("<embedded>"),
298                reason: format!("invalid kernel header: {e}"),
299            }
300        })?;
301
302        let actual_hash = sha3_256(image_bytes);
303
304        if actual_hash != header.image_hash {
305            return Err(KernelError::HashMismatch {
306                expected: header.image_hash,
307                actual: actual_hash,
308            });
309        }
310
311        let arch = KernelArch::try_from(header.arch).unwrap_or(KernelArch::Unknown);
312        let kernel_type = KernelType::try_from(header.kernel_type).unwrap_or(KernelType::Custom);
313
314        Ok(VerifiedKernel {
315            header,
316            arch,
317            kernel_type,
318            image_size: image_bytes.len() as u64,
319        })
320    }
321
322    /// Verify a kernel+binding pair, checking both the image hash and
323    /// that the binding version is valid.
324    pub fn verify_with_binding(
325        header_bytes: &[u8; 128],
326        binding_bytes: &[u8; 128],
327        image_bytes: &[u8],
328    ) -> Result<(VerifiedKernel, KernelBinding), KernelError> {
329        let verified = Self::verify(header_bytes, image_bytes)?;
330        let binding = KernelBinding::from_bytes_validated(binding_bytes).map_err(|e| {
331            KernelError::InvalidImage {
332                path: PathBuf::from("<embedded>"),
333                reason: format!("invalid kernel binding: {e}"),
334            }
335        })?;
336        Ok((verified, binding))
337    }
338}
339
340/// A kernel that has passed hash verification.
341#[derive(Debug)]
342pub struct VerifiedKernel {
343    /// The parsed kernel header.
344    pub header: KernelHeader,
345    /// The target architecture.
346    pub arch: KernelArch,
347    /// The kernel type.
348    pub kernel_type: KernelType,
349    /// Size of the verified image in bytes.
350    pub image_size: u64,
351}
352
353/// Build a KernelHeader for embedding into an RVF KERNEL_SEG.
354///
355/// This is a convenience function that constructs a properly filled
356/// KernelHeader from a `BuiltKernel` and builder configuration.
357pub fn build_kernel_header(
358    kernel: &BuiltKernel,
359    builder: &KernelBuilder,
360    api_port: u16,
361) -> KernelHeader {
362    let cmdline_offset = 128u64; // header is 128 bytes, cmdline follows
363    let cmdline_length = kernel.config.cmdline.len() as u32;
364
365    KernelHeader {
366        kernel_magic: KERNEL_MAGIC,
367        header_version: 1,
368        arch: builder.arch_byte(),
369        kernel_type: builder.kernel_type_byte(),
370        kernel_flags: builder.kernel_flags(),
371        min_memory_mb: 64, // reasonable default for microVM
372        entry_point: 0x0020_0000, // standard Linux load address
373        image_size: kernel.bzimage.len() as u64,
374        compressed_size: kernel.compressed_size,
375        compression: 0, // uncompressed (bzImage is self-decompressing)
376        api_transport: rvf_types::kernel::ApiTransport::TcpHttp as u8,
377        api_port,
378        api_version: 1,
379        image_hash: kernel.image_hash,
380        build_id: [0u8; 16], // caller should fill with UUID v7
381        build_timestamp: 0,  // caller should fill
382        vcpu_count: 1,
383        reserved_0: 0,
384        cmdline_offset,
385        cmdline_length,
386        reserved_1: 0,
387    }
388}
389
390/// Compute SHA3-256 hash of data.
391pub fn sha3_256(data: &[u8]) -> [u8; 32] {
392    let mut hasher = Sha3_256::new();
393    hasher.update(data);
394    let result = hasher.finalize();
395    let mut hash = [0u8; 32];
396    hash.copy_from_slice(&result);
397    hash
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn sha3_256_produces_32_bytes() {
406        let hash = sha3_256(b"test data");
407        assert_eq!(hash.len(), 32);
408        // Should be deterministic
409        assert_eq!(hash, sha3_256(b"test data"));
410        // Different data -> different hash
411        assert_ne!(hash, sha3_256(b"other data"));
412    }
413
414    #[test]
415    fn kernel_builder_defaults() {
416        let builder = KernelBuilder::new(KernelArch::X86_64);
417        assert_eq!(builder.arch, KernelArch::X86_64);
418        assert_eq!(builder.kernel_type, KernelType::MicroLinux);
419        assert_eq!(builder.arch_byte(), 0x00);
420        assert_eq!(builder.kernel_type_byte(), 0x01);
421    }
422
423    #[test]
424    fn kernel_builder_chaining() {
425        let builder = KernelBuilder::new(KernelArch::Aarch64)
426            .kernel_type(KernelType::Custom)
427            .cmdline("console=ttyAMA0 root=/dev/vda")
428            .with_initramfs(&["sshd", "rvf-server"])
429            .kernel_version("6.6.30");
430
431        assert_eq!(builder.arch, KernelArch::Aarch64);
432        assert_eq!(builder.kernel_type, KernelType::Custom);
433        assert_eq!(builder.config.cmdline, "console=ttyAMA0 root=/dev/vda");
434        assert!(builder.config.with_initramfs);
435        assert_eq!(builder.config.services, vec!["sshd", "rvf-server"]);
436        assert_eq!(builder.config.kernel_version, Some("6.6.30".to_string()));
437    }
438
439    #[test]
440    fn kernel_flags_include_virtio() {
441        let builder = KernelBuilder::new(KernelArch::X86_64);
442        let flags = builder.kernel_flags();
443        assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_VIRTIO_NET != 0);
444        assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_VIRTIO_BLK != 0);
445        assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_VSOCK != 0);
446        assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_NETWORKING != 0);
447        assert!(flags & rvf_types::kernel::KERNEL_FLAG_COMPRESSED != 0);
448    }
449
450    #[test]
451    fn kernel_flags_service_detection() {
452        let builder = KernelBuilder::new(KernelArch::X86_64)
453            .with_initramfs(&["sshd", "rvf-server"]);
454        let flags = builder.kernel_flags();
455        assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_QUERY_API != 0);
456        assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_ADMIN_API != 0);
457    }
458
459    #[test]
460    fn from_prebuilt_rejects_nonexistent() {
461        let result = KernelBuilder::from_prebuilt(Path::new("/nonexistent/bzImage"));
462        assert!(result.is_err());
463        match result.unwrap_err() {
464            KernelError::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::NotFound),
465            other => panic!("expected Io error, got: {other}"),
466        }
467    }
468
469    #[test]
470    fn from_prebuilt_rejects_too_small() {
471        let dir = tempfile::TempDir::new().unwrap();
472        let path = dir.path().join("tiny.img");
473        std::fs::write(&path, &[0u8; 100]).unwrap();
474
475        let result = KernelBuilder::from_prebuilt(&path);
476        assert!(result.is_err());
477        match result.unwrap_err() {
478            KernelError::ImageTooSmall { size, min_size } => {
479                assert_eq!(size, 100);
480                assert_eq!(min_size, 512);
481            }
482            other => panic!("expected ImageTooSmall, got: {other}"),
483        }
484    }
485
486    #[test]
487    fn from_prebuilt_reads_elf() {
488        let dir = tempfile::TempDir::new().unwrap();
489        let path = dir.path().join("kernel.elf");
490
491        // Create a minimal ELF-like file
492        let mut data = vec![0u8; 4096];
493        data[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
494        data[4] = 2; // 64-bit
495        data[5] = 1; // little-endian
496        std::fs::write(&path, &data).unwrap();
497
498        let kernel = KernelBuilder::from_prebuilt(&path).unwrap();
499        assert_eq!(kernel.bzimage.len(), 4096);
500        assert_eq!(kernel.image_hash, sha3_256(&data));
501        assert!(kernel.initramfs.is_none());
502    }
503
504    #[test]
505    fn from_prebuilt_reads_bzimage() {
506        let dir = tempfile::TempDir::new().unwrap();
507        let path = dir.path().join("bzImage");
508
509        // Create a minimal bzImage-like file (boot sector with 0x55AA at 510-511)
510        let mut data = vec![0u8; 8192];
511        data[510] = 0x55;
512        data[511] = 0xAA;
513        std::fs::write(&path, &data).unwrap();
514
515        let kernel = KernelBuilder::from_prebuilt(&path).unwrap();
516        assert_eq!(kernel.bzimage.len(), 8192);
517        assert_eq!(kernel.compressed_size, 8192);
518    }
519
520    #[test]
521    fn verifier_accepts_correct_hash() {
522        // Build a fake kernel image and header with matching hash
523        let image = b"this is a fake kernel image for testing hash verification";
524        let hash = sha3_256(image);
525
526        let header = KernelHeader {
527            kernel_magic: KERNEL_MAGIC,
528            header_version: 1,
529            arch: KernelArch::X86_64 as u8,
530            kernel_type: KernelType::MicroLinux as u8,
531            kernel_flags: 0,
532            min_memory_mb: 64,
533            entry_point: 0x0020_0000,
534            image_size: image.len() as u64,
535            compressed_size: image.len() as u64,
536            compression: 0,
537            api_transport: 0,
538            api_port: 8080,
539            api_version: 1,
540            image_hash: hash,
541            build_id: [0; 16],
542            build_timestamp: 0,
543            vcpu_count: 1,
544            reserved_0: 0,
545            cmdline_offset: 128,
546            cmdline_length: 0,
547            reserved_1: 0,
548        };
549        let header_bytes = header.to_bytes();
550
551        let verified = KernelVerifier::verify(&header_bytes, image).unwrap();
552        assert_eq!(verified.arch, KernelArch::X86_64);
553        assert_eq!(verified.kernel_type, KernelType::MicroLinux);
554        assert_eq!(verified.image_size, image.len() as u64);
555    }
556
557    #[test]
558    fn verifier_rejects_wrong_hash() {
559        let image = b"kernel image data";
560        let wrong_hash = [0xAA; 32];
561
562        let header = KernelHeader {
563            kernel_magic: KERNEL_MAGIC,
564            header_version: 1,
565            arch: KernelArch::X86_64 as u8,
566            kernel_type: KernelType::MicroLinux as u8,
567            kernel_flags: 0,
568            min_memory_mb: 64,
569            entry_point: 0,
570            image_size: image.len() as u64,
571            compressed_size: image.len() as u64,
572            compression: 0,
573            api_transport: 0,
574            api_port: 0,
575            api_version: 1,
576            image_hash: wrong_hash,
577            build_id: [0; 16],
578            build_timestamp: 0,
579            vcpu_count: 0,
580            reserved_0: 0,
581            cmdline_offset: 128,
582            cmdline_length: 0,
583            reserved_1: 0,
584        };
585        let header_bytes = header.to_bytes();
586
587        let result = KernelVerifier::verify(&header_bytes, image);
588        assert!(result.is_err());
589        match result.unwrap_err() {
590            KernelError::HashMismatch { expected, actual } => {
591                assert_eq!(expected, wrong_hash);
592                assert_eq!(actual, sha3_256(image));
593            }
594            other => panic!("expected HashMismatch, got: {other}"),
595        }
596    }
597
598    #[test]
599    fn verifier_with_binding() {
600        let image = b"kernel with binding test";
601        let hash = sha3_256(image);
602
603        let header = KernelHeader {
604            kernel_magic: KERNEL_MAGIC,
605            header_version: 1,
606            arch: KernelArch::X86_64 as u8,
607            kernel_type: KernelType::MicroLinux as u8,
608            kernel_flags: 0,
609            min_memory_mb: 64,
610            entry_point: 0,
611            image_size: image.len() as u64,
612            compressed_size: image.len() as u64,
613            compression: 0,
614            api_transport: 0,
615            api_port: 0,
616            api_version: 1,
617            image_hash: hash,
618            build_id: [0; 16],
619            build_timestamp: 0,
620            vcpu_count: 0,
621            reserved_0: 0,
622            cmdline_offset: 256,
623            cmdline_length: 0,
624            reserved_1: 0,
625        };
626        let header_bytes = header.to_bytes();
627
628        let binding = KernelBinding {
629            manifest_root_hash: [0x11; 32],
630            policy_hash: [0x22; 32],
631            binding_version: 1,
632            min_runtime_version: 0,
633            _pad0: 0,
634            allowed_segment_mask: 0,
635            _reserved: [0; 48],
636        };
637        let binding_bytes = binding.to_bytes();
638
639        let (verified, decoded_binding) =
640            KernelVerifier::verify_with_binding(&header_bytes, &binding_bytes, image).unwrap();
641        assert_eq!(verified.arch, KernelArch::X86_64);
642        assert_eq!(decoded_binding.binding_version, 1);
643        assert_eq!(decoded_binding.manifest_root_hash, [0x11; 32]);
644    }
645
646    #[test]
647    fn build_kernel_header_fills_fields() {
648        let image_data = b"test kernel data for header building";
649        let hash = sha3_256(image_data);
650
651        let kernel = BuiltKernel {
652            bzimage: image_data.to_vec(),
653            initramfs: None,
654            config: KernelConfig {
655                cmdline: "console=ttyS0 root=/dev/vda".to_string(),
656                ..Default::default()
657            },
658            image_hash: hash,
659            compressed_size: image_data.len() as u64,
660        };
661
662        let builder = KernelBuilder::new(KernelArch::X86_64)
663            .with_initramfs(&["sshd"]);
664
665        let header = build_kernel_header(&kernel, &builder, 8080);
666
667        assert_eq!(header.kernel_magic, KERNEL_MAGIC);
668        assert_eq!(header.header_version, 1);
669        assert_eq!(header.arch, KernelArch::X86_64 as u8);
670        assert_eq!(header.kernel_type, KernelType::MicroLinux as u8);
671        assert_eq!(header.image_size, image_data.len() as u64);
672        assert_eq!(header.image_hash, hash);
673        assert_eq!(header.api_port, 8080);
674        assert_eq!(header.min_memory_mb, 64);
675        assert_eq!(header.entry_point, 0x0020_0000);
676        assert_eq!(header.cmdline_length, 27); // "console=ttyS0 root=/dev/vda"
677
678        // Should include ADMIN_API flag because sshd is in services
679        assert!(header.kernel_flags & rvf_types::kernel::KERNEL_FLAG_HAS_ADMIN_API != 0);
680    }
681
682    #[test]
683    fn build_initramfs_via_builder() {
684        let builder = KernelBuilder::new(KernelArch::X86_64);
685        let result = builder.build_initramfs(&["sshd"], &[]).unwrap();
686
687        // Should be gzipped
688        assert_eq!(result[0], 0x1F);
689        assert_eq!(result[1], 0x8B);
690        assert!(result.len() > 100);
691    }
692
693    #[test]
694    fn error_display_formatting() {
695        let err = KernelError::ImageTooSmall {
696            size: 100,
697            min_size: 512,
698        };
699        let msg = format!("{err}");
700        assert!(msg.contains("100"));
701        assert!(msg.contains("512"));
702
703        let err2 = KernelError::HashMismatch {
704            expected: [0xAA; 32],
705            actual: [0xBB; 32],
706        };
707        let msg2 = format!("{err2}");
708        assert!(msg2.contains("aaaa"));
709        assert!(msg2.contains("bbbb"));
710    }
711
712    #[test]
713    fn kernel_config_default() {
714        let cfg = KernelConfig::default();
715        assert!(cfg.prebuilt_path.is_none());
716        assert!(cfg.docker_context.is_none());
717        assert_eq!(cfg.cmdline, "console=ttyS0 quiet");
718        assert_eq!(cfg.arch, KernelArch::X86_64);
719        assert!(!cfg.with_initramfs);
720        assert!(cfg.services.is_empty());
721    }
722}