diff --git a/src/main.rs b/src/main.rs index 1249f2f..f003e6c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ mod parse; mod types; -use std::collections::VecDeque; +use std::{collections::VecDeque, path::PathBuf, str::FromStr}; use anyhow::{Context, Result, anyhow}; use elb_dl::{DependencyTree, DynamicLoader, glibc}; @@ -9,11 +9,10 @@ use landlock::{ RulesetCreatedAttr, Scope, make_bitflags, path_beneath_rules, }; -use crate::types::BasePermission; - fn main() -> Result<()> { let opts = parse::parse_args()?; if opts.exec.is_empty() { + // print help eprintln!( " yoke -- simple command sandboxer @@ -43,13 +42,18 @@ rule types resolve process dependencies and add to sandbox: --ldd | -l + unsandbox: + --no-fs | -nf + --no-tcp | -nt + specifiers ------------ fs: - r - read (implies execute) - w - write (implies read+execute) - i - allow ioctls (implies nothing - may require read or write) + r - read + w - write + x - execute + i - ioctl tcp: i - in/bind @@ -65,27 +69,36 @@ examples ); std::process::exit(1); } + + // set up scope of our intial ruleset let mut preempt = Ruleset::default(); + // TODO FIXME set up for lesser ABI versions preempt = preempt.set_compatibility(landlock::CompatLevel::HardRequirement); + + // disallow signals to other processes if !opts.signals { preempt = preempt.scope(Scope::Signal).context("scoping signals")?; } + + // disallow connections to unix domain sockets if !opts.sockets { preempt = preempt .scope(Scope::AbstractUnixSocket) .context("scoping sockets")?; } - let is_fs = !opts.fs.is_empty(); - let is_tcp = !opts.tcp.is_empty(); - preempt = if is_fs { + + // lock down fs access + preempt = if !opts.unsandbox.fs { preempt .handle_access(AccessFs::from_all(ABI::V6)) .context("handling fs access")? } else { preempt }; - preempt = if is_tcp { + + // lock down tcp access + preempt = if !opts.unsandbox.tcp { preempt .handle_access(AccessNet::BindTcp) .context("handling tcp bind access")? @@ -94,21 +107,38 @@ examples } else { preempt }; + + // create ruleset and begin inserting rules let mut ruleset = preempt.create().context("creating ruleset")?; + + // allow each path specified, grouped by access specifier for (perms, paths) in opts.fs { let mut access = BitFlags::empty(); - match perms.base { - BasePermission::Unset => {} - BasePermission::Read => { - access = AccessFs::from_read(ABI::V6); - } - BasePermission::Write => { - access = AccessFs::from_write(ABI::V6); - access.insert(AccessFs::from_read(ABI::V6)); - } - }; + if perms.read { + access.insert(make_bitflags!(AccessFs::{ReadFile | ReadDir})); + } + if perms.write { + access.insert(make_bitflags!( + AccessFs::{ WriteFile + | RemoveDir + | RemoveFile + | MakeChar + | MakeDir + | MakeReg + | MakeSock + | MakeFifo + | MakeBlock + | MakeSym + | Refer + | Truncate + } + )); + } + if perms.execute { + access.insert(AccessFs::Execute); + } if perms.ioctl { - access.insert(make_bitflags!(AccessFs::IoctlDev)); + access.insert(AccessFs::IoctlDev); } if access == BitFlags::empty() { return Err(anyhow!("invalid filesystem permissions requested")); @@ -117,6 +147,8 @@ examples .add_rules(path_beneath_rules(paths, access)) .context("adding fs rule")?; } + + // allow each tcp action specified, grouped by access specifier for (dir, ports) in opts.tcp { let mut access = BitFlags::empty(); if dir.inbound { @@ -131,14 +163,24 @@ examples .context("adding tcp rule")?; } } + + // locate our executable let fullpath = which::which(&opts.exec[0]).context("finding executable")?; - ruleset = ruleset.add_rules(path_beneath_rules( - std::slice::from_ref(&fullpath), - AccessFs::from_read(ABI::V6), - ))?; + // add executeable as read+execute + if !opts.unsandbox.fs { + ruleset = ruleset.add_rules(path_beneath_rules( + std::slice::from_ref(&fullpath), + AccessFs::from_read(ABI::V6), + ))?; + } + + // if requested, trace dependencies and add as read+execute if opts.ldd { let loader = DynamicLoader::options() .search_dirs(glibc::get_hard_coded_search_dirs(None)?) + .search_dirs(glibc::get_search_dirs( + PathBuf::from_str("/").context("finding root")?, + )?) .new_loader(); let mut tree = DependencyTree::new(); let mut queue = VecDeque::new(); @@ -157,11 +199,17 @@ examples )) .context("tracking dependencies")?; } + + // enforce the ruleset on ourselves ruleset.restrict_self().context("enforcing ruleset")?; + + // construct a command for the target program let mut cmd = exec::Command::new(fullpath); if opts.exec.len() > 1 { cmd.args(&opts.exec[1..]); } + + // clear env unless retention is requested if !opts.retain_env { for (k, _) in std::env::vars() { unsafe { @@ -169,6 +217,8 @@ examples } } } + + // add specified env vars if !opts.env.is_empty() { for (k, v) in opts.env { unsafe { @@ -176,6 +226,8 @@ examples } } } + + // execute and hopefully never return let err = cmd.exec(); eprintln!("failed to run process: {}", err); Ok(()) diff --git a/src/parse.rs b/src/parse.rs index 6e38e62..a369689 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,5 +1,5 @@ -use crate::types::{BasePermission, Direction, Permissions, Yoke}; -use anyhow::{Context, Result, anyhow}; +use crate::types::{Direction, Permissions, Yoke}; +use anyhow::{anyhow, Context, Result}; use std::{collections::HashMap, path::PathBuf, str::FromStr}; #[derive(PartialEq)] @@ -32,17 +32,18 @@ fn fs_parse(pairs: &[String]) -> Result>> { .ok_or(anyhow!("invalid filesystem pair"))?; let mut perms = Permissions::default(); - - use BasePermission::*; for c in s_perm.chars() { match c { - 'r' | 'R' if perms.base == Unset => { - perms.base = Read; + 'r' => { + perms.read = true; } - 'w' | 'W' => { - perms.base = Write; + 'w' => { + perms.write = true; } - 'i' | 'I' => { + 'x' => { + perms.execute = true; + } + 'i' => { perms.ioctl = true; } s => return Err(anyhow!("invalid access specifier {}", s)), @@ -150,6 +151,18 @@ pub fn parse_args() -> Result { cur_arg = Unset; yoke.ldd = true; } + "--no-fs" | "-nf" => { + collect_args(&mut yoke, &collector, &cur_arg)?; + collector.clear(); + cur_arg = Unset; + yoke.unsandbox.fs = true; + } + "--no-tcp" | "-nt" => { + collect_args(&mut yoke, &collector, &cur_arg)?; + collector.clear(); + cur_arg = Unset; + yoke.unsandbox.tcp = true; + } "--" => { collect_args(&mut yoke, &collector, &cur_arg)?; collector.clear(); diff --git a/src/types.rs b/src/types.rs index 50876bc..fffb4be 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,11 @@ use std::{collections::HashMap, path::PathBuf}; +#[derive(Debug, Default)] +pub struct Unsandbox { + pub fs: bool, + pub tcp: bool, +} + #[derive(Debug, Default)] pub struct Yoke { pub fs: HashMap>, @@ -8,21 +14,16 @@ pub struct Yoke { pub retain_env: bool, pub signals: bool, pub sockets: bool, + pub unsandbox: Unsandbox, pub ldd: bool, pub exec: Vec, } -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] -pub enum BasePermission { - #[default] - Unset, - Read, - Write, -} - #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] pub struct Permissions { - pub base: BasePermission, + pub read: bool, + pub write: bool, + pub execute: bool, pub ioctl: bool, }