From c8bf95ec9ca6597755af49a87d22f9e0569ce047 Mon Sep 17 00:00:00 2001 From: sisungo Date: Sat, 16 May 2026 12:15:55 +0800 Subject: [PATCH] initial commit Signed-off-by: sisungo --- .gitignore | 2 + Cargo.toml | 3 + bin/semios_sandbox_exec/Cargo.toml | 8 + bin/semios_sandbox_exec/src/main.rs | 58 ++++++ lib/semios_sandbox/Cargo.toml | 9 + lib/semios_sandbox/src/lib.rs | 4 + lib/semios_sandbox/src/vm.rs | 254 +++++++++++++++++++++++++++ lib/semios_sandbox_low/Cargo.toml | 9 + lib/semios_sandbox_low/src/lib.rs | 73 ++++++++ lib/semios_sandbox_low/src/linux.rs | 68 +++++++ lib/semios_sandbox_parser/Cargo.toml | 8 + lib/semios_sandbox_parser/src/lib.rs | 179 +++++++++++++++++++ share/rules/app.sb | 15 ++ 13 files changed, 690 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 bin/semios_sandbox_exec/Cargo.toml create mode 100644 bin/semios_sandbox_exec/src/main.rs create mode 100644 lib/semios_sandbox/Cargo.toml create mode 100644 lib/semios_sandbox/src/lib.rs create mode 100644 lib/semios_sandbox/src/vm.rs create mode 100644 lib/semios_sandbox_low/Cargo.toml create mode 100644 lib/semios_sandbox_low/src/lib.rs create mode 100644 lib/semios_sandbox_low/src/linux.rs create mode 100644 lib/semios_sandbox_parser/Cargo.toml create mode 100644 lib/semios_sandbox_parser/src/lib.rs create mode 100644 share/rules/app.sb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..486a123 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["bin/semios_sandbox_exec", "lib/semios_sandbox","lib/semios_sandbox_low","lib/semios_sandbox_parser"] +resolver = "3" diff --git a/bin/semios_sandbox_exec/Cargo.toml b/bin/semios_sandbox_exec/Cargo.toml new file mode 100644 index 0000000..a45608e --- /dev/null +++ b/bin/semios_sandbox_exec/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "semios_sandbox_exec" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4", default-features = false, features = ["std", "help", "usage", "derive"] } +semios_sandbox = { path = "../../lib/semios_sandbox" } diff --git a/bin/semios_sandbox_exec/src/main.rs b/bin/semios_sandbox_exec/src/main.rs new file mode 100644 index 0000000..4baf9e2 --- /dev/null +++ b/bin/semios_sandbox_exec/src/main.rs @@ -0,0 +1,58 @@ +use clap::Parser; +use semios_sandbox::vm::Vm; +use std::{ffi::OsString, os::unix::process::CommandExt, path::PathBuf}; + +macro_rules! errx { + ($($arg:tt)*) => { + eprintln!("semios_sandbox_exec: {}", format!($($arg)*)); + std::process::exit(1); + }; +} + +#[derive(Debug, Clone, Parser)] +struct Cli { + #[arg(short, long)] + rule: PathBuf, + + #[arg(long)] + arg0: Option, + + cmd: Vec, +} + +fn main() { + let cli = Cli::parse(); + let rule = match std::fs::read_to_string(&cli.rule) { + Ok(x) => x, + Err(err) => { + errx!("failed to read rule \"{}\": {}", cli.rule.display(), err); + } + }; + let ast = match semios_sandbox::parser::parse(&rule) { + Ok(x) => x, + Err(err) => { + errx!("failed to parse rule \"{}\": {}", cli.rule.display(), err); + } + }; + + let mut vm = Vm::new(); + if let Err(err) = vm.exec(&ast) { + errx!("failed to execute rule \"{}\": {}", cli.rule.display(), err); + }; + if let Err(err) = vm.finish() { + errx!("failed to apply rules: {err}"); + } + + let Some(path) = cli.cmd.first() else { + errx!("no program specified"); + }; + let arg0 = cli.arg0.as_ref().unwrap_or(path); + let args = &cli.cmd[1..]; + + let err = std::process::Command::new(path) + .arg0(arg0) + .args(args) + .exec(); + + errx!("failed to execute \"{}\": {}", path.display(), err); +} diff --git a/lib/semios_sandbox/Cargo.toml b/lib/semios_sandbox/Cargo.toml new file mode 100644 index 0000000..cb47e83 --- /dev/null +++ b/lib/semios_sandbox/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "semios_sandbox" +version = "0.1.0" +edition = "2024" + +[dependencies] +semios_sandbox_parser = { path = "../semios_sandbox_parser" } +semios_sandbox_low = { path = "../semios_sandbox_low" } +rustc-hash = "2" diff --git a/lib/semios_sandbox/src/lib.rs b/lib/semios_sandbox/src/lib.rs new file mode 100644 index 0000000..7aad6fe --- /dev/null +++ b/lib/semios_sandbox/src/lib.rs @@ -0,0 +1,4 @@ +pub mod vm; + +pub use semios_sandbox_low as low; +pub use semios_sandbox_parser as parser; diff --git a/lib/semios_sandbox/src/vm.rs b/lib/semios_sandbox/src/vm.rs new file mode 100644 index 0000000..e186711 --- /dev/null +++ b/lib/semios_sandbox/src/vm.rs @@ -0,0 +1,254 @@ +//! Virtual machine for executing rule code. + +use rustc_hash::FxHashMap; +use semios_sandbox_low::{FilesystemOps, FilesystemRule, Rules, apply}; +use semios_sandbox_parser::{Expr, LitPart, Stmt}; +use std::{ + fmt::{Debug, Display}, + path::PathBuf, +}; + +pub struct Vm { + native_mods: Vec>, +} +impl Vm { + pub fn new() -> Self { + Self { + native_mods: vec![Box::new(EnvMod), Box::new(SandboxMod::new())], + } + } + + pub fn exec(&mut self, ast: &[Stmt]) -> Result<(), VmError> { + let mut local_scope = LocalScope::new(); + for i in ast { + match i { + Stmt::Let(decl) => { + local_scope.declare(&decl.name.0, self.eval(&local_scope, decl.value.clone())?); + } + Stmt::Expr(expr) => { + self.eval(&local_scope, expr.clone())?; + } + } + } + Ok(()) + } + + pub fn finish(self) -> Result<(), VmError> { + for mut i in self.native_mods { + i.finish() + .map_err(|err| VmError::NativeError("[anonymous] finish".into(), err))?; + } + Ok(()) + } + + fn eval(&mut self, local_scope: &LocalScope, expr: Expr) -> Result { + match expr { + Expr::Lit(x) => { + let Some(parts) = semios_sandbox_parser::lit(&x) else { + return Err(VmError::LitSyntaxError); + }; + let mut s = String::new(); + for i in parts { + match i { + LitPart::Direct(y) => s.push_str(&y), + LitPart::Variable(key) => match local_scope.get(&key.0)? { + Value::Lit(z) => s.push_str(&z), + _ => return Err(VmError::VarAbsent(key.0)), + }, + } + } + Ok(Value::Lit(s)) + } + Expr::Var(x) => local_scope.get(&x.0), + Expr::Call(x) => { + let mut args = Vec::with_capacity(x.args.len()); + for i in x.args { + args.push(self.eval(local_scope, i)?); + } + for i in &mut self.native_mods { + match i.invoke(&x.name.0, &args) { + Ok(ret) => return Ok(ret), + Err(NativeModError::MethodAbsent) => continue, + Err(NativeModError::StdError(err)) => { + return Err(VmError::NativeError(x.name.0, err)); + } + } + } + Err(VmError::VarAbsent(x.name.0)) + } + Expr::Set(x) => { + let mut ret = Vec::with_capacity(x.len()); + for i in x { + ret.push(self.eval(local_scope, i)?); + } + Ok(Value::Set(ret)) + } + } + } +} +impl Debug for Vm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Vm").field("native_mods", &()).finish() + } +} + +#[derive(Debug, Clone, Default)] +struct LocalScope { + map: FxHashMap, +} +impl LocalScope { + fn new() -> Self { + Self::default() + } + + fn declare(&mut self, key: &str, val: Value) { + self.map.insert(key.into(), val); + } + + fn get(&self, key: &str) -> Result { + self.map + .get(key) + .cloned() + .ok_or_else(|| VmError::MethodAbsent(key.into())) + } +} + +pub trait NativeMod { + fn invoke(&mut self, method: &str, args: &[Value]) -> Result; + fn finish(&mut self) -> Result<(), Box> { + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub enum Value { + Set(Vec), + Lit(String), +} + +#[derive(Debug)] +pub enum NativeModError { + MethodAbsent, + StdError(Box), +} +impl From for NativeModError { + fn from(value: E) -> Self { + Self::StdError(Box::new(value)) + } +} +impl Display for NativeModError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MethodAbsent => write!(f, "method is absent"), + Self::StdError(err) => write!(f, "{err}"), + } + } +} + +#[derive(Debug)] +pub enum VmError { + MethodAbsent(String), + VarAbsent(String), + NativeError(String, Box), + LitSyntaxError, +} +impl Display for VmError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MethodAbsent(method) => write!(f, "method is absent: {method}"), + Self::VarAbsent(var) => write!(f, "variable is absent: {var}"), + Self::NativeError(method, err) => write!(f, "method `{method}` failed: {err}"), + Self::LitSyntaxError => write!(f, "literal syntax error"), + } + } +} +impl std::error::Error for VmError {} + +struct EnvMod; +impl NativeMod for EnvMod { + fn invoke(&mut self, method: &str, args: &[Value]) -> Result { + if method == "env" { + let Some(Value::Lit(key)) = args.get(0) else { + return Err(NativeModError::MethodAbsent); + }; + if let Ok(val) = std::env::var(key) { + Ok(Value::Lit(val)) + } else if let Some(Value::Lit(default)) = args.get(1) { + Ok(Value::Lit(default.into())) + } else { + Err(NativeModError::StdError(Box::from(format!( + "Environment `{key}` not found" + )))) + } + } else { + Err(NativeModError::MethodAbsent) + } + } +} + +struct SandboxMod { + rules: Rules, +} +impl SandboxMod { + fn new() -> Self { + Self { + rules: Rules::new(), + } + } + + fn fs_allow(&mut self, path: &str, sops: &[String]) -> Result { + let fs_op = |x| match x { + "read" => Ok(FilesystemOps::READ), + "write" => Ok(FilesystemOps::WRITE), + "exec" => Ok(FilesystemOps::EXEC), + "browse" => Ok(FilesystemOps::BROWSE), + "create_reg" => Ok(FilesystemOps::CREATE_REG), + "create_dir" => Ok(FilesystemOps::CREATE_DIR), + "create_dev" => Ok(FilesystemOps::CREATE_DEV), + "create_sym" => Ok(FilesystemOps::CREATE_SYM), + "create_ipc" => Ok(FilesystemOps::CREATE_IPC), + "remove_file" => Ok(FilesystemOps::REMOVE_FILE), + "remove_dir" => Ok(FilesystemOps::REMOVE_DIR), + "control_io" => Ok(FilesystemOps::CONTROL_IO), + "refer" => Ok(FilesystemOps::REFER), + other => Err(NativeModError::StdError(Box::from(format!( + "unrecognized filesystem op `{other}`" + )))), + }; + + let mut ops = FilesystemOps::empty(); + for sop in sops { + ops |= fs_op(sop)?; + } + + self.rules.filesystem.push(FilesystemRule { + path: PathBuf::from(path), + ops, + }); + + Ok(Value::Lit(String::new())) + } +} +impl NativeMod for SandboxMod { + fn invoke(&mut self, method: &str, args: &[Value]) -> Result { + match (method, args) { + ("fs_allow", [Value::Lit(path), Value::Set(ops)]) => { + let mut rops = Vec::with_capacity(ops.len()); + for op in ops { + if let Value::Lit(op) = op { + rops.push(op.into()); + } else { + return Err(NativeModError::StdError(Box::from("invalid arguments"))); + } + } + self.fs_allow(path, &rops) + } + ("fs_allow", [Value::Lit(path), Value::Lit(op)]) => self.fs_allow(path, &[op.into()]), + _ => Err(NativeModError::MethodAbsent), + } + } + + fn finish(&mut self) -> Result<(), Box> { + apply(self.rules.clone()).map_err(Box::from) + } +} diff --git a/lib/semios_sandbox_low/Cargo.toml b/lib/semios_sandbox_low/Cargo.toml new file mode 100644 index 0000000..2b5bbb3 --- /dev/null +++ b/lib/semios_sandbox_low/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "semios_sandbox_low" +version = "0.1.0" +edition = "2024" + +[dependencies] +bitflags = "2" +enumflags2 = "0.7" +landlock = "0.4" diff --git a/lib/semios_sandbox_low/src/lib.rs b/lib/semios_sandbox_low/src/lib.rs new file mode 100644 index 0000000..5641e19 --- /dev/null +++ b/lib/semios_sandbox_low/src/lib.rs @@ -0,0 +1,73 @@ +use bitflags::bitflags; +use std::path::PathBuf; + +#[cfg_attr(target_os = "linux", path = "linux.rs")] +mod imp; + +/// Low-level sandbox rules. +#[derive(Debug, Clone)] +pub struct Rules { + pub filesystem: Vec, +} +impl Rules { + pub fn new() -> Self { + Self { + filesystem: Vec::new(), + } + } +} + +/// Filesystem rule set. +#[derive(Debug, Clone)] +pub struct FilesystemRule { + pub path: PathBuf, + pub ops: FilesystemOps, +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub struct FilesystemOps: u32 { + /// Read a file. + const READ = 1; + + /// Write to a file. + const WRITE = 2; + + /// Execute a file. + const EXEC = 4; + + /// Read / Browse a directory. + const BROWSE = 8; + + /// Create a regular file. + const CREATE_REG = 16; + + /// Create a directory. + const CREATE_DIR = 32; + + /// Create a symbolic link. + const CREATE_SYM = 64; + + /// Create a IPC primitive. + const CREATE_IPC = 128; + + /// Create a device file. + const CREATE_DEV = 256; + + /// Removes a directory. + const REMOVE_DIR = 512; + + /// Removes a file. + const REMOVE_FILE = 1024; + + /// Link or rename a file from or to a different directory. + const REFER = 2048; + + /// Controls device input / output. + const CONTROL_IO = 4096; + } +} + +pub fn apply(rules: Rules) -> Result<(), Box> { + imp::apply_rules(rules) +} diff --git a/lib/semios_sandbox_low/src/linux.rs b/lib/semios_sandbox_low/src/linux.rs new file mode 100644 index 0000000..087a05d --- /dev/null +++ b/lib/semios_sandbox_low/src/linux.rs @@ -0,0 +1,68 @@ +use crate::{FilesystemOps, Rules}; +use enumflags2::BitFlag; +use landlock::{ + ABI, Access, AccessFs, BitFlags, RulesetAttr, RulesetCreatedAttr, path_beneath_rules, +}; + +pub fn apply_rules(rules: Rules) -> Result<(), Box> { + let mut ruleset = landlock::Ruleset::default() + .handle_access(AccessFs::from_all(ABI::V6))? + .create()?; + + for i in rules.filesystem.iter() { + ruleset = ruleset.add_rules(path_beneath_rules( + [i.path.clone()], + landlock_fs_access(i.ops), + ))?; + } + + ruleset.restrict_self()?; + + Ok(()) +} + +fn landlock_fs_access(x: FilesystemOps) -> BitFlags { + let mut ret = AccessFs::empty(); + + if x.contains(FilesystemOps::READ) { + ret |= AccessFs::ReadFile; + } + if x.contains(FilesystemOps::WRITE) { + ret |= AccessFs::WriteFile | AccessFs::Truncate; + } + if x.contains(FilesystemOps::EXEC) { + ret |= AccessFs::Execute; + } + if x.contains(FilesystemOps::BROWSE) { + ret |= AccessFs::ReadDir; + } + if x.contains(FilesystemOps::CREATE_REG) { + ret |= AccessFs::MakeReg; + } + if x.contains(FilesystemOps::CREATE_DIR) { + ret |= AccessFs::MakeDir; + } + if x.contains(FilesystemOps::CREATE_SYM) { + ret |= AccessFs::MakeSym; + } + if x.contains(FilesystemOps::CREATE_DEV) { + ret |= AccessFs::MakeBlock | AccessFs::MakeChar; + } + if x.contains(FilesystemOps::CREATE_IPC) { + ret |= AccessFs::MakeSock | AccessFs::MakeFifo; + } + if x.contains(FilesystemOps::REMOVE_FILE) { + ret |= AccessFs::RemoveFile; + } + if x.contains(FilesystemOps::REMOVE_DIR) { + ret |= AccessFs::RemoveDir; + } + if x.contains(FilesystemOps::CONTROL_IO) { + ret |= AccessFs::IoctlDev; + } + if x.contains(FilesystemOps::REFER) { + ret |= AccessFs::Refer; + } + + ret +} diff --git a/lib/semios_sandbox_parser/Cargo.toml b/lib/semios_sandbox_parser/Cargo.toml new file mode 100644 index 0000000..6ed37f4 --- /dev/null +++ b/lib/semios_sandbox_parser/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "semios_sandbox_parser" +version = "0.1.0" +edition = "2024" + +[dependencies] +peg = "0.8" +unicode-xid = "0.2" diff --git a/lib/semios_sandbox_parser/src/lib.rs b/lib/semios_sandbox_parser/src/lib.rs new file mode 100644 index 0000000..0e855c2 --- /dev/null +++ b/lib/semios_sandbox_parser/src/lib.rs @@ -0,0 +1,179 @@ +use unicode_xid::UnicodeXID; + +peg::parser! { + grammar sandbox() for str { + pub rule program() -> Vec + = _* stmt:stmt() ** (_*) _* { stmt } + + rule stmt() -> Stmt + = x:let_stmt() { Stmt::Let(x) } + / x:expr() _* ";" { Stmt::Expr(x) } + + rule let_stmt() -> StmtLet + = "let" _+ name:ident() _* "=" _* value:expr() _* ";" { StmtLet { name, value } } + + rule expr() -> Expr + = x:expr_call() { Expr::Call(x) } + / x:lit() { Expr::Lit(x) } + / x:ident() { Expr::Var(x) } + / x:expr_set() { Expr::Set(x) } + + rule expr_call() -> ExprCall + = name:ident() _* "(" _* args:expr() ** (_* "," _*) _* ")" { ExprCall { name, args } } + + rule expr_set() -> Vec + = "{" _* args:expr() ** (_* "," _*) _* "}" { args } + + rule ident() -> Ident + = f:([x if UnicodeXID::is_xid_start(x)]) s:([x if UnicodeXID::is_xid_continue(x)])* + { Ident(format!("{}{}", f, s.iter().collect::())) } + + rule lit() -> String + = lit_str() + / lit_int() + + rule lit_int() -> String + = x:['0'..='9']+ { x.into_iter().collect() } + / "0x" x:['a'..='f' | 'A'..='F' | '0'..='9']+ { "0x".chars().chain(x.into_iter()).collect() } + / "0o" x:['0'..='7']+ { "0o".chars().chain(x.into_iter()).collect() } + / "0b" x:['0'..='1']+ { "0b".chars().chain(x.into_iter()).collect() } + + rule lit_str() -> String + = "\"" x:lit_str_double_part()* "\"" { x.iter().collect() } + / "'" x:lit_str_single_part()* "'" { x.iter().collect() } + + rule lit_str_double_part() -> char + = x:lit_str_part_escape() { x } + / x:[^ '"' | '\n' | '\r'] { x } + + rule lit_str_single_part() -> char + = x:lit_str_part_escape() { x } + / x:[^ '\'' | '\n' | '\r'] { x } + + rule lit_str_part_escape() -> char + = "\\\\" { '\\' } + / "\\'" { '\'' } + / "\\\"" { '"' } + / "\\n" { '\n' } + / "\\r" { '\r' } + / "\\t" { '\t' } + + rule comment() + = "//" [^ '\n']* + + rule _() + = [' ' | '\t' | '\n'] + / comment() + } +} + +#[derive(Debug, Clone)] +pub struct Ident(pub String); + +#[derive(Debug, Clone)] +pub enum Stmt { + Let(StmtLet), + Expr(Expr), +} + +#[derive(Debug, Clone)] +pub struct StmtLet { + pub name: Ident, + pub value: Expr, +} + +#[derive(Debug, Clone)] +pub enum Expr { + Call(ExprCall), + Lit(String), + Var(Ident), + Set(Vec), +} + +#[derive(Debug, Clone)] +pub struct ExprCall { + pub name: Ident, + pub args: Vec, +} + +pub use sandbox::program as parse; + +#[derive(Debug, Clone)] +pub enum LitPart { + Direct(String), + Variable(Ident), +} + +pub fn lit(s: &str) -> Option> { + let mut ret = Vec::new(); + let mut current = String::new(); + let mut chars = s.chars().peekable(); + let mut in_ident = false; + + while let Some(c) = chars.next() { + if c == '{' { + if let Some(&next_c) = chars.peek() { + if next_c == '{' { + current.push('{'); + chars.next(); + continue; + } + } + + if !in_ident { + if !current.is_empty() { + ret.push(LitPart::Direct(current.clone())); + current.clear(); + } + in_ident = true; + + let mut ident = String::new(); + let mut closed = false; + + while let Some(ch) = chars.next() { + if ch == '}' { + closed = true; + break; + } + ident.push(ch); + } + + if !closed { + return None; + } + + if ident.is_empty() { + return None; + } + + ret.push(LitPart::Variable(Ident(ident))); + } else { + return None; + } + } else if c == '}' { + if let Some(&next_c) = chars.peek() { + if next_c == '}' { + current.push('}'); + chars.next(); + continue; + } + } + return None; + } else { + if in_ident { + return None; + } + current.push(c); + } + } + + if in_ident { + return None; + } + + if !current.is_empty() { + ret.push(LitPart::Direct(current)); + } + + Some(ret) +} diff --git a/share/rules/app.sb b/share/rules/app.sb new file mode 100644 index 0000000..a628a83 --- /dev/null +++ b/share/rules/app.sb @@ -0,0 +1,15 @@ +// Pre-defined Sandbox Rule: APPLICATION + +let home = env('HOME'); + +// Allow linking to system libraries. +fs_allow('/lib', { "read", "exec", "browse" }); +fs_allow('/lib64', { "read", "exec", "browse" }); +fs_allow('/usr/lib', { "read", "exec", "browse" }); + +// Allow executing system commands. +fs_allow('/bin', { "read", "browse", "exec" }); +fs_allow('/usr/bin', { "read", "browse", "exec" }); + +// Allow accessing user home. +fs_allow(home, { "read", "write", "browse" });