From c9e9049a3a21579e594859e76e7c3f6a6de514f7 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 29 Oct 2022 21:36:27 +0200 Subject: [PATCH] Add mounting of system directories when chrooting --- Cargo.lock | 37 ++++ Cargo.toml | 1 + configs/crystal/install-base/up.nu | 6 + src/error.rs | 12 ++ src/task/chrooting/mapping.rs | 176 ++++++++++++++++++++ src/task/{chrooting.rs => chrooting/mod.rs} | 32 ++-- src/task/chrooting/setup.rs | 31 ++++ src/task/chrooting/teardown.rs | 0 src/task/task_executor.rs | 23 ++- src/utils.rs | 11 +- 10 files changed, 308 insertions(+), 21 deletions(-) create mode 100644 src/task/chrooting/mapping.rs rename src/task/{chrooting.rs => chrooting/mod.rs} (66%) create mode 100644 src/task/chrooting/setup.rs create mode 100644 src/task/chrooting/teardown.rs diff --git a/Cargo.lock b/Cargo.lock index 0c3299f..edbc521 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1564,6 +1564,17 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "loopdev" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bfa0855b04611e38acaff718542e9e809cddfc16535d39f9d9c694ab19f7388" +dependencies = [ + "bindgen", + "errno", + "libc", +] + [[package]] name = "lscolors" version = "0.12.0" @@ -3168,6 +3179,17 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "smart-default" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "smawk" version = "0.3.1" @@ -3272,6 +3294,20 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "sys-mount" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1af10c09a6d1f65753e52772a4621e00da8b1d772d0f24595b60ccd36d6b51" +dependencies = [ + "bitflags", + "libc", + "loopdev", + "smart-default", + "thiserror", + "tracing", +] + [[package]] name = "sysinfo" version = "0.26.4" @@ -3522,6 +3558,7 @@ dependencies = [ "rusty-value", "serde", "serde_json", + "sys-mount", "thiserror", "tokio", "toml", diff --git a/Cargo.toml b/Cargo.toml index 63b284a..796f27a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ paste = "1.0.9" rusty-value = { version = "0.6.0", features = ["derive", "json"] } serde = { version = "1.0.145", features = ["derive"] } serde_json = "1.0.86" +sys-mount = "1.5.1" thiserror = "1.0.37" tokio = { version = "1.21.2", features = ["rt", "io-std", "io-util", "process", "time", "macros", "tracing", "fs"] } toml = "0.5.9" diff --git a/configs/crystal/install-base/up.nu b/configs/crystal/install-base/up.nu index 3fbee4f..ad03156 100644 --- a/configs/crystal/install-base/up.nu +++ b/configs/crystal/install-base/up.nu @@ -1,4 +1,10 @@ +let REQUIRES_CHROOT = true; # Applies all system changes of `install-base` def main [cfg] { echo "Executing up task `install-base` with config" $cfg + ls /etc + cat /etc/mtab + pacman -S --noconfirm neofetch + neofetch + exit 1 } diff --git a/src/error.rs b/src/error.rs index bbdff70..6246400 100644 --- a/src/error.rs +++ b/src/error.rs @@ -90,9 +90,21 @@ pub enum ChrootError { #[error("Failed to unshare FS resources with parent: {0}")] Unshare(io::Error), + #[error("Failed to create chroot dir: {0}")] + CreateChroot(io::Error), + #[error("Failed to chroot: {0}")] Chroot(io::Error), + #[error("Failed to mount directory {0} in chroot: {1}")] + Mount(PathBuf, io::Error), + + #[error("Failed to create symlink {0} in chroot: {1}")] + Link(PathBuf, io::Error), + + #[error("Failed to remove symlink in chroot: {0}")] + Unlink(io::Error), + #[error("Failed to change process working directory: {0}")] ChDir(io::Error), } diff --git a/src/task/chrooting/mapping.rs b/src/task/chrooting/mapping.rs new file mode 100644 index 0000000..1f71ebf --- /dev/null +++ b/src/task/chrooting/mapping.rs @@ -0,0 +1,176 @@ +use std::path::{Path, PathBuf}; + +use sys_mount::{FilesystemType, Mount, MountBuilder, MountFlags, UnmountDrop, UnmountFlags}; +use tokio::fs; + +use crate::error::ChrootError; + +pub enum Mapping { + Device(PathBuf), + Dir(PathBuf, DirOpts), + Special(PathBuf, PathBuf, DirOpts), +} + +#[derive(Default)] +pub struct DirOpts { + fs_type: Option, + mount_flags: Option, + required: bool, + unmount_flags: Option, +} + +impl DirOpts { + pub fn fs_type>(mut self, fs_type: S) -> Self { + self.fs_type = Some(fs_type.into()); + + self + } + + pub fn flags(mut self, flags: MountFlags) -> Self { + self.mount_flags = Some(flags); + + self + } + + pub fn optional(mut self) -> Self { + self.required = false; + + self + } + + pub fn unmount_flags(mut self, flags: UnmountFlags) -> Self { + self.unmount_flags = Some(flags); + + self + } +} + +pub enum MappingHandle { + Mount(UnmountDrop), +} + +impl Mapping { + fn dev>(path: P) -> Self { + Self::Device(path.into()) + } + + fn dir>(path: P, opt: DirOpts) -> Self { + Self::Dir(path.into(), opt) + } + + fn special, P2: Into>(src: P1, dst: P2, opt: DirOpts) -> Self { + Self::Special(src.into(), dst.into(), opt) + } + + pub async fn create_mapping(&self, root_path: &Path) -> Result { + match &self { + Mapping::Device(d) => self.map_dev(d, &root_path.join(d)).await, + Mapping::Dir(d, opt) => self.map_dir(d, &root_path.join(d), opt).await, + Mapping::Special(src, dst, opt) => self.map_dir(src, &root_path.join(dst), opt).await, + } + } + + async fn map_dev(&self, src: &Path, dst: &Path) -> Result { + if !dst.exists() { + if let Some(parent) = dst.parent() { + fs::create_dir_all(&parent) + .await + .map_err(|e| ChrootError::Mount(dst.to_owned(), e))?; + } + fs::write(&dst, "") + .await + .map_err(|e| ChrootError::Mount(dst.to_owned(), e))?; + } + let mount = MountBuilder::default() + .flags(MountFlags::BIND) + .mount_autodrop(src, dst, UnmountFlags::empty()) + .map_err(|e| ChrootError::Mount(dst.to_owned(), e))?; + + Ok(MappingHandle::Mount(mount)) + } + + async fn map_dir( + &self, + src: &Path, + dst: &Path, + opt: &DirOpts, + ) -> Result { + if !dst.exists() { + fs::create_dir_all(&dst) + .await + .map_err(|e| ChrootError::Mount(dst.to_owned(), e))?; + } + let fs_type = + FilesystemType::Manual(opt.fs_type.as_ref().map(String::as_str).unwrap_or("")); + let flags = opt.mount_flags.unwrap_or_else(|| MountFlags::empty()); + + let mount = MountBuilder::default() + .fstype(fs_type) + .flags(flags) + .mount_autodrop(src, dst, opt.unmount_flags.unwrap_or(UnmountFlags::empty())) + .map_err(|e| ChrootError::Mount(src.to_owned(), e))?; + + Ok(MappingHandle::Mount(mount)) + } +} + +pub fn default_mappings() -> Vec { + static NOSUID: MountFlags = MountFlags::NOSUID; + static NOEXEC: MountFlags = MountFlags::NOEXEC; + static NODEV: MountFlags = MountFlags::NODEV; + static STRICTATIME: MountFlags = MountFlags::STRICTATIME; + + vec![ + Mapping::dir( + "proc", + DirOpts::default() + .fs_type("proc") + .flags(NOSUID | NOEXEC | NODEV), + ), + Mapping::dir( + "sys", + DirOpts::default() + .fs_type("sysfs") + .flags(NOSUID | NOEXEC | NODEV) + .unmount_flags(UnmountFlags::DETACH), + ), + Mapping::dir( + "sys/firmware/efi/efivars", + DirOpts::default() + .fs_type("efivarfs") + .flags(NOSUID | NOEXEC | NODEV) + .optional(), + ), + Mapping::special( + "udev", + "dev", + DirOpts::default().fs_type("devtmpfs").flags(NOSUID), + ), + Mapping::special( + "devpts", + "dev/pts", + DirOpts::default().fs_type("devpts").flags(NOSUID | NOEXEC), + ), + Mapping::special( + "shm", + "dev/shm", + DirOpts::default().fs_type("tmpfs").flags(NOSUID | NODEV), + ), + Mapping::dev("/dev/full"), + Mapping::dev("/dev/null"), + Mapping::dev("/dev/random"), + Mapping::dev("/dev/tty"), + Mapping::dev("/dev/urandom"), + Mapping::dev("/dev/zero"), + Mapping::dir( + "/run", + DirOpts::default().fs_type("tmpfs").flags(NOSUID | NODEV), + ), + Mapping::dir( + "/tmp", + DirOpts::default() + .fs_type("tmpfs") + .flags(STRICTATIME | NODEV | NOSUID), + ), + ] +} diff --git a/src/task/chrooting.rs b/src/task/chrooting/mod.rs similarity index 66% rename from src/task/chrooting.rs rename to src/task/chrooting/mod.rs index 4555e71..f321efd 100644 --- a/src/task/chrooting.rs +++ b/src/task/chrooting/mod.rs @@ -1,34 +1,34 @@ use std::{ - ffi::{c_int, CString}, + ffi::{c_int, CString, OsString}, io, os::unix::prelude::OsStrExt, path::{Path, PathBuf}, }; use libc::CLONE_FS; + use tokio::task::JoinHandle; +mod mapping; +pub mod setup; +pub mod teardown; use crate::error::ChrootError; -pub struct ChrootedTask { +use self::mapping::MappingHandle; + +pub struct Chroot { root_path: PathBuf, + _mappings: Vec, } -impl ChrootedTask { - /// Creates a new chrooted thread with the given path - pub fn new>(root_path: P) -> Self { - Self { - root_path: root_path.into(), - } - } - +impl Chroot { /// Runs the given future in a new chroot - pub fn run(self, call: F) -> JoinHandle> + pub fn run(&self, call: F) -> JoinHandle> where F: FnOnce() -> T + Send + 'static, T: Send + 'static, { - let root_path = self.root_path; + let root_path = self.root_path.clone(); let handle = std::thread::spawn(move || { unsafe { init_chroot(&root_path)?; @@ -41,12 +41,12 @@ impl ChrootedTask { unsafe fn init_chroot(path: &Path) -> Result<(), ChrootError> { handle_err_code(libc::unshare(CLONE_FS)).map_err(ChrootError::Unshare)?; - let path_str = CString::new(path.as_os_str().as_bytes().to_vec()).unwrap(); + let path_str = c_str(path); handle_err_code(libc::chroot( path_str.as_bytes_with_nul().as_ptr() as *const libc::c_char )) .map_err(ChrootError::Chroot)?; - std::env::set_current_dir(path).map_err(ChrootError::ChDir)?; + std::env::set_current_dir("/").map_err(ChrootError::ChDir)?; std::env::set_var("PWD", "/"); Ok(()) @@ -59,3 +59,7 @@ fn handle_err_code(code: c_int) -> Result<(), io::Error> { Ok(()) } } + +pub(crate) fn c_str>(o: O) -> CString { + CString::new(o.into().as_bytes().to_vec()).unwrap() +} diff --git a/src/task/chrooting/setup.rs b/src/task/chrooting/setup.rs new file mode 100644 index 0000000..02b4435 --- /dev/null +++ b/src/task/chrooting/setup.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +use tokio::fs; + +use crate::error::ChrootError; + +use super::{mapping::default_mappings, Chroot}; + +impl Chroot { + /// Creates a new chroot with the given path + pub async fn create>(root_path: P) -> Result { + let root_path = root_path.into(); + if !root_path.exists() { + fs::create_dir_all(&root_path) + .await + .map_err(ChrootError::CreateChroot)?; + } + let default_mappings = default_mappings(); + let mut handles = Vec::with_capacity(default_mappings.len()); + + for mapping in default_mappings { + let handle = mapping.create_mapping(&root_path).await?; + handles.push(handle); + } + + Ok(Self { + root_path, + _mappings: handles, + }) + } +} diff --git a/src/task/chrooting/teardown.rs b/src/task/chrooting/teardown.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/task/task_executor.rs b/src/task/task_executor.rs index 282e10c..6849d6d 100644 --- a/src/task/task_executor.rs +++ b/src/task/task_executor.rs @@ -1,10 +1,13 @@ +use std::process::Command; + use crate::{ distro::{distro_config::DistroConfig, OSConfig}, error::AppResult, + utils::ROOT_MNT, }; use super::{ - base_task::ALL_BASE_TASKS, chrooting::ChrootedTask, custom_task::CustomTask, Task, TaskTrait, + base_task::ALL_BASE_TASKS, chrooting::Chroot, custom_task::CustomTask, Task, TaskTrait, }; pub struct TaskExecutor { @@ -62,13 +65,23 @@ impl TaskExecutor { #[tracing::instrument(level = "trace", skip_all)] pub async fn execute(&mut self) -> AppResult<()> { self.tasks.sort_by(Task::compare); + let chroot = Chroot::create(&*ROOT_MNT).await?; + chroot + .run(move || { + Command::new("pacman") + .args(["-S", "--noconfirm", "neofetch"]) + .spawn() + .unwrap() + .wait() + .unwrap(); + Command::new("neofetch").spawn().unwrap().wait().unwrap(); + }) + .await + .unwrap()?; for task in &self.tasks { if let Some(up_task) = task.up(&self.os_config)? { - ChrootedTask::new("chroot/root") - .run(move || up_task.exec()) - .await - .unwrap()??; + up_task.exec()?; } } diff --git a/src/utils.rs b/src/utils.rs index 9a5347c..6d7520a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -8,10 +8,17 @@ use crate::task::base_task::ALL_BASE_TASKS; const DEFAULT_CONFIG_DIR: &str = "/etc"; -lazy_static::lazy_static! { - pub static ref CFG_PATH: PathBuf = env::var("TRM_CFG_PATH").map(PathBuf::from).unwrap_or_else(|_| PathBuf::from(DEFAULT_CONFIG_DIR).join("tourmaline")); +macro_rules! env_cfg { + ($name:ident: $type:ident <- $env_key:literal default $default:expr) => { + lazy_static::lazy_static! { + pub static ref $name: $type = env::var($env_key).map($type::from).unwrap_or_else(|_| $default); + } + } } +env_cfg!(CFG_PATH: PathBuf <- "TRM_CFG_PATH" default PathBuf::from(DEFAULT_CONFIG_DIR).join("tourmaline")); +env_cfg!(ROOT_MNT: PathBuf <- "TRM_ROOT_MNT" default PathBuf::from("/mnt")); + pub async fn generate_script_files>(output: P) -> AppResult<()> { let output = output.as_ref();