From 61599dfa2586102accd53ce6ac4dc51eda200c4c Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 28 Jun 2025 20:04:30 +0200 Subject: [PATCH] add landlock --- CHANGELOG.md | 2 ++ Cargo.toml | 2 ++ src/cli/args.rs | 3 +++ src/main.rs | 37 +++++++++++++++++++++++++++++++++ src/sandbox.rs | 39 +++++++++++++++++++++++++++++++++++ src/utils/landlock_support.rs | 24 +++++++++++++++++++++ src/utils/mod.rs | 1 + tests/landlock.rs | 8 +++++++ 8 files changed, 116 insertions(+) create mode 100644 src/sandbox.rs create mode 100644 src/utils/landlock_support.rs create mode 100644 tests/landlock.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e47501e..f4c4e5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ Categories Used: ## [Unreleased](https://github.com/ouch-org/ouch/compare/0.6.1...HEAD) +- Add landlock support for linux filesystem isolation [\#723](https://github.com/ouch-org/ouch/pull/723) ([valoq](https://github.com/valoq)) + ### New Features - Merge folders in decompression [\#798](https://github.com/ouch-org/ouch/pull/798) ([tommady](https://github.com/tommady)) diff --git a/Cargo.toml b/Cargo.toml index 5d31493..0537fb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ gzp = { version = "0.11.3", default-features = false, features = [ "snappy_default", ] } ignore = "0.4.23" +landlock = "0.4.2" libc = "0.2.155" linked-hash-map = "0.5.6" lz4_flex = "0.11.3" @@ -39,6 +40,7 @@ sevenz-rust2 = { version = "0.13.1", features = ["compress", "aes256"] } snap = "1.1.1" tar = "0.4.42" tempfile = "3.10.1" +thiserror = "2.0.12" time = { version = "0.3.36", default-features = false } unrar = { version = "0.5.7", optional = true } xz2 = "0.1.7" diff --git a/src/cli/args.rs b/src/cli/args.rs index b28156c..f3b72c9 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -49,6 +49,8 @@ pub struct CliArgs { #[arg(short = 'c', long, global = true)] pub threads: Option, + pub output_dir: Option, + // Ouch and claps subcommands #[command(subcommand)] pub cmd: Subcommand, @@ -155,6 +157,7 @@ mod tests { // This is usually replaced in assertion tests password: None, threads: None, + output_dir: None, cmd: Subcommand::Decompress { // Put a crazy value here so no test can assert it unintentionally files: vec!["\x00\x11\x22".into()], diff --git a/src/main.rs b/src/main.rs index 3f1f7dd..e74d4df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,8 +7,10 @@ pub mod error; pub mod extension; pub mod list; pub mod utils; +pub mod sandbox; use std::{env, path::PathBuf}; +use std::path::Path; use cli::CliArgs; use once_cell::sync::Lazy; @@ -43,5 +45,40 @@ fn main() { fn run() -> Result<()> { let (args, skip_questions_positively, file_visibility_policy) = CliArgs::parse_and_validate_args()?; + + // Get the output dir if specified, else use current dir + let working_dir = args.output_dir + .clone() + .unwrap_or_else(|| env::current_dir().unwrap_or_default()); + + // restrict filesystem access to working_dir; + init_sandbox(&working_dir); + commands::run(args, skip_questions_positively, file_visibility_policy) } + +fn init_sandbox(allowed_dir: &Path) { + + if std::env::var("CI").is_ok() { + return; + } + + + if utils::landlock_support::is_landlock_supported() { + + let path_str = allowed_dir.to_str().expect("Cannot convert path"); + match sandbox::restrict_paths(&[path_str]) { + Ok(status) => { + //check + } + Err(e) => { + //log warning + std::process::exit(EXIT_FAILURE); + } + } + } else { +// warn!("Landlock is NOT supported on this platform or kernel (<5.19)."); + } + +} + diff --git a/src/sandbox.rs b/src/sandbox.rs new file mode 100644 index 0000000..9c555f7 --- /dev/null +++ b/src/sandbox.rs @@ -0,0 +1,39 @@ +// generic landlock implementation https://landlock.io/rust-landlock/landlock/struct.Ruleset.html + +use landlock::{ + Access, AccessFs, PathBeneath, PathFd, PathFdError, RestrictionStatus, Ruleset, + RulesetAttr, RulesetCreatedAttr, RulesetError, ABI, +}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum MyRestrictError { + #[error(transparent)] + Ruleset(#[from] RulesetError), + #[error(transparent)] + AddRule(#[from] PathFdError), +} + +pub fn restrict_paths(hierarchies: &[&str]) -> Result { + // The Landlock ABI should be incremented (and tested) regularly. + // ABI set to 2 in compatibility with linux 5.19 and higher + let abi = ABI::V2; + let access_all = AccessFs::from_all(abi); + let access_read = AccessFs::from_read(abi); + + Ok(Ruleset::default() + .handle_access(access_all)? + .create()? + // Read-only access to / (entire filesystem). + .add_rules(landlock::path_beneath_rules(&["/"], access_read))? + .add_rules( + hierarchies + .iter() + .map::, _>(|p| { + Ok(PathBeneath::new(PathFd::new(p)?, access_all)) + }), + )? + .restrict_self()?) +} + + diff --git a/src/utils/landlock_support.rs b/src/utils/landlock_support.rs new file mode 100644 index 0000000..e196284 --- /dev/null +++ b/src/utils/landlock_support.rs @@ -0,0 +1,24 @@ +//Check Landlock kernel support (Linux kernel >= 5.19) + +#[cfg(target_os = "linux")] +pub fn is_landlock_supported() -> bool { + use std::process::Command; + + if let Ok(output) = Command::new("uname").arg("-r").output() { + if let Ok(version_str) = String::from_utf8(output.stdout) { + // Version string is expected to be in "5.19.0-foo" or similar + let mut parts = version_str.trim().split('.'); + if let (Some(major), Some(minor)) = (parts.next(), parts.next()) { + if let (Ok(major), Ok(minor)) = (major.parse::(), minor.parse::()) { + return (major > 5) || (major == 5 && minor >= 19); + } + } + } + } + false +} + +#[cfg(not(target_os = "linux"))] +pub fn is_landlock_supported() -> bool { + false +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 444cf1f..a5cc375 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -10,6 +10,7 @@ mod fs; pub mod io; pub mod logger; mod question; +pub mod landlock_support; pub use self::{ file_visibility::FileVisibilityPolicy, diff --git a/tests/landlock.rs b/tests/landlock.rs new file mode 100644 index 0000000..5c7c357 --- /dev/null +++ b/tests/landlock.rs @@ -0,0 +1,8 @@ +#[test] +fn test_landlock_restriction() { + if !cfg!(target_os = "linux") { + eprintln!("Skipping Landlock test: not running on Linux."); + return; + } + // TODO: Add test +}