From 1a1a0bf3e87d6d5ab4c055865fd7a32c11cc3c2e Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 29 Jul 2025 00:13:57 -0400 Subject: [PATCH] Merge EnclaveOS => Nit refactor --- .dockerignore | 3 + .gitignore | 14 ++ src/system/Cargo.lock => Cargo.lock | 4 +- Cargo.toml | 5 + Containerfile | 7 +- LICENSE | 9 + Makefile | 4 +- README.md | 32 ++++ crates/nit/Cargo.toml | 12 ++ crates/nit/src/config.rs | 45 +++++ crates/nit/src/main.rs | 86 ++++++++++ crates/nit/src/platform/aws.rs | 74 ++++++++ crates/nit/src/platform/mod.rs | 78 +++++++++ crates/nit/src/result.rs | 52 ++++++ crates/nit/src/system/mod.rs | 156 +++++++++++++++++ crates/nit/src/system/syscall.rs | 257 ++++++++++++++++++++++++++++ src/aws/Cargo.lock | 24 --- src/aws/Cargo.toml | 10 -- src/aws/src/lib.rs | 34 ---- src/init/Cargo.lock | 33 ---- src/init/Cargo.toml | 14 -- src/init/init.rs | 54 ------ src/system/Cargo.toml | 9 - src/system/src/lib.rs | 129 -------------- 24 files changed, 831 insertions(+), 314 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore rename src/system/Cargo.lock => Cargo.lock (92%) create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 crates/nit/Cargo.toml create mode 100644 crates/nit/src/config.rs create mode 100644 crates/nit/src/main.rs create mode 100644 crates/nit/src/platform/aws.rs create mode 100644 crates/nit/src/platform/mod.rs create mode 100644 crates/nit/src/result.rs create mode 100644 crates/nit/src/system/mod.rs create mode 100644 crates/nit/src/system/syscall.rs delete mode 100644 src/aws/Cargo.lock delete mode 100644 src/aws/Cargo.toml delete mode 100644 src/aws/src/lib.rs delete mode 100644 src/init/Cargo.lock delete mode 100644 src/init/Cargo.toml delete mode 100644 src/init/init.rs delete mode 100644 src/system/Cargo.toml delete mode 100644 src/system/src/lib.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4e2eff2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +out +Containerfile diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1e83bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# ---> Rust +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# These artifacts are made when using Docker +out/ diff --git a/src/system/Cargo.lock b/Cargo.lock similarity index 92% rename from src/system/Cargo.lock rename to Cargo.lock index 904872a..18b77c1 100644 --- a/src/system/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "libc" @@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] -name = "system" +name = "nit" version = "0.1.0" dependencies = [ "libc", diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a9fb651 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +resolver = "2" +members = [ + "crates/nit", +] diff --git a/Containerfile b/Containerfile index f772326..593feff 100644 --- a/Containerfile +++ b/Containerfile @@ -2,15 +2,16 @@ FROM stagex/pallet-rust@sha256:740b9ed5f2a897d45cafdc806976d84231aa50a6499861075 ENV TARGET=x86_64-unknown-linux-musl ENV RUSTFLAGS="-C target-feature=+crt-static" -ENV CARGOFLAGS="--locked --no-default-features --release --target ${TARGET}" +ENV CARGOFLAGS="--locked --all-features --release --target ${TARGET}" +ENV CARGO_TARGET_DIR=/cargo-target ADD . /src -WORKDIR /src/init +WORKDIR /src RUN cargo build ${CARGOFLAGS} WORKDIR /rootfs -RUN cp /src/init/target/${TARGET}/release/init . +RUN install -Dm755 /cargo-target/${TARGET}/release/nit /rootfs/usr/bin/init FROM scratch as package COPY --from=build /rootfs . diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..efd8365 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 Distrust + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile index 6242555..b178d79 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,8 @@ out: out/init: out docker build \ - --tag local/init \ + --tag local/nit \ --progress=plain \ --output type=local,rewrite-timestamp=true,dest=out\ -f Containerfile \ - src/ + . diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e815a8 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# nit + +25% smaller than init + +## Testing with AWS on Amazon Linux + +Ensure the instance is set up with Nitro Enclaves, and run the following +command to install and enable nitro enclave support: + +```sh +# You can leave off `git` if you've already installed it to clone the repo. +sudo dnf install aws-nitro-enclaves-cli make git +sudo systemctl enable --now nitro-enclaves-allocator.service +sudo systemctl enable --now docker.service +# Add the current user to the Docker group +sudo usermod -aG docker $USER +``` + +You may need to reconnect to change the signed-in users. Use `groups` to see if +you are in the `docker` group. + +Then, the EIF can be built using: + +```sh +make +``` + +And tested using: + +```sh +sudo make run +``` diff --git a/crates/nit/Cargo.toml b/crates/nit/Cargo.toml new file mode 100644 index 0000000..23313e2 --- /dev/null +++ b/crates/nit/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "nit" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[features] +aws = [] +default = ["aws"] + +[dependencies] +libc = "0.2.172" diff --git a/crates/nit/src/config.rs b/crates/nit/src/config.rs new file mode 100644 index 0000000..f520087 --- /dev/null +++ b/crates/nit/src/config.rs @@ -0,0 +1,45 @@ +use crate::platform::{self, Platform}; +use crate::result::{Context, Result}; + +#[derive(Debug)] +pub enum Mode { + Spawn, + Exec, +} + +#[derive(Debug)] +pub struct Config { + pub platform: Option>, + pub mode: Mode, + pub target: String, +} + +pub fn get_config() -> Result { + let mut values = std::collections::HashMap::<&str, String>::new(); + let cmdline = std::fs::read_to_string("/proc/cmdline") + .context(format_args!("could not read kernel cmdline"))?; + + for word in cmdline.split_whitespace() { + if let Some((lhs, rhs)) = word.split_once('=') { + if let Some(("nit", name)) = lhs.split_once('.') { + values.insert(name, rhs.to_string()); + } + } + } + + let mode = if let Some(mode) = values.remove("mode") { + match &*mode { + "spawn" => Mode::Spawn, + "exec" => Mode::Exec, + m => panic!("Bad mode: {m}"), + } + } else { + Mode::Exec + }; + + let platform = platform::get_current_platform(values.remove("platform").as_deref())?; + + let target = values.remove("target").unwrap_or(String::from("/usr/bin/hello")); + + Ok(Config { platform, mode, target }) +} diff --git a/crates/nit/src/main.rs b/crates/nit/src/main.rs new file mode 100644 index 0000000..c282fef --- /dev/null +++ b/crates/nit/src/main.rs @@ -0,0 +1,86 @@ +mod config; +mod platform; +mod result; +mod system; + +use result::Result; +use system::dmesg; + +fn main() { + if let Err(e) = init() { + dmesg(format!("Error: {e}")); + let mut opt = std::error::Error::source(&e); + while let Some(current_source) = opt { + dmesg(format!("Caused by: {current_source}")); + opt = current_source.source(); + } + } +} + +extern "C" fn handle_sigchld(_sig: i32) { + // wait on all dead processes + while system::syscall::waitpid(-1, libc::WNOHANG).is_ok() { + // pass + } +} + +fn init() -> Result<()> { + if let Err(errors) = system::mount_default_targets() { + for error in errors { + dmesg(format!("Error while mounting: {error}")); + let mut opt = std::error::Error::source(&error); + while let Some(current_source) = opt { + dmesg(format!("Caused by: {current_source}")); + opt = current_source.source(); + } + } + } + init_console()?; + + let config = config::get_config()?; + + if let Some(platform) = config.platform.as_deref() { + platform::init(platform)?; + } else if let Some(platform) = platform::get_current_platform(None)?.as_deref() { + platform::init(platform)?; + } + + system::dmesg("EnclaveOS Booted"); + + let command = &config.target; + match config.mode { + config::Mode::Spawn => { + panic!("Spawn mode has not been tested.") + /* + // set up a process reaper. any time a child process dies, a SIGCHLD will be fired, and + // the signal handler will reap the processes + eprintln!("installing signal handler"); + system::syscall::signal(libc::SIGCHLD, handle_sigchld)?; + loop { + eprintln!("spawning {command}"); + if let Err(e) = std::process::Command::new(command).spawn() { + eprintln!("Encountered error running {command}: {e}"); + } + } + */ + } + config::Mode::Exec => { + dmesg(format!("pivoting to {command}")); + system::syscall::execv(command, &[])?; + } + } + + Ok(()) +} + +fn init_console() -> Result<(), result::CtxError> { + for (filename, mode, fd) in [ + ("/dev/console", "r", 0), + ("/dev/console", "w", 1), + ("/dev/console", "w", 2), + ] { + system::syscall::freopen(filename, mode, &fd)?; + } + + Ok(()) +} diff --git a/crates/nit/src/platform/aws.rs b/crates/nit/src/platform/aws.rs new file mode 100644 index 0000000..91366be --- /dev/null +++ b/crates/nit/src/platform/aws.rs @@ -0,0 +1,74 @@ +use crate::{ + result::{Context, Result}, + system::dmesg, +}; + +#[derive(Debug)] +pub struct Aws; + +#[derive(Debug)] +pub struct InvalidHeartbeatResponse(u8); + +impl std::fmt::Display for InvalidHeartbeatResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Invalid heartbeat response: expected 0xB7, got 0x{}", + self.0 + ) + } +} + +impl std::error::Error for InvalidHeartbeatResponse {} + +impl Aws { + fn init_heartbeat() -> Result<()> { + use crate::system::syscall::{ + close, connect, read, sockaddr_vm, socket, write, SocketFamily, SocketType, + }; + + let mut sockaddr: sockaddr_vm = sockaddr_vm { + svm_family: SocketFamily::Vsock as u16, + svm_port: 9000, + svm_cid: 3, + svm_reserved1: 0, + svm_zero: [0; 4], + }; + let mut buf = [0xB7]; + + let fd = socket(SocketFamily::Vsock, SocketType::Stream)?; + unsafe { + connect( + fd, + std::ptr::from_mut(&mut sockaddr).cast(), + std::mem::size_of_val(&sockaddr), + )?; + }; + + dmesg("heartbeat =>"); + write(fd, &buf)?; + read(fd, &mut buf)?; + close(fd)?; + + if buf[0] != 0xB7 { + return Err(InvalidHeartbeatResponse(buf[0])) + .context(format_args!("Bad value from heartbeat")); + } + + dmesg("<= heartbeat"); + + Ok(()) + } +} + +impl super::Platform for Aws { + fn is(&self) -> Result { + std::fs::exists("/dev/nsm").context(format_args!("could not check if /dev/nsm exists")) + } + + fn init(&self) -> Result<()> { + Self::init_heartbeat()?; + // enclaveos_shim::init_platform(); + Ok(()) + } +} diff --git a/crates/nit/src/platform/mod.rs b/crates/nit/src/platform/mod.rs new file mode 100644 index 0000000..383863a --- /dev/null +++ b/crates/nit/src/platform/mod.rs @@ -0,0 +1,78 @@ +use crate::{ + result::Result, + system::{self, Mount}, +}; + +pub trait Platform: std::fmt::Debug { + /// Whether the current Platform is the `Self` platform. + /// + /// This method should check for the existence of hardware devices that irrefutably defines + /// this platform as the given platform. For instance, the existence of `/dev/n` + fn is(&self) -> Result; + + /// Return the names and parameters for any required kernel modules. + fn get_modules(&self) -> Result> { + Ok(vec![]) + } + + /// The configuration for mounting filesystems for the platform. + /// + /// Filesystems such as `/proc` and `/dev` have already been mounted. This method should be + /// used to define additional mounts. + fn get_mounts(&self) -> Result> { + Ok(vec![]) + } + + /// Initialize all necessary requirements for the platform. + fn init(&self) -> Result<()> { + Ok(()) + } +} + +fn init_filesystems(iter: impl IntoIterator) -> Result<()> { + for mount in iter { + mount.mount_self()?; + } + + Ok(()) +} + +fn init_modules(iter: impl IntoIterator) -> Result<()> { + for (module, params) in iter { + system::insmod(&module, ¶ms)?; + } + + Ok(()) +} + +#[cfg(feature = "aws")] +pub mod aws; + +pub fn get_current_platform(name: Option<&str>) -> Result>> { + #[allow(clippy::collapsible_match)] + if let Some(name) = name { + match name { + #[cfg(feature = "aws")] + "aws" => return Ok(Some(Box::new(aws::Aws))), + _ => {} + } + } + + #[cfg(feature = "aws")] + if aws::Aws.is()? { + return Ok(Some(Box::new(aws::Aws))); + } + + Ok(None) +} + +pub fn init(platform: &dyn Platform) -> Result<()> { + // TODO: Error handling strategy: If a platform is compiled in and loaded, if platform + // specific error handling doesn't work, fall back to generic? + + platform.get_mounts().and_then(init_filesystems)?; + platform.get_modules().and_then(init_modules)?; + platform.init()?; + + Ok(()) +} diff --git a/crates/nit/src/result.rs b/crates/nit/src/result.rs new file mode 100644 index 0000000..9f8420b --- /dev/null +++ b/crates/nit/src/result.rs @@ -0,0 +1,52 @@ +//! Error handling niceties. + +// Because who needs anyhow anyways. + +/// Provide context for an existing error. +pub trait Context { + fn context(self, fmt: std::fmt::Arguments<'_>) -> Result; +} + +impl Context for std::result::Result +where + E: std::error::Error + 'static, +{ + fn context(self, fmt: std::fmt::Arguments<'_>) -> Result { + match self { + Ok(o) => Ok(o), + Err(e) => Err(CtxError { + context: fmt.to_string(), + inner: Box::new(e), + }), + } + } +} + +/// An error with context attached. +/// +/// The context is provided as format arguments, but will be constructed into a string if the value +/// is an error. +#[derive(Debug)] +pub struct CtxError { + context: String, + inner: Box, +} + +impl std::fmt::Display for CtxError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.context.as_str()) + } +} + +impl std::error::Error for CtxError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(self.inner.as_ref()) + } +} + +pub type Result = std::result::Result; + +pub fn ctx_os_error(args: std::fmt::Arguments<'_>) -> Result<(), CtxError> { + Err(std::io::Error::last_os_error()).context(args) +} + diff --git a/crates/nit/src/system/mod.rs b/crates/nit/src/system/mod.rs new file mode 100644 index 0000000..d85ca1c --- /dev/null +++ b/crates/nit/src/system/mod.rs @@ -0,0 +1,156 @@ +use crate::result::{Context, CtxError, Result}; +use libc::{MS_NODEV, MS_NOEXEC, MS_NOSUID}; +use std::path::{Path, PathBuf}; + +pub mod syscall; + +pub fn dmesg(value: impl std::fmt::Display) { + let timespec = syscall::clock_gettime(libc::CLOCK_BOOTTIME).unwrap(); + for line in value.to_string().lines() { + eprintln!( + "[{: >5}.{}] {line}", + timespec.tv_sec, + timespec.tv_nsec / 1000 + ); + } +} + +pub enum MountType { + DevTmpFs, + DevPts, + Shm, + Proc, + TmpFs, + SysFs, +} + +impl From for PathBuf { + fn from(value: MountType) -> Self { + Self::from(value.as_str()) + } +} + +impl MountType { + fn as_str(&self) -> &'static str { + match self { + MountType::DevTmpFs => "devtmpfs", + MountType::DevPts => "devpts", + MountType::Shm => "shm", + MountType::Proc => "proc", + MountType::TmpFs => "tmpfs", + MountType::SysFs => "sysfs", + } + } +} + +pub struct Mount { + source: PathBuf, + target: PathBuf, + fstype: MountType, + flags: libc::c_ulong, + data: Option, +} + +impl Mount { + pub fn new<'a>( + source: impl Into, + target: impl Into, + fstype: MountType, + flags: libc::c_ulong, + data: impl Into>, + ) -> Self { + let source = source.into(); + let target = target.into(); + let data = data.into().map(Into::into); + + Self { + source, + target, + fstype, + flags, + data, + } + } + + pub fn mount_self(&self) -> Result<()> { + if !std::fs::exists(&self.target).context(format_args!( + "could not check if path exists: {path}", + path = self.target.display() + ))? { + dmesg(format!( + "Making directory: {target}", + target = self.target.display() + )); + syscall::mkdir(&self.target)?; + } + + dmesg(format!( + "Mounting {source} to {target}", + source = self.source.display(), + target = self.target.display() + )); + syscall::mount( + &self.source, + &self.target, + self.fstype.as_str(), + self.flags, + self.data.as_deref(), + )?; + Ok(()) + } +} + +#[allow(clippy::similar_names)] +pub fn mount_default_targets() -> Result<(), Vec> { + let no_dse = MS_NODEV | MS_NOSUID | MS_NOEXEC; + let no_se = MS_NOSUID | MS_NOEXEC; + let m0755 = Some("mode=0755"); + let hidepid = Some("hidepid=2"); + + // why, oh why, rustfmt + let mounts = [ + Mount::new( + MountType::DevTmpFs, + "/dev", + MountType::DevTmpFs, + no_se, + m0755, + ), + Mount::new(MountType::Proc, "/proc", MountType::Proc, no_dse, hidepid), + Mount::new(MountType::TmpFs, "/tmp", MountType::TmpFs, no_dse, None), + Mount::new(MountType::SysFs, "/sys", MountType::SysFs, no_dse, None), + Mount::new( + "cgroup_root", + "/sys/fs/cgroup", + MountType::TmpFs, + no_dse, + m0755, + ), + ]; + + let mut errors = vec![]; + + for mount in mounts { + if let Err(e) = mount.mount_self() { + errors.push(e); + } + } + + if !errors.is_empty() { + return Err(errors); + } + + Ok(()) +} + +pub fn insmod(path: impl AsRef, params: impl AsRef) -> Result<()> { + let path = path.as_ref(); + let params = params.as_ref(); + let file = std::fs::File::open(path).context(format_args!( + "could not open: {path}", + path = path.display() + ))?; + + syscall::finit_module(file, params)?; + Ok(()) +} diff --git a/crates/nit/src/system/syscall.rs b/crates/nit/src/system/syscall.rs new file mode 100644 index 0000000..74cc0d9 --- /dev/null +++ b/crates/nit/src/system/syscall.rs @@ -0,0 +1,257 @@ +//! Wrappers for libc system calls and other necessary functions. +//! +//! These functions, for one reason or another, require using the `libc` crate and can't +//! be implemented directly in the standard library. + +// Some of these methods might not be called, because support for the features that +// invoke the functions were not enabled. +#![allow(dead_code)] + +use crate::result::{ctx_os_error, Context, Result}; +use libc::{self, c_int, c_ulong, c_void}; +use std::{ + ffi::CString, + os::fd::{AsRawFd, RawFd}, + path::Path, +}; + +pub fn clock_gettime(clock_id: libc::clockid_t) -> Result { + let mut t = libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }; + + match unsafe { libc::clock_gettime(clock_id, std::ptr::from_mut(&mut t)) } { + 0 => Ok(t), + -1 => { + // at this point, Err(ctx_os_error()) is starting to seem appealing + ctx_os_error(format_args!("error calling clock_gettime({clock_id}, ...)")).map(|()| { + libc::timespec { + tv_sec: 0, + tv_nsec: 0, + } + }) + } + n => unreachable!("clock_gettime() syscall returned bad value: {n}"), + } +} + +pub fn mount( + src: impl AsRef, + target: impl AsRef, + fstype: impl AsRef, + flags: c_ulong, + data: Option<&str>, +) -> Result<()> { + let src = src.as_ref(); + let target = target.as_ref(); + let fstype = fstype.as_ref(); + + // it upsets me to no end that these two lines format differently. + #[rustfmt::ignore] + let src_cstr = CString::new(src.as_os_str().as_encoded_bytes()) + .context(format_args!("bad src: {src}", src = src.display()))?; + #[rustfmt::ignore] + let target_cstr = CString::new(target.as_os_str().as_encoded_bytes()).context(format_args!( + "bad target: {target}", + target = target.display() + ))?; + let fstype_cs = CString::new(fstype).context(format_args!("bad fstype: {fstype}"))?; + + let data_cs = data + .map(CString::new) + .transpose() + .context(format_args!("bad data: {data:?}"))?; + + let data_ptr = if let Some(s) = data_cs { + s.as_ptr() + } else { + std::ptr::null() + }; + + // SAFETY: mount() can accept nullptr for data. no other pointers are constructed manually. + match unsafe { + libc::mount( + src_cstr.as_ptr(), + target_cstr.as_ptr(), + fstype_cs.as_ptr(), + flags, + data_ptr.cast::(), + ) + } { + 0 => Ok(()), + -1 => ctx_os_error(format_args!( + "error calling mount({src}, {target}, {fstype}, {flags}, {data:?})", + src = src.display(), + target = target.display() + )), + n => unreachable!("mount() syscall returned bad value: {n}"), + } +} + +pub fn mkdir(path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + let path_cs = CString::new(path.as_os_str().as_encoded_bytes()) + .context(format_args!("bad path: {path}", path = path.display()))?; + + match unsafe { libc::mkdir(path_cs.as_ptr(), 0o755) } { + 0 => Ok(()), + -1 => ctx_os_error(format_args!( + "error calling mkdir({path}, 0o755)", + path = path.display() + )), + n => unreachable!("mount() syscall returned bad value: {n}"), + } +} + +pub fn freopen(path: impl AsRef, mode: impl AsRef, fd: &impl AsRawFd) -> Result<()> { + let path = path.as_ref(); + let mode = mode.as_ref(); + let fd = fd.as_raw_fd(); + + #[rustfmt::ignore] + let filename_cs = CString::new(path.as_os_str().as_encoded_bytes()) + .context(format_args!("bad path: {path}", path = path.display()))?; + let mode_cs = CString::new(mode).context(format_args!("bad mode: {mode}"))?; + + // SAFETY: no pointers are constructed manually. + let file = unsafe { libc::fdopen(fd, mode.as_ptr().cast::()) }; + + if file.is_null() { + return ctx_os_error(format_args!("bad fdopen({fd}, {mode})")); + } + + // SAFETY: no pointers are constructed manually. + let file = unsafe { libc::freopen(filename_cs.as_ptr(), mode_cs.as_ptr(), file) }; + + if file.is_null() { + return ctx_os_error(format_args!( + "bad freopen({path}, {mode}, {file:?} as *FILE)", + path = path.display() + )); + } + + Ok(()) +} + +// NOTE: We want to consume the file, so we do not allow a ref to impl AsRawFd +#[allow(clippy::needless_pass_by_value)] +pub fn finit_module(fd: impl AsRawFd, params: impl AsRef) -> Result<()> { + let fd = fd.as_raw_fd(); + let params = params.as_ref(); + let params_cs = CString::new(params).context(format_args!("bad params: {params}"))?; + + // SAFETY: no pointers are constructed manually, and AsRawFd should be a proper + // file descriptor. + match unsafe { libc::syscall(libc::SYS_finit_module, fd, params_cs.as_ptr(), 0) } { + 0 => Ok(()), + -1 => ctx_os_error(format_args!("error calling finit_module({fd}, {params})")), + n => unreachable!("syscall(SYS_finit_module, ...) returned bad value: {n}"), + } +} + +#[repr(i32)] +#[derive(Debug, Clone, Copy)] +pub enum SocketFamily { + Vsock = libc::AF_VSOCK, +} + +#[repr(i32)] +#[derive(Debug, Clone, Copy)] +pub enum SocketType { + Stream = libc::SOCK_STREAM, +} + +// TODO: allow specifying protocol? +pub fn socket(family: SocketFamily, typ: SocketType) -> Result { + match unsafe { libc::socket(family as i32, typ as i32, 0) } { + -1 => ctx_os_error(format_args!("error calling socket({family:?}, {typ:?})")).map(|()| 0), + fd => Ok(RawFd::from(fd)), + } +} + +pub use libc::sockaddr_vm; + +// This function is unsafe since we have to pass it a C-style union. +pub unsafe fn connect(fd: RawFd, sockaddr: *mut libc::sockaddr, size: usize) -> Result<()> { + let size = u32::try_from(size).context(format_args!( + "connect(..., size = {size}) has size > {}", + u32::MAX + ))?; + + match unsafe { libc::connect(fd, sockaddr, size) } { + 0 => Ok(()), + -1 => ctx_os_error(format_args!("error calling connect({fd}, ...)")), + n => unreachable!("connect({fd}, ...) returned bad value: {n}"), + } +} + +pub fn write(fd: RawFd, bytes: &[u8]) -> Result { + match unsafe { libc::write(fd, bytes.as_ptr().cast(), bytes.len()) } { + ..0 => ctx_os_error(format_args!("error calling write({fd}, ...)")).map(|()| 0), + n @ 0.. => Ok(usize::try_from(n).unwrap()), + } +} + +pub fn read(fd: RawFd, buffer: &mut [u8]) -> Result { + match unsafe { libc::read(fd, buffer.as_mut_ptr().cast(), buffer.len()) } { + ..0 => ctx_os_error(format_args!("error calling read({fd}, ...)")).map(|()| 0), + n @ 0.. => Ok(usize::try_from(n).unwrap()), + } +} + +pub fn close(fd: RawFd) -> Result<()> { + match unsafe { libc::close(fd) } { + 0 => Ok(()), + -1 => ctx_os_error(format_args!("error calling close({fd})")), + n => unreachable!("close({fd}) returned bad value: {n}"), + } +} + +pub fn execv(command: impl AsRef, args: &[&str]) -> Result<()> { + let command = command.as_ref(); + + let command_cstr = CString::new(command.as_os_str().as_encoded_bytes()) + .context(format_args!("bad command: {}", command.display()))?; + // NOTE: command should be the first string, according to execve() documentation. + let mut args_cstr = vec![command_cstr.clone()]; + for arg in args { + let cur_arg_cstr = CString::new(arg.as_bytes()).context(format_args!("bad arg: {arg}"))?; + args_cstr.push(cur_arg_cstr); + } + + // NOTE: The last arg must be a null pointer, but we can't construct a CString from nullptr, so + // we construct an array of pointers and use that + let args_ptrs: Vec<_> = args_cstr + .iter() + .map(|s| s.as_ptr()) + .chain(std::iter::once(std::ptr::null())) + .collect(); + + if unsafe { libc::execv(command_cstr.as_ptr().cast(), args_ptrs.as_ptr().cast()) } == -1 { + return ctx_os_error(format_args!( + "error calling exec({command}, {args:?})", + command = command.display() + )); + } + + Ok(()) +} + +pub type SignalHandler = extern "C" fn(c_int); + +pub fn signal(signal: i32, handler: SignalHandler) -> Result<()> { + #[allow(clippy::single_match)] + match unsafe { libc::signal(signal, handler as libc::sighandler_t) } { + libc::SIG_ERR => ctx_os_error(format_args!("invalid handler for {signal:?}")), + _ => Ok(()), + } +} + +pub fn waitpid(pid: libc::pid_t, flags: i32) -> Result { + match unsafe { libc::waitpid(pid, std::ptr::null_mut(), flags) } { + pid @ 0.. => Ok(pid), + -1 => ctx_os_error(format_args!("error calling waitpid({pid}, {flags})")).map(|()| 0), + n => unreachable!("waitpid({pid}, {flags}) returned bad value: {n}"), + } +} diff --git a/src/aws/Cargo.lock b/src/aws/Cargo.lock deleted file mode 100644 index b0eb785..0000000 --- a/src/aws/Cargo.lock +++ /dev/null @@ -1,24 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aws" -version = "0.1.0" -dependencies = [ - "libc", - "system", -] - -[[package]] -name = "libc" -version = "0.2.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" - -[[package]] -name = "system" -version = "0.1.0" -dependencies = [ - "libc", -] diff --git a/src/aws/Cargo.toml b/src/aws/Cargo.toml deleted file mode 100644 index 1a52684..0000000 --- a/src/aws/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "aws" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -libc = "0.2.134" -system = { path = "../system"} diff --git a/src/aws/src/lib.rs b/src/aws/src/lib.rs deleted file mode 100644 index 76b364d..0000000 --- a/src/aws/src/lib.rs +++ /dev/null @@ -1,34 +0,0 @@ -use system::{dmesg, SystemError}; - -// Signal to Nitro hypervisor that booting was successful -fn nitro_heartbeat() { - use system::socket_connect; - use libc::{write, read, close, AF_VSOCK}; - let mut buf: [u8; 1] = [0; 1]; - buf[0] = 0xB7; // AWS Nitro heartbeat value - let fd = match socket_connect(AF_VSOCK, 9000, 3) { - Ok(f)=> f, - Err(e)=> { - eprintln!("{}", e); - return - }, - }; - unsafe { - write(fd, buf.as_ptr() as _, 1); - read(fd, buf.as_ptr() as _, 1); - close(fd); - } - dmesg(format!("Sent NSM heartbeat")); -} - -// Initialize nitro device -pub fn init_platform(){ - use system::insmod; - // TODO: error handling - nitro_heartbeat(); - - match insmod("/nsm.ko") { - Ok(())=> dmesg(format!("Loaded nsm.ko")), - Err(e)=> eprintln!("{}", e) - }; -} diff --git a/src/init/Cargo.lock b/src/init/Cargo.lock deleted file mode 100644 index 6afc809..0000000 --- a/src/init/Cargo.lock +++ /dev/null @@ -1,33 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aws" -version = "0.1.0" -dependencies = [ - "libc", - "system", -] - -[[package]] -name = "init" -version = "0.1.0" -dependencies = [ - "aws", - "libc", - "system", -] - -[[package]] -name = "libc" -version = "0.2.134" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" - -[[package]] -name = "system" -version = "0.1.0" -dependencies = [ - "libc", -] diff --git a/src/init/Cargo.toml b/src/init/Cargo.toml deleted file mode 100644 index 25b7b47..0000000 --- a/src/init/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "init" -version = "0.1.0" -edition = "2021" - -[dependencies] -libc = "0.2.134" -aws = { path = "../aws"} -system = { path = "../system"} - - -[[bin]] -name = "init" -path = "init.rs" diff --git a/src/init/init.rs b/src/init/init.rs deleted file mode 100644 index 0d8e183..0000000 --- a/src/init/init.rs +++ /dev/null @@ -1,54 +0,0 @@ -use system::{reboot, freopen, mount, dmesg}; - -use aws::{init_platform}; - -// Mount common filesystems with conservative permissions -fn init_rootfs() { - use libc::{MS_NOSUID, MS_NOEXEC, MS_NODEV }; - let no_dse = MS_NODEV | MS_NOSUID | MS_NOEXEC; - let no_se = MS_NOSUID | MS_NOEXEC; - let args = [ - ("devtmpfs", "/dev", "devtmpfs", no_se, "mode=0755"), - ("devtmpfs", "/dev", "devtmpfs", no_se, "mode=0755"), - ("devpts", "/dev/pts", "devpts", no_se, ""), - ("shm", "/dev/shm", "tmpfs", no_dse, "mode=0755"), - ("proc", "/proc", "proc", no_dse, "hidepid=2"), - ("tmpfs", "/run", "tmpfs", no_dse, "mode=0755"), - ("tmpfs", "/tmp", "tmpfs", no_dse, ""), - ("sysfs", "/sys", "sysfs", no_dse, ""), - ("cgroup_root", "/sys/fs/cgroup", "tmpfs", no_dse, "mode=0755"), - ]; - for (src, target, fstype, flags, data) in args { - match mount(src, target, fstype, flags, data) { - Ok(())=> dmesg(format!("Mounted {}", target)), - Err(e)=> eprintln!("{}", e), - } - } -} - -// Initialize console with stdin/stdout/stderr -fn init_console() { - let args = [ - ("/dev/console", "r", 0), - ("/dev/console", "w", 1), - ("/dev/console", "w", 2), - ]; - for (filename, mode, file) in args { - match freopen(filename, mode, file) { - Ok(())=> {}, - Err(e)=> eprintln!("{}", e), - } - } -} - -fn boot(){ - init_rootfs(); - init_console(); - init_platform(); -} - -fn main() { - boot(); - dmesg("System booted".to_string()); - reboot(); -} diff --git a/src/system/Cargo.toml b/src/system/Cargo.toml deleted file mode 100644 index c4fbcc6..0000000 --- a/src/system/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "system" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -libc = "0.2.134" diff --git a/src/system/src/lib.rs b/src/system/src/lib.rs deleted file mode 100644 index 4e8d93c..0000000 --- a/src/system/src/lib.rs +++ /dev/null @@ -1,129 +0,0 @@ -use libc::{ c_ulong, c_int, c_void }; -use std::{ - mem::{zeroed, size_of}, - ffi::CString, - fs::File, - os::unix::io::AsRawFd, - fmt, -}; - -pub struct SystemError { - pub message: String, -} -impl fmt::Display for SystemError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} {}", boot_time(), self.message) - } -} - -// Log dmesg formatted log to console -pub fn dmesg(message: String){ - println!("{} {}", boot_time(), message); -} - -// Dmesg formatted seconds since boot -pub fn boot_time() -> String { - use libc::{clock_gettime, timespec, CLOCK_BOOTTIME}; - let mut t = timespec { tv_sec: 0, tv_nsec: 0 }; - unsafe { clock_gettime(CLOCK_BOOTTIME, &mut t as *mut timespec); } - format!("[ {: >4}.{}]", t.tv_sec, t.tv_nsec / 1000).to_string() -} - -// Unconditionally reboot the system now -pub fn reboot(){ - use libc::{reboot, RB_AUTOBOOT}; - unsafe { - reboot(RB_AUTOBOOT); - } -} - -// libc::mount casting/error wrapper -pub fn mount( - src: &str, - target: &str, - fstype: &str, - flags: c_ulong, - data: &str, -) -> Result<(), SystemError> { - use libc::mount; - let src_cs = CString::new(src).unwrap(); - let fstype_cs = CString::new(fstype).unwrap(); - let data_cs = CString::new(data).unwrap(); - let target_cs = CString::new(target).unwrap(); - if unsafe { - mount( - src_cs.as_ptr(), - target_cs.as_ptr(), - fstype_cs.as_ptr(), - flags, - data_cs.as_ptr() as *const c_void - ) - } != 0 { - Err(SystemError { message: format!("Failed to mount: {}", target) }) - } else { - Ok(()) - } -} - -// libc::freopen casting/error wrapper -pub fn freopen( - filename: &str, - mode: &str, - file: c_int, -) -> Result<(), SystemError> { - use libc::{freopen, fdopen}; - let filename_cs = CString::new(filename).unwrap(); - let mode_cs = CString::new(mode).unwrap(); - if unsafe { - freopen( - filename_cs.as_ptr(), - mode_cs.as_ptr(), - fdopen(file, mode_cs.as_ptr() as *const i8) - ) - }.is_null() { - Err(SystemError { message: format!("Failed to freopen: {}", filename) }) - } else { - Ok(()) - } -} - -// Insert kernel module into memory -pub fn insmod(path: &str) -> Result<(), SystemError> { - use libc::{syscall, SYS_finit_module}; - let file = File::open(path).unwrap(); - let fd = file.as_raw_fd(); - if unsafe { syscall(SYS_finit_module, fd, &[0u8; 1], 0) } < 0 { - Err(SystemError { - message: format!("Failed to insert kernel module: {}", path) - }) - } else { - Ok(()) - } -} - -// Instantiate a socket -pub fn socket_connect( - family: c_int, - port: u32, - cid: u32, -) -> Result { - use libc::{connect, socket, sockaddr, sockaddr_vm, SOCK_STREAM}; - let fd = unsafe { socket(family, SOCK_STREAM, 0) }; - if unsafe { - let mut sa: sockaddr_vm = zeroed(); - sa.svm_family = family as _; - sa.svm_port = port; - sa.svm_cid = cid; - connect( - fd, - &sa as *const _ as *mut sockaddr, - size_of::() as _, - ) - } < 0 { - Err(SystemError { - message: format!("Failed to connect to socket: {}", family) - }) - } else { - Ok(fd) - } -}