Skip to content

libc-free Rust and hardened_std #118

@zvonkok

Description

@zvonkok

Summary

This issue tracks the development of hardened_std, a security-hardened replacement for Rust's standard library, specifically designed for NVRC's minimal init use case in confidential VMs.

Status: Planning

Motivation

NVRC runs as PID 1 in ephemeral confidential VMs with NVIDIA GPUs. As the init process, it has full system access, making security paramount. The standard library provides too many capabilities and too much attack surface for our constrained environment.

Key drivers:

  1. Reduced attack surface - Less compiled code = fewer potential vulnerabilities
  2. Enforced security constraints - Whitelist-only access to filesystem, processes, and sockets
  3. no_std compatibility - Enable smaller binary size and pure syscall interface
  4. Fail-fast philosophy - In an ephemeral VM with fresh filesystem, if something exists that shouldn't, it's an error

Design Principles

1. Fresh Filesystem Assumption

NVRC boots in an ephemeral VM with a fresh filesystem. No defensive "remove if exists" logic needed:

// Traditional approach (hidden bugs):
if path.exists() { remove_file(path)?; }
create_file(path)?;

// hardened_std approach (fail-fast):
create_file(path)?;  // Fails if exists - something is wrong!

2. Whitelist-Only Access

All operations restricted to explicitly allowed paths/binaries:

const ALLOWED_PATHS: &[&str] = &["/proc/sys/", "/dev/", "/var/run/", "/sys/"];
const ALLOWED_BINARIES: &[&str] = &[
    "/bin/nvidia-persistenced",
    "/bin/nvidia-smi", 
    "/sbin/modprobe",
    "/usr/bin/kata-agent",
];

3. Static Arguments Only

Process arguments must be &'static str - prevents runtime injection:

// Allowed:
Command::new("/sbin/modprobe").args(&["nvidia"]).spawn()?;

// Not allowed (won't compile with strict mode):
Command::new("/bin/sh").args(&["-c", &user_input]).spawn()?;

4. Size Limits

Write operations have enforced size limits (100 bytes sufficient for sysctl values).

Proposed Modules

Module Description
hardened_std::fs File operations with path whitelisting, size limits
hardened_std::process Process execution with binary whitelisting
hardened_std::os::unix::net Unix sockets with path whitelisting
hardened_std::panic VM power-off on panic
hardened_std::collections Stack-allocated HashMap
hardened_std::kernlog Direct syslog via libc
hardened_std::os::fd AsFd/AsRawFd traits

no_std Transition Roadmap

Planned

  • hardened_std::fs - File operations with path whitelisting
  • hardened_std::process - Process execution with binary whitelisting
  • hardened_std::os::unix::net - Unix sockets with path whitelisting
  • Replace std::sync::Once with once_cell (no_std compatible)
  • hardened_std::fs::exists() - Replace std::path::Path::exists()
  • hardened_std::panic - Panic hook and power_off for VM shutdown
  • Implement std::os::fd::AsFd for hardened types
  • Full #![no_std] main.rs with panic handler and global allocator
  • Replace nix crate with direct libc syscalls
  • Replace kernlog crate with hardened_std::kernlog
  • Replace rlimit crate with direct libc calls
  • Remove anyhow dependency (use hardened_std::error)
  • Audit all dependencies for no_std compatibility

Technical Details

no_std Binary Structure

#![no_std]
#![no_main]

extern crate alloc;

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    // Write to stderr, sync, power off
    hardened_std::panic::power_off()
}

#[global_allocator]
static ALLOCATOR: LibcAllocator = LibcAllocator;

#[no_mangle]
pub extern "C" fn main() -> isize { /* ... */ }

Build Configuration

Requires nightly Rust with -Zbuild-std:

# .cargo/config.toml
[target.x86_64-unknown-linux-musl]
build-std = ["core", "alloc"]
rustflags = ["-C", "panic=abort"]

Dependencies no_std Status

Crate no_std? Action
log Yes Keep (default-features = false)
once_cell Yes Keep (features = ["alloc"])
libc Yes Keep
cfg-if Yes Keep
anyhow Yes Replace with hardened_std::error
nix No Replace with direct libc
kernlog No Replace with hardened_std::kernlog
rlimit No Replace with direct libc

Security Benefits

  1. Compile-time enforcement - Invalid paths/binaries won't compile
  2. Runtime validation - Extra layer for dynamic values
  3. Fail-fast - Unexpected state causes immediate failure
  4. Minimal surface - Only implement what's actually needed
  5. No hidden behavior - Direct syscalls, no std abstractions

Open Questions

  • Best approach for incremental migration vs. big-bang switch?
  • How to handle test dependencies that need std?
  • Should the whitelist be configurable via features?

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions