diff --git a/.gitignore b/.gitignore index 78da254..7f526c2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ Cargo.lock # Misc test-config.json +chroot diff --git a/Cargo.lock b/Cargo.lock index 4514d4b..0c3299f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3517,6 +3517,7 @@ dependencies = [ "dotenv", "embed-nu", "lazy_static", + "libc", "paste", "rusty-value", "serde", diff --git a/Cargo.toml b/Cargo.toml index 4b02839..63b284a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ codename = "Walter White" name = "tourmaline" [[bin]] -name = "trl" +name = "trm" path = "src/main.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -21,6 +21,7 @@ color-eyre = "0.6.2" dotenv = "0.15.0" embed-nu = "0.3.3" lazy_static = "1.4.0" +libc = "0.2.135" paste = "1.0.9" rusty-value = { version = "0.6.0", features = ["derive", "json"] } serde = { version = "1.0.145", features = ["derive"] } diff --git a/src/error.rs b/src/error.rs index c1279df..bbdff70 100644 --- a/src/error.rs +++ b/src/error.rs @@ -26,6 +26,9 @@ pub enum AppError { #[error(transparent)] OSConfig(#[from] OSConfigError), + + #[error(transparent)] + Chroot(#[from] ChrootError), } #[derive(Error, Debug)] @@ -78,3 +81,18 @@ pub enum OSConfigError { #[error("Missing config key {0}")] MissingConfigKey(String), } + +#[derive(Error, Debug)] +pub enum ChrootError { + #[error("Could not find chroot directory {0}")] + NotFound(PathBuf), + + #[error("Failed to unshare FS resources with parent: {0}")] + Unshare(io::Error), + + #[error("Failed to chroot: {0}")] + Chroot(io::Error), + + #[error("Failed to change process working directory: {0}")] + ChDir(io::Error), +} diff --git a/src/task/base_task.rs b/src/task/base_task.rs index 429dad8..ef78d98 100644 --- a/src/task/base_task.rs +++ b/src/task/base_task.rs @@ -84,12 +84,9 @@ impl TaskTrait for BaseTask { } else { Option::<()>::None.into_value() }; + let exec = ExecBuilder::create(script, config.to_owned(), task_config)?; - Ok(Some(ExecBuilder { - script, - os_config: config.to_owned(), - task_config, - })) + Ok(Some(exec)) } #[tracing::instrument(level = "trace", skip_all)] @@ -101,12 +98,9 @@ impl TaskTrait for BaseTask { } else { Option::<()>::None.into_value() }; + let exec = ExecBuilder::create(script, config.to_owned(), task_config)?; - Ok(Some(ExecBuilder { - script, - os_config: config.to_owned(), - task_config, - })) + Ok(Some(exec)) } fn order(&self) -> usize { diff --git a/src/task/chrooting.rs b/src/task/chrooting.rs new file mode 100644 index 0000000..4555e71 --- /dev/null +++ b/src/task/chrooting.rs @@ -0,0 +1,61 @@ +use std::{ + ffi::{c_int, CString}, + io, + os::unix::prelude::OsStrExt, + path::{Path, PathBuf}, +}; + +use libc::CLONE_FS; +use tokio::task::JoinHandle; + +use crate::error::ChrootError; + +pub struct ChrootedTask { + root_path: PathBuf, +} + +impl ChrootedTask { + /// Creates a new chrooted thread with the given path + pub fn new>(root_path: P) -> Self { + Self { + root_path: root_path.into(), + } + } + + /// Runs the given future in a new chroot + pub fn run(self, call: F) -> JoinHandle> + where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, + { + let root_path = self.root_path; + let handle = std::thread::spawn(move || { + unsafe { + init_chroot(&root_path)?; + } + Ok(call()) + }); + tokio::task::spawn_blocking(|| handle.join().unwrap()) + } +} + +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(); + 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_var("PWD", "/"); + + Ok(()) +} + +fn handle_err_code(code: c_int) -> Result<(), io::Error> { + if code != 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} diff --git a/src/task/custom_task.rs b/src/task/custom_task.rs index 81f3e2f..132143c 100644 --- a/src/task/custom_task.rs +++ b/src/task/custom_task.rs @@ -45,11 +45,9 @@ impl TaskTrait for CustomTask { if self.skip_on_false && self.config_key.is_some() && config_is_falsy(&task_config) { Ok(None) } else { - Ok(Some(ExecBuilder { - script: self.up_script.to_owned(), - os_config: config.to_owned(), - task_config, - })) + let exec = + ExecBuilder::create(self.up_script.to_owned(), config.to_owned(), task_config)?; + Ok(Some(exec)) } } @@ -64,11 +62,9 @@ impl TaskTrait for CustomTask { if self.skip_on_false && self.config_key.is_some() && config_is_falsy(&task_config) { Ok(None) } else { - Ok(Some(ExecBuilder { - script: self.down_script.to_owned(), - os_config: config.to_owned(), - task_config, - })) + let exec = + ExecBuilder::create(self.down_script.to_owned(), config.to_owned(), task_config)?; + Ok(Some(exec)) } } diff --git a/src/task/exec_builder.rs b/src/task/exec_builder.rs index b20a19b..75b5f63 100644 --- a/src/task/exec_builder.rs +++ b/src/task/exec_builder.rs @@ -1,24 +1,41 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use embed_nu::{Argument, CommandGroupConfig, Context, ValueIntoExpression}; -use tokio::fs; +use std::fs; use crate::{distro::OSConfig, error::ScriptError, utils::CFG_PATH}; +#[derive(Clone)] pub struct ExecBuilder { - pub script: PathBuf, - pub os_config: OSConfig, - pub task_config: embed_nu::Value, + script: PathBuf, + script_contents: String, + os_config: OSConfig, + task_config: embed_nu::Value, } impl ExecBuilder { + pub fn create( + script: PathBuf, + os_config: OSConfig, + task_config: embed_nu::Value, + ) -> Result { + let script_contents = Self::get_script_contents(&script)?; + + Ok(Self { + script, + script_contents, + os_config, + task_config, + }) + } + #[tracing::instrument(level = "trace", skip_all)] - pub async fn exec(self) -> Result<(), ScriptError> { - let script_contents = self.get_script_contents().await?; + pub fn exec(self) -> Result<(), ScriptError> { let mut ctx = Context::builder() .with_command_groups(CommandGroupConfig::default().all_groups(true))? + .add_parent_env_vars() .add_var("TRM_CONFIG", self.os_config)? - .add_script(script_contents)? + .add_script(self.script_contents)? .build()?; if ctx.has_fn("main") { @@ -35,11 +52,11 @@ impl ExecBuilder { } #[tracing::instrument(level = "trace", skip_all)] - async fn get_script_contents(&self) -> Result { - let path = CFG_PATH.join(&self.script); + fn get_script_contents(path: &Path) -> Result { + let path = CFG_PATH.join(path); if path.exists() { - fs::read_to_string(path).await.map_err(ScriptError::from) + fs::read_to_string(path).map_err(ScriptError::from) } else { Err(ScriptError::ScriptNotFound(path)) } diff --git a/src/task/mod.rs b/src/task/mod.rs index f98f81b..327218c 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -4,6 +4,7 @@ use crate::{distro::OSConfig, error::AppResult}; use self::{base_task::BaseTask, custom_task::CustomTask, exec_builder::ExecBuilder}; pub mod base_task; +mod chrooting; pub mod custom_task; pub mod exec_builder; pub mod task_executor; diff --git a/src/task/task_executor.rs b/src/task/task_executor.rs index ba4a882..282e10c 100644 --- a/src/task/task_executor.rs +++ b/src/task/task_executor.rs @@ -3,7 +3,9 @@ use crate::{ error::AppResult, }; -use super::{base_task::ALL_BASE_TASKS, custom_task::CustomTask, Task, TaskTrait}; +use super::{ + base_task::ALL_BASE_TASKS, chrooting::ChrootedTask, custom_task::CustomTask, Task, TaskTrait, +}; pub struct TaskExecutor { distro_config: DistroConfig, @@ -63,7 +65,10 @@ impl TaskExecutor { for task in &self.tasks { if let Some(up_task) = task.up(&self.os_config)? { - up_task.exec().await? + ChrootedTask::new("chroot/root") + .run(move || up_task.exec()) + .await + .unwrap()??; } }