From 61599dfa2586102accd53ce6ac4dc51eda200c4c Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 28 Jun 2025 20:04:30 +0200 Subject: [PATCH 01/22] 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 +} From 1aa6bad460cf76324c9c8517e6c5f6d72f831d89 Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 28 Jun 2025 23:00:24 +0200 Subject: [PATCH 02/22] cleanup --- Cargo.lock | 59 +++++++++++++++++++++++++++++-- src/main.rs | 22 ++++++++++-- src/sandbox.rs | 39 --------------------- src/utils/landlock.rs | 66 +++++++++++++++++++++++++++++++++++ src/utils/landlock_support.rs | 24 ------------- src/utils/mod.rs | 2 +- 6 files changed, 142 insertions(+), 70 deletions(-) delete mode 100644 src/sandbox.rs create mode 100644 src/utils/landlock.rs delete mode 100644 src/utils/landlock_support.rs diff --git a/Cargo.lock b/Cargo.lock index 71b8cf9..e6a6d15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -284,7 +284,7 @@ dependencies = [ "byteorder", "bytesize", "libbzip3-sys", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -570,6 +570,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "errno" version = "0.3.10" @@ -728,7 +748,7 @@ dependencies = [ "libz-sys", "num_cpus", "snap", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -886,6 +906,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "landlock" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d2ef408b88e913bfc6594f5e693d57676f6463ded7d8bf994175364320c706" +dependencies = [ + "enumflags2", + "libc", + "thiserror 2.0.12", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1114,6 +1145,7 @@ dependencies = [ "insta", "is_executable", "itertools", + "landlock", "libc", "linked-hash-map", "lz4_flex", @@ -1132,6 +1164,7 @@ dependencies = [ "tar", "tempfile", "test-strategy", + "thiserror 2.0.12", "time", "unrar", "xz2", @@ -1716,7 +1749,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -1730,6 +1772,17 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "time" version = "0.3.41" diff --git a/src/main.rs b/src/main.rs index e74d4df..1e1466a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ pub mod error; pub mod extension; pub mod list; pub mod utils; -pub mod sandbox; +//pub mod sandbox; use std::{env, path::PathBuf}; use std::path::Path; @@ -23,6 +23,9 @@ use self::{ }, }; +//use utils::landlock::*; + + // Used in BufReader and BufWriter to perform less syscalls const BUFFER_CAPACITY: usize = 1024 * 32; @@ -52,6 +55,19 @@ fn run() -> Result<()> { .unwrap_or_else(|| env::current_dir().unwrap_or_default()); // restrict filesystem access to working_dir; + // 1. working_dir is either the output_dir specified by the -d option or + // 2. it is the temporary .tmp-ouch-XXXXXX directory that is renamed after decompression + // + //Case 1: Files are directly written to the output_directory, which may be created by ouch + // full landlock permissions granted inside the specified directory + //Case 2: Files are written to the .tmp-ouch directory, requiring make_dir permissions on the + // parent (cwd) for renaming and full permissions within the tmp-ouch directory itself + // + // Since either the specified output directory is created if it did not exist, or the .ouch-tmp + // directory is created in the current working directory, the parent directory of the target + // directory requires LANDLOCK_ACCESS_FS_MAKE_DIR + + // expects either the .tmp-ouch-XXXXXX path or the specified output directory (-d option) init_sandbox(&working_dir); commands::run(args, skip_questions_positively, file_visibility_policy) @@ -64,10 +80,10 @@ fn init_sandbox(allowed_dir: &Path) { } - if utils::landlock_support::is_landlock_supported() { + if utils::landlock::is_landlock_supported() { let path_str = allowed_dir.to_str().expect("Cannot convert path"); - match sandbox::restrict_paths(&[path_str]) { + match utils::landlock::restrict_paths(&[path_str]) { Ok(status) => { //check } diff --git a/src/sandbox.rs b/src/sandbox.rs deleted file mode 100644 index 9c555f7..0000000 --- a/src/sandbox.rs +++ /dev/null @@ -1,39 +0,0 @@ -// 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.rs b/src/utils/landlock.rs new file mode 100644 index 0000000..8ab67a9 --- /dev/null +++ b/src/utils/landlock.rs @@ -0,0 +1,66 @@ +// Landlock support and generic Landlock sandbox 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; + +/// Returns true if Landlock is supported by the running kernel (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 +} + +#[derive(Debug, Error)] +pub enum MyRestrictError { + #[error(transparent)] + Ruleset(#[from] RulesetError), + #[error(transparent)] + AddRule(#[from] PathFdError), +} + +/// Restricts the process to only access the given hierarchies using Landlock, if supported. +/// +/// The Landlock ABI is set to v2 for compatibility with Linux 5.19+. +/// All hierarchies are given full access, but root ("/") is read-only. +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()?) +} \ No newline at end of file diff --git a/src/utils/landlock_support.rs b/src/utils/landlock_support.rs deleted file mode 100644 index e196284..0000000 --- a/src/utils/landlock_support.rs +++ /dev/null @@ -1,24 +0,0 @@ -//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 a5cc375..611ee89 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -10,7 +10,7 @@ mod fs; pub mod io; pub mod logger; mod question; -pub mod landlock_support; +pub mod landlock; pub use self::{ file_visibility::FileVisibilityPolicy, From a6b3e96df57bd055d1a98a28484bdd5be4ed30de Mon Sep 17 00:00:00 2001 From: valoq Date: Sun, 29 Jun 2025 12:10:20 +0200 Subject: [PATCH 03/22] improve code structure --- src/commands/decompress.rs | 5 ++++ src/commands/list.rs | 6 +++++ src/main.rs | 36 +++----------------------- src/utils/landlock.rs | 53 +++++++++++++++++++++++++++++++++----- 4 files changed, 61 insertions(+), 39 deletions(-) diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index 6c254ad..0885f8c 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -5,6 +5,7 @@ use std::{ }; use fs_err as fs; +use crate::utils::landlock; #[cfg(not(feature = "bzip3"))] use crate::archive; @@ -316,6 +317,10 @@ fn execute_decompression( is_output_dir_provided: bool, is_smart_unpack: bool, ) -> crate::Result> { + + // init landlock sandbox to restrict file system write access to output_dir + landlock::init_sandbox(output_dir); + if is_smart_unpack { return smart_unpack(unpack_fn, output_dir, output_file_path, question_policy); } diff --git a/src/commands/list.rs b/src/commands/list.rs index 4a344a7..4fafec7 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -4,6 +4,7 @@ use std::{ }; use fs_err as fs; +use crate::utils::landlock; use crate::{ archive, @@ -23,6 +24,11 @@ pub fn list_archive_contents( question_policy: QuestionPolicy, password: Option<&[u8]>, ) -> crate::Result<()> { + + // Initialize landlock sandbox with empty write path + // This allows only read access to the filesystem + //landlock::init_sandbox(None); + let reader = fs::File::open(archive_path)?; // Zip archives are special, because they require io::Seek, so it requires it's logic separated diff --git a/src/main.rs b/src/main.rs index 1e1466a..4375188 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,8 @@ 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; @@ -50,9 +48,9 @@ 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()); + //let working_dir = args.output_dir + // .clone() + // .unwrap_or_else(|| env::current_dir().unwrap_or_default()); // restrict filesystem access to working_dir; // 1. working_dir is either the output_dir specified by the -d option or @@ -68,33 +66,7 @@ fn run() -> Result<()> { // directory requires LANDLOCK_ACCESS_FS_MAKE_DIR // expects either the .tmp-ouch-XXXXXX path or the specified output directory (-d option) - init_sandbox(&working_dir); + //utils::landlock::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::is_landlock_supported() { - - let path_str = allowed_dir.to_str().expect("Cannot convert path"); - match utils::landlock::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/utils/landlock.rs b/src/utils/landlock.rs index 8ab67a9..e7f256c 100644 --- a/src/utils/landlock.rs +++ b/src/utils/landlock.rs @@ -7,6 +7,11 @@ use landlock::{ }; use thiserror::Error; +use std::path::Path; + +/// The status code returned from `ouch` on error +pub const EXIT_FAILURE: i32 = libc::EXIT_FAILURE; + /// Returns true if Landlock is supported by the running kernel (Linux kernel >= 5.19). #[cfg(target_os = "linux")] pub fn is_landlock_supported() -> bool { @@ -43,24 +48,58 @@ pub enum MyRestrictError { /// /// The Landlock ABI is set to v2 for compatibility with Linux 5.19+. /// All hierarchies are given full access, but root ("/") is read-only. -pub fn restrict_paths(hierarchies: &[&str]) -> Result { +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() + + let mut ruleset = Ruleset::default() .handle_access(access_all)? .create()? // Read-only access to / (entire filesystem). - .add_rules(landlock::path_beneath_rules(&["/"], access_read))? - .add_rules( + .add_rules(landlock::path_beneath_rules(&["/"], access_read))?; + + // Add write permissions to specified directory of provided + if !hierarchies.is_empty() { + ruleset = ruleset.add_rules( hierarchies .iter() .map::, _>(|p| { Ok(PathBeneath::new(PathFd::new(p)?, access_all)) }), - )? - .restrict_self()?) -} \ No newline at end of file + )?; + } + + Ok(ruleset.restrict_self()?) +} + + +pub fn init_sandbox(allowed_dir: &Path) { + + if std::env::var("CI").is_ok() { + return; + } + + + if is_landlock_supported() { + + let path_str = allowed_dir.to_str().expect("Cannot convert path"); + + match 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)."); + } + +} + From bf22fdaf50206f02c611357b955e1eb7aeeef271 Mon Sep 17 00:00:00 2001 From: valoq Date: Sun, 29 Jun 2025 12:24:54 +0200 Subject: [PATCH 04/22] complete decompress and list commands --- src/commands/decompress.rs | 2 +- src/commands/list.rs | 2 +- src/utils/landlock.rs | 20 +++++++++++++------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index 0885f8c..bbfc0ef 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -319,7 +319,7 @@ fn execute_decompression( ) -> crate::Result> { // init landlock sandbox to restrict file system write access to output_dir - landlock::init_sandbox(output_dir); + landlock::init_sandbox(Some(output_dir)); if is_smart_unpack { return smart_unpack(unpack_fn, output_dir, output_file_path, question_policy); diff --git a/src/commands/list.rs b/src/commands/list.rs index 4fafec7..5042958 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -27,7 +27,7 @@ pub fn list_archive_contents( // Initialize landlock sandbox with empty write path // This allows only read access to the filesystem - //landlock::init_sandbox(None); + landlock::init_sandbox(None); let reader = fs::File::open(archive_path)?; diff --git a/src/utils/landlock.rs b/src/utils/landlock.rs index e7f256c..f9d11a5 100644 --- a/src/utils/landlock.rs +++ b/src/utils/landlock.rs @@ -77,7 +77,7 @@ fn restrict_paths(hierarchies: &[&str]) -> Result) { if std::env::var("CI").is_ok() { return; @@ -85,21 +85,27 @@ pub fn init_sandbox(allowed_dir: &Path) { if is_landlock_supported() { + let status = if let Some(allowed_dir) = allowed_dir { + let path_str = allowed_dir.to_str().expect("Cannot convert path"); + restrict_paths(&[path_str]) + } else { + restrict_paths(&[]) + }; - let path_str = allowed_dir.to_str().expect("Cannot convert path"); - - match restrict_paths(&[path_str]) { - Ok(status) => { + match status { + Ok(_status) => { //check } - Err(e) => { + Err(_e) => { //log warning std::process::exit(EXIT_FAILURE); } } } else { -// warn!("Landlock is NOT supported on this platform or kernel (<5.19)."); + // warn!("Landlock is NOT supported on this platform or kernel (<5.19)."); } + + } From 3799f9ee5b01bbad2448d7446d2d18e54f1bf84c Mon Sep 17 00:00:00 2001 From: valoq Date: Sun, 29 Jun 2025 13:26:10 +0200 Subject: [PATCH 05/22] fix edge cases --- src/commands/decompress.rs | 5 +++++ src/commands/list.rs | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index bbfc0ef..ef11dd7 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -319,6 +319,8 @@ fn execute_decompression( ) -> crate::Result> { // init landlock sandbox to restrict file system write access to output_dir + // The output directory iseither specified with the -d option or the current working directory is used + // TODO: restrict acess to the current working directory to allow only creating new files landlock::init_sandbox(Some(output_dir)); if is_smart_unpack { @@ -384,6 +386,9 @@ fn smart_unpack( nice_directory_display(temp_dir_path) )); + //first attempt to restict to the tmp file and allow only to rename it in the parent + //landlock::init_sandbox(Some(temp_dir_path)); + let files = unpack_fn(temp_dir_path)?; let root_contains_only_one_element = fs::read_dir(temp_dir_path)?.take(2).count() == 1; diff --git a/src/commands/list.rs b/src/commands/list.rs index 5042958..1b43c9b 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -25,9 +25,8 @@ pub fn list_archive_contents( password: Option<&[u8]>, ) -> crate::Result<()> { - // Initialize landlock sandbox with empty write path - // This allows only read access to the filesystem - landlock::init_sandbox(None); + // Initialize landlock sandbox with write access restricted to /tmp as required by some formats + landlock::init_sandbox(Some(Path::new("/tmp"))); let reader = fs::File::open(archive_path)?; From 639fab3476852edf7251f83f3be5fc7a264ab737 Mon Sep 17 00:00:00 2001 From: valoq Date: Sun, 29 Jun 2025 13:33:41 +0200 Subject: [PATCH 06/22] remove ugly hack --- src/cli/args.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index f3b72c9..b28156c 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -49,8 +49,6 @@ 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, @@ -157,7 +155,6 @@ 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()], From 1a8698a64c120471b209e116cda6583a8da0ae5c Mon Sep 17 00:00:00 2001 From: valoq Date: Sun, 29 Jun 2025 13:48:09 +0200 Subject: [PATCH 07/22] remove CI exception --- src/utils/landlock.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/landlock.rs b/src/utils/landlock.rs index f9d11a5..e18a57f 100644 --- a/src/utils/landlock.rs +++ b/src/utils/landlock.rs @@ -79,9 +79,9 @@ fn restrict_paths(hierarchies: &[&str]) -> Result) { - if std::env::var("CI").is_ok() { - return; - } +// if std::env::var("CI").is_ok() { +// return; +// } if is_landlock_supported() { From 1af69600ad242b84be62702cb155d3a0a5c1551d Mon Sep 17 00:00:00 2001 From: valoq Date: Sun, 29 Jun 2025 14:22:28 +0200 Subject: [PATCH 08/22] improve list restrictions --- src/commands/list.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/commands/list.rs b/src/commands/list.rs index 1b43c9b..91b03a7 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -25,8 +25,11 @@ pub fn list_archive_contents( password: Option<&[u8]>, ) -> crate::Result<()> { - // Initialize landlock sandbox with write access restricted to /tmp as required by some formats - landlock::init_sandbox(Some(Path::new("/tmp"))); + //rar uses a temporary file which needs to be defined early to be permitted in landlock + let mut temp_file = tempfile::NamedTempFile::new()?; + + // Initialize landlock sandbox with write access restricted to /tmp/ as required by some formats + landlock::init_sandbox(Some(temp_file.path())); let reader = fs::File::open(archive_path)?; @@ -104,7 +107,6 @@ pub fn list_archive_contents( #[cfg(feature = "unrar")] Rar => { if formats.len() > 1 { - let mut temp_file = tempfile::NamedTempFile::new()?; io::copy(&mut reader, &mut temp_file)?; Box::new(crate::archive::rar::list_archive(temp_file.path(), password)?) } else { From 6ccc7a39729e454bd323bb3794de09adca028362 Mon Sep 17 00:00:00 2001 From: valoq Date: Sun, 29 Jun 2025 15:48:53 +0200 Subject: [PATCH 09/22] allow multiple paths --- src/commands/decompress.rs | 12 +++- src/commands/list.rs | 2 +- src/utils/landlock.rs | 28 ++++---- src/utils/src_utils_landlock.rs | 109 ++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 18 deletions(-) create mode 100644 src/utils/src_utils_landlock.rs diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index ef11dd7..b94e45f 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -321,9 +321,17 @@ fn execute_decompression( // init landlock sandbox to restrict file system write access to output_dir // The output directory iseither specified with the -d option or the current working directory is used // TODO: restrict acess to the current working directory to allow only creating new files - landlock::init_sandbox(Some(output_dir)); + + // TODO: move to unpack and smart_unpack to cover the differetn dirctories used for + // decompression - if is_smart_unpack { + //if !input_is_stdin && options.remove { + //permit write access to input_file_path + //} else { + landlock::init_sandbox(&[output_dir]); + //} + + if is_smart_unpack { return smart_unpack(unpack_fn, output_dir, output_file_path, question_policy); } diff --git a/src/commands/list.rs b/src/commands/list.rs index 91b03a7..df6494a 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -29,7 +29,7 @@ pub fn list_archive_contents( let mut temp_file = tempfile::NamedTempFile::new()?; // Initialize landlock sandbox with write access restricted to /tmp/ as required by some formats - landlock::init_sandbox(Some(temp_file.path())); + landlock::init_sandbox(&[temp_file.path()]); let reader = fs::File::open(archive_path)?; diff --git a/src/utils/landlock.rs b/src/utils/landlock.rs index e18a57f..f20b1d9 100644 --- a/src/utils/landlock.rs +++ b/src/utils/landlock.rs @@ -55,7 +55,6 @@ fn restrict_paths(hierarchies: &[&str]) -> Result Result) { - -// if std::env::var("CI").is_ok() { -// return; -// } - +/// Restricts the process to only access the given hierarchies using Landlock, if supported. +/// Accepts multiple allowed directories as &[&Path]. +pub fn init_sandbox(allowed_dirs: &[&Path]) { + // if std::env::var("CI").is_ok() { + // return; + // } if is_landlock_supported() { - let status = if let Some(allowed_dir) = allowed_dir { - let path_str = allowed_dir.to_str().expect("Cannot convert path"); - restrict_paths(&[path_str]) + let paths: Vec<&str> = allowed_dirs + .iter() + .map(|p| p.to_str().expect("Cannot convert path")) + .collect(); + + let status = if !paths.is_empty() { + restrict_paths(&paths) } else { restrict_paths(&[]) }; @@ -104,8 +106,4 @@ pub fn init_sandbox(allowed_dir: Option<&Path>) { } else { // warn!("Landlock is NOT supported on this platform or kernel (<5.19)."); } - - - } - diff --git a/src/utils/src_utils_landlock.rs b/src/utils/src_utils_landlock.rs new file mode 100644 index 0000000..0fa9d76 --- /dev/null +++ b/src/utils/src_utils_landlock.rs @@ -0,0 +1,109 @@ +// Landlock support and generic Landlock sandbox 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; + +use std::path::Path; + +/// The status code returned from `ouch` on error +pub const EXIT_FAILURE: i32 = libc::EXIT_FAILURE; + +/// Returns true if Landlock is supported by the running kernel (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 +} + +#[derive(Debug, Error)] +pub enum MyRestrictError { + #[error(transparent)] + Ruleset(#[from] RulesetError), + #[error(transparent)] + AddRule(#[from] PathFdError), +} + +/// Restricts the process to only access the given hierarchies using Landlock, if supported. +/// +/// The Landlock ABI is set to v2 for compatibility with Linux 5.19+. +/// All hierarchies are given full access, but root ("/") is read-only. +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); + + let mut ruleset = Ruleset::default() + .handle_access(access_all)? + .create()? + // Read-only access to / (entire filesystem). + .add_rules(landlock::path_beneath_rules(&["/"], access_read))?; + + // Add write permissions to specified directory of provided + if !hierarchies.is_empty() { + ruleset = ruleset.add_rules( + hierarchies + .iter() + .map::, _>(|p| { + Ok(PathBeneath::new(PathFd::new(p)?, access_all)) + }), + )?; + } + + Ok(ruleset.restrict_self()?) +} + +/// Restricts the process to only access the given hierarchies using Landlock, if supported. +/// Accepts multiple allowed directories as &[&Path]. +pub fn init_sandbox(allowed_dirs: &[&Path]) { + // if std::env::var("CI").is_ok() { + // return; + // } + + if is_landlock_supported() { + let paths: Vec<&str> = allowed_dirs + .iter() + .map(|p| p.to_str().expect("Cannot convert path")) + .collect(); + + let status = if !paths.is_empty() { + restrict_paths(&paths) + } else { + restrict_paths(&[]) + }; + + match status { + 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)."); + } +} \ No newline at end of file From 7f0d1e72ec9192391e7cae4f517b3db9d4490133 Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 16:58:21 +0200 Subject: [PATCH 10/22] add disable-sandbox option --- src/cli/args.rs | 4 ++++ src/commands/decompress.rs | 6 ++++-- src/commands/list.rs | 3 ++- src/commands/mod.rs | 2 ++ src/utils/landlock.rs | 7 ++++++- 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index b28156c..69021d3 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -49,6 +49,10 @@ pub struct CliArgs { #[arg(short = 'c', long, global = true)] pub threads: Option, + /// Disable the sandbox feature + #[arg(long, global = true)] + pub no_sandbox: bool, + // Ouch and claps subcommands #[command(subcommand)] pub cmd: Subcommand, diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index b94e45f..16af3dd 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -328,10 +328,12 @@ fn execute_decompression( //if !input_is_stdin && options.remove { //permit write access to input_file_path //} else { - landlock::init_sandbox(&[output_dir]); //} - if is_smart_unpack { + //landlock::init_sandbox(&[output_dir]); + + + if is_smart_unpack { return smart_unpack(unpack_fn, output_dir, output_file_path, question_policy); } diff --git a/src/commands/list.rs b/src/commands/list.rs index df6494a..e5c1e0d 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -23,13 +23,14 @@ pub fn list_archive_contents( list_options: ListOptions, question_policy: QuestionPolicy, password: Option<&[u8]>, + disable_sandbox: bool, ) -> crate::Result<()> { //rar uses a temporary file which needs to be defined early to be permitted in landlock let mut temp_file = tempfile::NamedTempFile::new()?; // Initialize landlock sandbox with write access restricted to /tmp/ as required by some formats - landlock::init_sandbox(&[temp_file.path()]); + landlock::init_sandbox(&[temp_file.path()], disable_sandbox); let reader = fs::File::open(archive_path)?; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4a81d85..b4aa4d5 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -257,7 +257,9 @@ pub fn run( args.password .as_deref() .map(|str| <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")), + args.no_sandbox, )?; + } Ok(()) diff --git a/src/utils/landlock.rs b/src/utils/landlock.rs index f20b1d9..2c28723 100644 --- a/src/utils/landlock.rs +++ b/src/utils/landlock.rs @@ -77,10 +77,15 @@ fn restrict_paths(hierarchies: &[&str]) -> Result = allowed_dirs From 78b993c500ccfd85525cb53f71b575059819ac61 Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 17:25:25 +0200 Subject: [PATCH 11/22] complete disable-sandbox option --- src/cli/args.rs | 2 +- src/commands/decompress.rs | 9 ++++++++- src/commands/mod.rs | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index 69021d3..ecf1f6d 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -51,7 +51,7 @@ pub struct CliArgs { /// Disable the sandbox feature #[arg(long, global = true)] - pub no_sandbox: bool, + pub disable_sandbox: bool, // Ouch and claps subcommands #[command(subcommand)] diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index 16af3dd..f90ff00 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -40,6 +40,7 @@ pub struct DecompressOptions<'a> { pub quiet: bool, pub password: Option<&'a [u8]>, pub remove: bool, + pub disable_sandbox: bool, } /// Decompress a file @@ -80,6 +81,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { options.question_policy, options.is_output_dir_provided, options.is_smart_unpack, + options.disable_sandbox, )? { files } else { @@ -169,6 +171,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { options.question_policy, options.is_output_dir_provided, options.is_smart_unpack, + options.disable_sandbox, )? { files } else { @@ -204,6 +207,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { options.question_policy, options.is_output_dir_provided, options.is_smart_unpack, + options.disable_sandbox, )? { files } else { @@ -237,6 +241,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { options.question_policy, options.is_output_dir_provided, options.is_smart_unpack, + options.disable_sandbox, )? { files } else { @@ -280,6 +285,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { options.question_policy, options.is_output_dir_provided, options.is_smart_unpack, + options.disable_sandbox, )? { files } else { @@ -316,6 +322,7 @@ fn execute_decompression( question_policy: QuestionPolicy, is_output_dir_provided: bool, is_smart_unpack: bool, + disable_sandbox: bool, ) -> crate::Result> { // init landlock sandbox to restrict file system write access to output_dir @@ -330,7 +337,7 @@ fn execute_decompression( //} else { //} - //landlock::init_sandbox(&[output_dir]); + landlock::init_sandbox(&[output_dir], disable_sandbox); if is_smart_unpack { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b4aa4d5..52168f6 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -216,6 +216,7 @@ pub fn run( <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed") }), remove, + disable_sandbox: args.disable_sandbox, }) }) } @@ -257,7 +258,7 @@ pub fn run( args.password .as_deref() .map(|str| <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")), - args.no_sandbox, + args.disable_sandbox, )?; } From e455e8193955dbb2a1a5e8222cf8494050feae63 Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 17:29:19 +0200 Subject: [PATCH 12/22] test the new option by tricking the ci --- src/cli/args.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli/args.rs b/src/cli/args.rs index ecf1f6d..8d8d269 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -159,6 +159,7 @@ mod tests { // This is usually replaced in assertion tests password: None, threads: None, + disable_sandbox: true, cmd: Subcommand::Decompress { // Put a crazy value here so no test can assert it unintentionally files: vec!["\x00\x11\x22".into()], From bfe773f9fba5311acdeae7290f27ac67add010e3 Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 17:52:05 +0200 Subject: [PATCH 13/22] fix tests --- src/cli/args.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cli/args.rs b/src/cli/args.rs index 8d8d269..a7c719c 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -180,6 +180,7 @@ mod tests { output_dir: None, remove: false, no_smart_unpack: false, + disable_sandbox: true, }, ..mock_cli_args() } @@ -192,6 +193,7 @@ mod tests { output_dir: None, remove: false, no_smart_unpack: false, + disable_sandbox: true, }, ..mock_cli_args() } @@ -204,6 +206,7 @@ mod tests { output_dir: None, remove: false, no_smart_unpack: false, + disable_sandbox: true, }, ..mock_cli_args() } @@ -219,6 +222,7 @@ mod tests { fast: false, slow: false, follow_symlinks: false, + disable_sandbox: true, }, ..mock_cli_args() } From 3633b383ff71d1a83b75d8a88de3184ce099ae9a Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 17:57:11 +0200 Subject: [PATCH 14/22] fix subcommand arguments --- src/cli/args.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cli/args.rs b/src/cli/args.rs index a7c719c..dde09ad 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -108,6 +108,10 @@ pub enum Subcommand { /// Disable Smart Unpack #[arg(long)] no_smart_unpack: bool, + + /// Mark sandbox as disabled + #[arg(long, global = true)] + disable_sandbox: bool, }, /// List contents of an archive #[command(visible_aliases = ["l", "ls"])] From 133f46a2c8f9175ee228467f112ddbe2887fc04e Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 18:08:39 +0200 Subject: [PATCH 15/22] add missing parameter --- src/commands/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 52168f6..780bda0 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -151,6 +151,7 @@ pub fn run( output_dir, remove, no_smart_unpack, + disable_sandbox, } => { let mut output_paths = vec![]; let mut formats = vec![]; From a4ff65f44cbb1dd84cda3dd929aa80b8cbc10bf0 Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 18:20:15 +0200 Subject: [PATCH 16/22] improve code structure --- src/commands/decompress.rs | 10 +++------- src/commands/list.rs | 5 ++--- src/commands/mod.rs | 2 -- src/main.rs | 6 +----- src/utils/landlock.rs | 4 ++-- src/utils/mod.rs | 2 +- 6 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index f90ff00..096c3de 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -5,7 +5,7 @@ use std::{ }; use fs_err as fs; -use crate::utils::landlock; +//use crate::utils::landlock; #[cfg(not(feature = "bzip3"))] use crate::archive; @@ -19,7 +19,7 @@ use crate::{ utils::{ self, io::lock_and_flush_output_stdio, - is_path_stdin, + is_path_stdin, landlock, logger::{info, info_accessible}, nice_directory_display, user_wants_to_continue, }, @@ -324,21 +324,17 @@ fn execute_decompression( is_smart_unpack: bool, disable_sandbox: bool, ) -> crate::Result> { - // init landlock sandbox to restrict file system write access to output_dir // The output directory iseither specified with the -d option or the current working directory is used // TODO: restrict acess to the current working directory to allow only creating new files - // TODO: move to unpack and smart_unpack to cover the differetn dirctories used for // decompression - //if !input_is_stdin && options.remove { //permit write access to input_file_path //} else { //} - + landlock::init_sandbox(&[output_dir], disable_sandbox); - if is_smart_unpack { return smart_unpack(unpack_fn, output_dir, output_file_path, question_policy); diff --git a/src/commands/list.rs b/src/commands/list.rs index e5c1e0d..7dd956e 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -4,14 +4,14 @@ use std::{ }; use fs_err as fs; -use crate::utils::landlock; +//use crate::utils::landlock; use crate::{ archive, commands::warn_user_about_loading_zip_in_memory, extension::CompressionFormat::{self, *}, list::{self, FileInArchive, ListOptions}, - utils::{io::lock_and_flush_output_stdio, user_wants_to_continue}, + utils::{io::lock_and_flush_output_stdio, user_wants_to_continue, landlock}, QuestionAction, QuestionPolicy, BUFFER_CAPACITY, }; @@ -25,7 +25,6 @@ pub fn list_archive_contents( password: Option<&[u8]>, disable_sandbox: bool, ) -> crate::Result<()> { - //rar uses a temporary file which needs to be defined early to be permitted in landlock let mut temp_file = tempfile::NamedTempFile::new()?; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 780bda0..4579ef3 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -261,9 +261,7 @@ pub fn run( .map(|str| <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")), args.disable_sandbox, )?; - } - Ok(()) } } diff --git a/src/main.rs b/src/main.rs index 4375188..f282dcf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,9 +21,6 @@ use self::{ }, }; -//use utils::landlock::*; - - // Used in BufReader and BufWriter to perform less syscalls const BUFFER_CAPACITY: usize = 1024 * 32; @@ -51,7 +48,7 @@ fn run() -> Result<()> { //let working_dir = args.output_dir // .clone() // .unwrap_or_else(|| env::current_dir().unwrap_or_default()); - + // restrict filesystem access to working_dir; // 1. working_dir is either the output_dir specified by the -d option or // 2. it is the temporary .tmp-ouch-XXXXXX directory that is renamed after decompression @@ -64,7 +61,6 @@ fn run() -> Result<()> { // Since either the specified output directory is created if it did not exist, or the .ouch-tmp // directory is created in the current working directory, the parent directory of the target // directory requires LANDLOCK_ACCESS_FS_MAKE_DIR - // expects either the .tmp-ouch-XXXXXX path or the specified output directory (-d option) //utils::landlock::init_sandbox(&working_dir); diff --git a/src/utils/landlock.rs b/src/utils/landlock.rs index 2c28723..ffb4400 100644 --- a/src/utils/landlock.rs +++ b/src/utils/landlock.rs @@ -1,14 +1,14 @@ // Landlock support and generic Landlock sandbox implementation. // https://landlock.io/rust-landlock/landlock/struct.Ruleset.html +use std::path::Path; + use landlock::{ Access, AccessFs, PathBeneath, PathFd, PathFdError, RestrictionStatus, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetError, ABI, }; use thiserror::Error; -use std::path::Path; - /// The status code returned from `ouch` on error pub const EXIT_FAILURE: i32 = libc::EXIT_FAILURE; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 611ee89..126aa0f 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -8,9 +8,9 @@ mod file_visibility; mod formatting; mod fs; pub mod io; +pub mod landlock; pub mod logger; mod question; -pub mod landlock; pub use self::{ file_visibility::FileVisibilityPolicy, From 3a8cc9edda1045ab690d859cf73ed763c732cafd Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 18:26:29 +0200 Subject: [PATCH 17/22] fix subcommand --- src/cli/args.rs | 3 ++- src/commands/list.rs | 2 +- src/main.rs | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index dde09ad..bf31e5e 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -108,7 +108,7 @@ pub enum Subcommand { /// Disable Smart Unpack #[arg(long)] no_smart_unpack: bool, - + /// Mark sandbox as disabled #[arg(long, global = true)] disable_sandbox: bool, @@ -280,6 +280,7 @@ mod tests { fast: false, slow: false, follow_symlinks: false, + disable_sandbox: true, }, format: Some("tar.gz".into()), ..mock_cli_args() diff --git a/src/commands/list.rs b/src/commands/list.rs index 7dd956e..3461b15 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -11,7 +11,7 @@ use crate::{ commands::warn_user_about_loading_zip_in_memory, extension::CompressionFormat::{self, *}, list::{self, FileInArchive, ListOptions}, - utils::{io::lock_and_flush_output_stdio, user_wants_to_continue, landlock}, + utils::{io::lock_and_flush_output_stdio, landlock, user_wants_to_continue}, QuestionAction, QuestionPolicy, BUFFER_CAPACITY, }; diff --git a/src/main.rs b/src/main.rs index f282dcf..927e309 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,7 +48,6 @@ fn run() -> Result<()> { //let working_dir = args.output_dir // .clone() // .unwrap_or_else(|| env::current_dir().unwrap_or_default()); - // restrict filesystem access to working_dir; // 1. working_dir is either the output_dir specified by the -d option or // 2. it is the temporary .tmp-ouch-XXXXXX directory that is renamed after decompression From 1034130d5dbbc1d9e90cf972b52e8d642a9b8d73 Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 18:30:58 +0200 Subject: [PATCH 18/22] complete subcommands --- src/cli/args.rs | 2 ++ src/commands/compress.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/cli/args.rs b/src/cli/args.rs index bf31e5e..81e8b8e 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -241,6 +241,7 @@ mod tests { fast: false, slow: false, follow_symlinks: false, + disable_sandbox: true, }, ..mock_cli_args() } @@ -255,6 +256,7 @@ mod tests { fast: false, slow: false, follow_symlinks: false, + disable_sandbox: true, }, ..mock_cli_args() } diff --git a/src/commands/compress.rs b/src/commands/compress.rs index 15880bd..aa81efb 100644 --- a/src/commands/compress.rs +++ b/src/commands/compress.rs @@ -35,6 +35,7 @@ pub fn compress_files( question_policy: QuestionPolicy, file_visibility_policy: FileVisibilityPolicy, level: Option, + disable_sandbox: bool, ) -> crate::Result { // If the input files contain a directory, then the total size will be underestimated let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file); From 6b90deae14afb3b157d4deb24bcb418952cb74ff Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 18:41:07 +0200 Subject: [PATCH 19/22] fix mod --- src/commands/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4579ef3..15b9b65 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -116,6 +116,7 @@ pub fn run( question_policy, file_visibility_policy, level, + args.disable_sandbox, ); if let Ok(true) = compress_result { From a6cf506635b29515c6a9ff5d669bb75dc5a68af8 Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 18:48:59 +0200 Subject: [PATCH 20/22] fix subcommand --- src/cli/args.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/cli/args.rs b/src/cli/args.rs index 81e8b8e..9246d9f 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -89,6 +89,10 @@ pub enum Subcommand { /// Archive target files instead of storing symlinks (supported by `tar` and `zip`) #[arg(long, short = 'S')] follow_symlinks: bool, + + /// Mark sandbox as disabled + #[arg(long, global = true)] + disable_sandbox: bool, }, /// Decompresses one or more files, optionally into another folder #[command(visible_alias = "d")] @@ -123,6 +127,10 @@ pub enum Subcommand { /// Show archive contents as a tree #[arg(short, long)] tree: bool, + + /// Mark sandbox as disabled + #[arg(long, global = true)] + disable_sandbox: bool, }, } @@ -170,6 +178,7 @@ mod tests { output_dir: None, remove: false, no_smart_unpack: false, + disable_sandbox: true, }, } } From b019ffb8a6b0e15249cd6c8e9c2781482fb46e5e Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 18:55:10 +0200 Subject: [PATCH 21/22] testing --- src/commands/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 15b9b65..629e0f0 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -69,6 +69,7 @@ pub fn run( fast, slow, follow_symlinks, + disable_sandbox, } => { // After cleaning, if there are no input files left, exit if files.is_empty() { @@ -222,7 +223,8 @@ pub fn run( }) }) } - Subcommand::List { archives: files, tree } => { + // check again if we need to provide disable_sandbox as argument here + Subcommand::List { archives: files, tree, disable_sandbox} => { let mut formats = vec![]; if let Some(format) = args.format { From f85080dc70a4a247eb3af72f54b061424af3ba31 Mon Sep 17 00:00:00 2001 From: valoq Date: Sat, 12 Jul 2025 19:05:37 +0200 Subject: [PATCH 22/22] fix tests --- src/cli/args.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index 9246d9f..df80379 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -171,14 +171,14 @@ mod tests { // This is usually replaced in assertion tests password: None, threads: None, - disable_sandbox: true, + disable_sandbox: false, cmd: Subcommand::Decompress { // Put a crazy value here so no test can assert it unintentionally files: vec!["\x00\x11\x22".into()], output_dir: None, remove: false, no_smart_unpack: false, - disable_sandbox: true, + disable_sandbox: false, }, } } @@ -193,7 +193,7 @@ mod tests { output_dir: None, remove: false, no_smart_unpack: false, - disable_sandbox: true, + disable_sandbox: false, }, ..mock_cli_args() } @@ -206,7 +206,7 @@ mod tests { output_dir: None, remove: false, no_smart_unpack: false, - disable_sandbox: true, + disable_sandbox: false, }, ..mock_cli_args() } @@ -219,7 +219,7 @@ mod tests { output_dir: None, remove: false, no_smart_unpack: false, - disable_sandbox: true, + disable_sandbox: false, }, ..mock_cli_args() } @@ -235,7 +235,7 @@ mod tests { fast: false, slow: false, follow_symlinks: false, - disable_sandbox: true, + disable_sandbox: false, }, ..mock_cli_args() } @@ -250,7 +250,7 @@ mod tests { fast: false, slow: false, follow_symlinks: false, - disable_sandbox: true, + disable_sandbox: false, }, ..mock_cli_args() } @@ -265,7 +265,7 @@ mod tests { fast: false, slow: false, follow_symlinks: false, - disable_sandbox: true, + disable_sandbox: false, }, ..mock_cli_args() } @@ -291,7 +291,7 @@ mod tests { fast: false, slow: false, follow_symlinks: false, - disable_sandbox: true, + disable_sandbox: false, }, format: Some("tar.gz".into()), ..mock_cli_args()