From e15ac71405af75071117d1410e6b410035c5dd40 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Mon, 22 Jan 2024 05:08:14 -0800 Subject: [PATCH] hypha: implement the ability to execute container images --- hypha/Cargo.toml | 4 + hypha/src/container/init.rs | 153 ++++++++++++++++++++++++++++++++---- hypha/src/error.rs | 13 +++ 3 files changed, 155 insertions(+), 15 deletions(-) diff --git a/hypha/Cargo.toml b/hypha/Cargo.toml index 9ad3d6f..d0c479f 100644 --- a/hypha/Cargo.toml +++ b/hypha/Cargo.toml @@ -48,6 +48,10 @@ features = ["v4"] default-features = false version = "2.1.1" +[dependencies.nix] +features = ["process"] +version = "0.27.1" + [lib] path = "src/lib.rs" diff --git a/hypha/src/container/init.rs b/hypha/src/container/init.rs index d24add5..ea70b55 100644 --- a/hypha/src/container/init.rs +++ b/hypha/src/container/init.rs @@ -1,8 +1,18 @@ use crate::error::Result; -use oci_spec::image::ImageConfiguration; +use crate::hypha_err; +use log::trace; +use nix::libc::dup2; +use nix::unistd::execve; +use oci_spec::image::{Config, ImageConfiguration}; +use std::ffi::CString; use std::fs; +use std::fs::{File, OpenOptions}; +use std::os::fd::AsRawFd; +use std::os::linux::fs::MetadataExt; +use std::os::unix::fs::chroot; use std::path::Path; use sys_mount::{FilesystemType, Mount, MountFlags}; +use walkdir::WalkDir; const IMAGE_BLOCK_DEVICE_PATH: &str = "/dev/xvda"; const CONFIG_BLOCK_DEVICE_PATH: &str = "/dev/xvdb"; @@ -15,8 +25,7 @@ const OVERLAY_IMAGE_BIND_PATH: &str = "/overlay/image"; const OVERLAY_WORK_PATH: &str = "/overlay/work"; const OVERLAY_UPPER_PATH: &str = "/overlay/upper"; -const PIVOT_PATH: &str = "/pivot"; - +const NEW_ROOT_PATH: &str = "/newroot"; const IMAGE_CONFIG_JSON_PATH: &str = "/config/image/config.json"; pub struct ContainerInit {} @@ -33,19 +42,27 @@ impl ContainerInit { } pub fn init(&mut self) -> Result<()> { - self.mount_early()?; - let config = self.parse_image_config()?; - self.mount_late()?; + let console = OpenOptions::new() + .read(true) + .write(true) + .open("/dev/console")?; + self.mount_squashfs_images()?; + let config = self.parse_image_config()?; + self.mount_new_root()?; + self.nuke_initrd()?; + self.bind_new_root()?; + self.map_console(console)?; if let Some(cfg) = config.config() { - if let Some(cmd) = cfg.cmd() { - println!("image command: {:?}", cmd); - } + self.run(cfg)?; + } else { + return hypha_err!("unable to determine what to execute, image config doesn't tell us"); } Ok(()) } - fn mount_early(&mut self) -> Result<()> { + fn mount_squashfs_images(&mut self) -> Result<()> { + trace!("mounting squashfs images"); let image_mount_path = Path::new(IMAGE_MOUNT_PATH); let config_mount_path = Path::new(CONFIG_MOUNT_PATH); self.mount_squashfs(Path::new(IMAGE_BLOCK_DEVICE_PATH), image_mount_path)?; @@ -54,6 +71,7 @@ impl ContainerInit { } fn mount_squashfs(&mut self, from: &Path, to: &Path) -> Result<()> { + trace!("mounting squashfs image {:?} to {:?}", from, to); if !to.is_dir() { fs::create_dir(to)?; } @@ -64,10 +82,13 @@ impl ContainerInit { Ok(()) } - fn mount_late(&mut self) -> Result<()> { + fn mount_new_root(&mut self) -> Result<()> { + trace!("mounting new root"); self.mount_overlay_tmpfs()?; self.bind_image_to_overlay_tmpfs()?; - self.mount_overlay_to_pivot()?; + self.mount_overlay_to_new_root()?; + std::env::set_current_dir(NEW_ROOT_PATH)?; + trace!("mounted new root"); Ok(()) } @@ -90,8 +111,8 @@ impl ContainerInit { Ok(()) } - fn mount_overlay_to_pivot(&mut self) -> Result<()> { - fs::create_dir(PIVOT_PATH)?; + fn mount_overlay_to_new_root(&mut self) -> Result<()> { + fs::create_dir(NEW_ROOT_PATH)?; Mount::builder() .fstype(FilesystemType::Manual("overlay")) .flags(MountFlags::NOATIME) @@ -99,13 +120,115 @@ impl ContainerInit { "lowerdir={},upperdir={},workdir={}", OVERLAY_IMAGE_BIND_PATH, OVERLAY_UPPER_PATH, OVERLAY_WORK_PATH )) - .mount(format!("overlayfs:{}", OVERLAY_MOUNT_PATH), PIVOT_PATH)?; + .mount(format!("overlayfs:{}", OVERLAY_MOUNT_PATH), NEW_ROOT_PATH)?; Ok(()) } fn parse_image_config(&mut self) -> Result { + trace!("parsing image config"); let image_config_path = Path::new(IMAGE_CONFIG_JSON_PATH); let config = ImageConfiguration::from_file(image_config_path)?; Ok(config) } + + fn nuke_initrd(&mut self) -> Result<()> { + trace!("nuking initrd"); + let initrd_dev = fs::metadata("/")?.st_dev(); + for item in WalkDir::new("/") + .same_file_system(true) + .follow_links(false) + .contents_first(true) + { + if item.is_err() { + continue; + } + + let item = item?; + let metadata = match item.metadata() { + Ok(value) => value, + Err(_) => continue, + }; + + if metadata.st_dev() != initrd_dev { + continue; + } + + if metadata.is_symlink() || metadata.is_file() { + let _ = fs::remove_file(item.path()); + trace!("deleting file {:?}", item.path()); + } else if metadata.is_dir() { + let _ = fs::remove_dir(item.path()); + trace!("deleting directory {:?}", item.path()); + } + } + trace!("nuked initrd"); + Ok(()) + } + + fn bind_new_root(&mut self) -> Result<()> { + trace!("binding new root"); + Mount::builder() + .fstype(FilesystemType::Manual("none")) + .flags(MountFlags::BIND) + .mount(".", "/")?; + trace!("chrooting into new root"); + chroot(".")?; + trace!("setting root as current directory"); + std::env::set_current_dir("/")?; + Ok(()) + } + + fn map_console(&mut self, console: File) -> Result<()> { + trace!("map console"); + unsafe { + dup2(console.as_raw_fd(), 0); + dup2(console.as_raw_fd(), 1); + dup2(console.as_raw_fd(), 2); + } + drop(console); + Ok(()) + } + + fn run(&mut self, config: &Config) -> Result<()> { + let mut cmd = match config.cmd() { + None => vec![], + Some(value) => value.clone(), + }; + if cmd.is_empty() { + cmd.push("/bin/sh".to_string()); + } + trace!("running container command: {}", cmd.join(" ")); + let path = cmd.remove(0); + let mut env = match config.env() { + None => vec![], + Some(value) => value.clone(), + }; + env.push("HYPHA_CONTAINER=1".to_string()); + let path_cstr = CString::new(path)?; + let mut cmd_cstr = ContainerInit::strings_as_cstrings(cmd)?; + cmd_cstr.push(CString::new("")?); + let mut env_cstr = ContainerInit::strings_as_cstrings(env)?; + env_cstr.push(CString::new("")?); + let mut working_dir = config + .working_dir() + .as_ref() + .map(|x| x.to_string()) + .unwrap_or("/".to_string()); + + if working_dir.is_empty() { + working_dir = "/".to_string(); + } + + std::env::set_current_dir(&working_dir)?; + execve(&path_cstr, &cmd_cstr, &env_cstr)?; + Ok(()) + } + + fn strings_as_cstrings(values: Vec) -> Result> { + let mut results: Vec = vec![]; + for value in values { + results.push(CString::new(value.as_bytes().to_vec())?); + } + Ok(results) + } } diff --git a/hypha/src/error.rs b/hypha/src/error.rs index ed8ac5c..4a8ead1 100644 --- a/hypha/src/error.rs +++ b/hypha/src/error.rs @@ -2,6 +2,7 @@ use backhand::BackhandError; use cli_tables::TableError; use oci_spec::OciSpecError; use std::error::Error; +use std::ffi::NulError; use std::fmt::{Display, Formatter}; use std::num::ParseIntError; use std::path::StripPrefixError; @@ -127,3 +128,15 @@ impl From for HyphaError { HyphaError::new(value.to_string().as_str()) } } + +impl From for HyphaError { + fn from(value: NulError) -> Self { + HyphaError::new(value.to_string().as_str()) + } +} + +impl From for HyphaError { + fn from(value: nix::Error) -> Self { + HyphaError::new(value.to_string().as_str()) + } +}