diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index f1115e2..97ced8f 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -25,6 +25,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 id: filter + with: filters: | code: @@ -62,7 +63,7 @@ jobs: # The #[cfg(coverage)] paths in require_root() panic if not run as root, # ensuring coverage accurately reflects execution with proper permissions. - name: Generate coverage - run: sudo -E env "PATH=$PATH" cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info --ignore-filename-regex 'main\.rs' + run: sudo -E env "PATH=$PATH" cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info -- --include-ignored - name: Upload coverage to Coveralls uses: coverallsapp/github-action@cfd0633edbd2411b532b808ba7a8b5e04f76d2c8 # v2.3.4 diff --git a/.github/workflows/static-checks.yaml b/.github/workflows/static-checks.yaml index f361099..d62bc49 100644 --- a/.github/workflows/static-checks.yaml +++ b/.github/workflows/static-checks.yaml @@ -51,7 +51,7 @@ jobs: with: toolchain: stable - - run: sudo -E env "PATH=$PATH" cargo test --all-features + - run: sudo -E env "PATH=$PATH" cargo test --all-features -- --include-ignored # Check code formatting against Rust style guidelines formatting: diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 341ca7c..f4b50f1 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -20,11 +20,3 @@ path = "fuzz_targets/kernel_params.rs" test = false doc = false bench = false - -# Mount table parsing fuzz target -[[bin]] -name = "mount_parsing" -path = "fuzz_targets/mount_parsing.rs" -test = false -doc = false -bench = false diff --git a/fuzz/fuzz_targets/mount_parsing.rs b/fuzz/fuzz_targets/mount_parsing.rs deleted file mode 100644 index ab97160..0000000 --- a/fuzz/fuzz_targets/mount_parsing.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Fuzz target for /proc/mounts parsing. -//! -//! Tests that arbitrary input to is_mounted_in() doesn't panic. -//! The function parses mount table format with whitespace splitting. - -#![no_main] - -use libfuzzer_sys::fuzz_target; -use NVRC::mount::is_mounted_in; - -fuzz_target!(|data: &[u8]| { - // Only fuzz valid UTF-8 strings - if let Ok(input) = std::str::from_utf8(data) { - // Test with various target paths - let _ = is_mounted_in(input, "/"); - let _ = is_mounted_in(input, "/dev"); - let _ = is_mounted_in(input, "/proc"); - let _ = is_mounted_in(input, "/sys/kernel/security"); - let _ = is_mounted_in(input, ""); - - // Also fuzz the path parameter - let _ = is_mounted_in("tmpfs /tmp tmpfs rw 0 0\n", input); - } -}); - diff --git a/src/coreutils.rs b/src/coreutils.rs deleted file mode 100644 index 2bdf4e4..0000000 --- a/src/coreutils.rs +++ /dev/null @@ -1,242 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (c) NVIDIA CORPORATION - -use anyhow::{Context, Result}; -use nix::fcntl::AT_FDCWD; -use nix::sys::stat::{self, Mode, SFlag}; -use nix::unistd::symlinkat; -use std::fs; -use std::path::Path; - -#[cfg(test)] -use serial_test::serial; -/// Create (or update) a symbolic link from target to linkpath. -/// Idempotent: if link already points to target, it is left unchanged. -pub fn ln(target: &str, linkpath: &str) -> Result<()> { - let path = Path::new(linkpath); - - // Check if it's already a correct symlink - if let Ok(existing) = fs::read_link(path) { - if existing == Path::new(target) { - return Ok(()); // already correct - } - } - - // Remove whatever exists at linkpath (file, symlink, etc.) - if path.exists() || path.is_symlink() { - let _ = fs::remove_file(path); - } - - symlinkat(target, AT_FDCWD, linkpath).with_context(|| format!("ln {} -> {}", linkpath, target)) -} - -/// Create (or replace) a character device node with desired major/minor. -/// Always recreates to avoid stale metadata/permissions. -pub fn mknod(path: &str, kind: SFlag, major: u64, minor: u64) -> Result<()> { - if Path::new(path).exists() { - fs::remove_file(path).with_context(|| format!("remove {} failed", path))?; - } - - let perm = Mode::from_bits_truncate(0o666); - - // Temporarily clear umask so we get exact permissions requested - let old_umask = stat::umask(Mode::empty()); - let result = stat::mknod(path, kind, perm, stat::makedev(major, minor)); - stat::umask(old_umask); // Restore original umask - - result.with_context(|| format!("mknod {} failed", path)) -} -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::require_root; - use std::os::unix::fs::{FileTypeExt, MetadataExt}; - use tempfile::TempDir; - - // ==================== ln tests ==================== - - #[test] - fn test_ln_creates_symlink() { - let tmpdir = TempDir::new().unwrap(); - let target = tmpdir.path().join("target.txt"); - let link = tmpdir.path().join("link.txt"); - - // Create target file - fs::write(&target, "hello").unwrap(); - - // Create symlink - ln(target.to_str().unwrap(), link.to_str().unwrap()).unwrap(); - - // Verify symlink exists and points to target - assert!(link.is_symlink()); - assert_eq!(fs::read_link(&link).unwrap(), target); - } - - #[test] - fn test_ln_idempotent() { - let tmpdir = TempDir::new().unwrap(); - let target = tmpdir.path().join("target.txt"); - let link = tmpdir.path().join("link.txt"); - - fs::write(&target, "hello").unwrap(); - - // Create symlink twice - should succeed both times - ln(target.to_str().unwrap(), link.to_str().unwrap()).unwrap(); - ln(target.to_str().unwrap(), link.to_str().unwrap()).unwrap(); - - assert!(link.is_symlink()); - assert_eq!(fs::read_link(&link).unwrap(), target); - } - - #[test] - fn test_ln_updates_existing_link() { - let tmpdir = TempDir::new().unwrap(); - let target1 = tmpdir.path().join("target1.txt"); - let target2 = tmpdir.path().join("target2.txt"); - let link = tmpdir.path().join("link.txt"); - - fs::write(&target1, "first").unwrap(); - fs::write(&target2, "second").unwrap(); - - // Create link to target1 - ln(target1.to_str().unwrap(), link.to_str().unwrap()).unwrap(); - assert_eq!(fs::read_link(&link).unwrap(), target1); - - // Update link to target2 - ln(target2.to_str().unwrap(), link.to_str().unwrap()).unwrap(); - assert_eq!(fs::read_link(&link).unwrap(), target2); - } - - #[test] - fn test_ln_to_nonexistent_target() { - let tmpdir = TempDir::new().unwrap(); - let target = tmpdir.path().join("nonexistent"); - let link = tmpdir.path().join("link.txt"); - - // Symlinks can point to nonexistent targets (dangling symlinks) - ln(target.to_str().unwrap(), link.to_str().unwrap()).unwrap(); - - assert!(link.is_symlink()); - assert!(!link.exists()); // Dangling symlink - } - - #[test] - fn test_ln_replaces_regular_file() { - let tmpdir = TempDir::new().unwrap(); - let target = tmpdir.path().join("target.txt"); - let link = tmpdir.path().join("link.txt"); - - fs::write(&target, "target").unwrap(); - fs::write(&link, "was a file").unwrap(); // Regular file at link path - - // ln should replace the regular file with a symlink - ln(target.to_str().unwrap(), link.to_str().unwrap()).unwrap(); - - assert!(link.is_symlink()); - assert_eq!(fs::read_link(&link).unwrap(), target); - } - - // ==================== mknod tests ==================== - // FIFO (named pipe) can be created without root, char devices need root - - #[test] - #[serial] // umask is process-global - fn test_mknod_creates_fifo() { - // FIFO doesn't require root - tests the mknod logic - let tmpdir = TempDir::new().unwrap(); - let fifopath = tmpdir.path().join("test_fifo"); - - mknod(fifopath.to_str().unwrap(), SFlag::S_IFIFO, 0, 0).unwrap(); - - assert!(fifopath.exists()); - let meta = fs::metadata(&fifopath).unwrap(); - assert!(meta.file_type().is_fifo()); - } - - #[test] - #[serial] // umask is process-global - fn test_mknod_fifo_permissions() { - let tmpdir = TempDir::new().unwrap(); - let fifopath = tmpdir.path().join("test_fifo_perm"); - - mknod(fifopath.to_str().unwrap(), SFlag::S_IFIFO, 0, 0).unwrap(); - - let meta = fs::metadata(&fifopath).unwrap(); - let mode = meta.mode() & 0o777; - assert_eq!(mode, 0o666); - } - - #[test] - #[serial] // umask is process-global - fn test_mknod_replaces_existing_with_fifo() { - let tmpdir = TempDir::new().unwrap(); - let fifopath = tmpdir.path().join("test_replace_fifo"); - - // Create a regular file first - fs::write(&fifopath, "placeholder").unwrap(); - assert!(fifopath.is_file()); - - // mknod should replace it with a FIFO - mknod(fifopath.to_str().unwrap(), SFlag::S_IFIFO, 0, 0).unwrap(); - - let meta = fs::metadata(&fifopath).unwrap(); - assert!(meta.file_type().is_fifo()); - } - - #[test] - #[serial] // umask is process-global - fn test_mknod_umask_not_applied() { - let tmpdir = TempDir::new().unwrap(); - let fifopath = tmpdir.path().join("test_umask_fifo"); - - // Set a restrictive umask - let old_umask = stat::umask(Mode::from_bits_truncate(0o077)); - - // Create FIFO - should get exact permissions despite umask - mknod(fifopath.to_str().unwrap(), SFlag::S_IFIFO, 0, 0).unwrap(); - - // Restore umask - stat::umask(old_umask); - - let meta = fs::metadata(&fifopath).unwrap(); - let mode = meta.mode() & 0o777; - assert_eq!(mode, 0o666, "umask should not affect mknod permissions"); - } - - // Char device tests - require root, will rerun with sudo if needed - #[test] - #[serial] - fn test_mknod_creates_char_device() { - require_root(); - - let tmpdir = TempDir::new().unwrap(); - let devpath = tmpdir.path().join("test_null"); - - mknod(devpath.to_str().unwrap(), SFlag::S_IFCHR, 1, 3).unwrap(); - - let meta = fs::metadata(&devpath).unwrap(); - assert!(meta.file_type().is_char_device()); - } - - // ==================== error path tests ==================== - // These tests trigger the .with_context() closures for coverage - - #[test] - fn test_ln_error_nonexistent_parent() { - // symlinkat fails when parent directory doesn't exist - let result = ln("/target", "/nonexistent/dir/link"); - assert!(result.is_err()); - let err = result.unwrap_err().to_string(); - assert!(err.contains("ln"), "error should mention ln: {}", err); - } - - #[test] - #[serial] // umask is process-global - fn test_mknod_error_nonexistent_parent() { - // mknod fails when parent directory doesn't exist - let result = mknod("/nonexistent/dir/fifo", SFlag::S_IFIFO, 0, 0); - assert!(result.is_err()); - let err = result.unwrap_err().to_string(); - assert!(err.contains("mknod"), "error should mention mknod: {}", err); - } -} diff --git a/src/lib.rs b/src/lib.rs index f0c010f..97bf78c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,6 @@ #![allow(non_snake_case)] //! The main binary uses these modules internally. -pub mod coreutils; pub mod daemon; pub mod execute; pub mod kata_agent; diff --git a/src/lockdown.rs b/src/lockdown.rs index 69930f2..83c5d7e 100644 --- a/src/lockdown.rs +++ b/src/lockdown.rs @@ -71,6 +71,7 @@ mod tests { } #[test] + #[ignore] // Permanently disables module loading until reboot - run with --include-ignored on CI fn test_disable_modules_loading() { require_root(); @@ -91,6 +92,7 @@ mod tests { } #[test] + #[ignore] // Installs real power_off hook - run with --include-ignored on CI fn test_set_panic_hook() { // Installs the real hook (with power_off) - just don't trigger it! let _ = set_panic_hook(); diff --git a/src/main.rs b/src/main.rs index 1ad80db..8caf5d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) NVIDIA CORPORATION -mod coreutils; mod daemon; mod execute; mod kata_agent; diff --git a/src/mount.rs b/src/mount.rs index ce44003..351ec9e 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -3,10 +3,8 @@ //! Filesystem setup for the minimal init environment. -use crate::coreutils::{ln, mknod}; use anyhow::{Context, Result}; use nix::mount::MsFlags; -use nix::sys::stat; use std::fs; use std::path::Path; @@ -51,42 +49,11 @@ fn mount_optional( Ok(()) } -/// Create /dev symlinks pointing to /proc entries. -/// Standard Unix convention: /dev/stdin, /dev/stdout, /dev/stderr should -/// exist for programs that expect them. /dev/fd provides access to open -/// file descriptors via /proc/self/fd. -fn proc_symlinks(root: &str) -> Result<()> { - for (src, dst) in [ - ("/proc/kcore", "dev/core"), - ("/proc/self/fd", "dev/fd"), - ("/proc/self/fd/0", "dev/stdin"), - ("/proc/self/fd/1", "dev/stdout"), - ("/proc/self/fd/2", "dev/stderr"), - ] { - ln(src, &format!("{root}/{dst}"))?; - } - Ok(()) -} - -/// Create essential /dev device nodes for basic I/O. -/// These character devices are fundamental Unix primitives: -/// - /dev/null: discard output, read returns EOF -/// - /dev/zero: infinite stream of zeros -/// - /dev/random, /dev/urandom: cryptographic randomness -fn device_nodes(root: &str) -> Result<()> { - for (path, minor) in [ - ("dev/null", 3u64), - ("dev/zero", 5u64), - ("dev/random", 8u64), - ("dev/urandom", 9u64), - ] { - mknod(&format!("{root}/{path}"), stat::SFlag::S_IFCHR, 1, minor)?; // major 1 = memory devices - } - Ok(()) -} - /// Set up the minimal filesystem hierarchy required for GPU initialization. -/// Creates /proc, /dev, /sys, /run, /tmp mounts and essential device nodes. +/// Creates /proc, /dev, /sys, /run, /tmp mounts. +/// devtmpfs automatically creates standard device nodes; symlinks +/// (/dev/stdin, /dev/stdout, /dev/stderr, /dev/fd, /dev/core) are +/// created later by kata-agent. pub fn setup() -> Result<()> { setup_at("") } @@ -97,6 +64,8 @@ fn setup_at(root: &str) -> Result<()> { mount("proc", &format!("{root}/proc"), "proc", common, None)?; + // devtmpfs automatically creates /dev/null, /dev/zero, /dev/random, /dev/urandom + // Symlinks (/dev/stdin, /dev/stdout, /dev/stderr, /dev/fd, /dev/core) are created by kata-agent let dev_flags = MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_RELATIME; mount( "dev", @@ -136,8 +105,6 @@ fn setup_at(root: &str) -> Result<()> { common, )?; - proc_symlinks(root)?; - device_nodes(root)?; Ok(()) } @@ -208,24 +175,6 @@ mod tests { ); } - // === Functions that need root but are safe === - - #[test] - fn test_proc_symlinks() { - // These symlinks already exist on any Linux system. - // ln() is idempotent - returns Ok if already correct. - require_root(); - assert!(proc_symlinks("").is_ok()); - } - - #[test] - fn test_device_nodes() { - // mknod() removes existing nodes first, then recreates. - // Safe: just recreates /dev/null, /dev/zero, etc. with same params. - require_root(); - assert!(device_nodes("").is_ok()); - } - // === setup_at() tests with temp directory === #[test] @@ -247,13 +196,11 @@ mod tests { let result = setup_at(root); assert!(result.is_ok(), "setup_at failed: {:?}", result); - // Verify device nodes were created + // devtmpfs creates these automatically assert!(Path::new(&format!("{root}/dev/null")).exists()); assert!(Path::new(&format!("{root}/dev/zero")).exists()); - - // Verify symlinks were created - assert!(Path::new(&format!("{root}/dev/stdin")).is_symlink()); - assert!(Path::new(&format!("{root}/dev/stdout")).is_symlink()); + assert!(Path::new(&format!("{root}/dev/random")).exists()); + assert!(Path::new(&format!("{root}/dev/urandom")).exists()); // Cleanup: unmount in reverse order for dir in ["tmp", "run", "sys", "dev", "proc"] {