Skip to content

Commit 0652a62

Browse files
committed
add landlock support
1 parent bbce746 commit 0652a62

File tree

11 files changed

+299
-4
lines changed

11 files changed

+299
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Categories Used:
2727
- Provide Nushell completions (packages still need to install them) [\#827](https://github.com/ouch-org/ouch/pull/827) ([FrancescElies](https://github.com/FrancescElies))
2828
- Support `.lz` decompression [\#838](https://github.com/ouch-org/ouch/pull/838) ([zzzsyyy](https://github.com/zzzsyyy))
2929
- Support `.lzma` decompression (and fix `.lzma` being a wrong alias for `.xz`) [\#838](https://github.com/ouch-org/ouch/pull/838) ([zzzsyyy](https://github.com/zzzsyyy))
30+
- Add landlock support for linux filesystem isolation [\#723](https://github.com/ouch-org/ouch/pull/723) ([valoq](https://github.com/valoq))
3031

3132
### Improvements
3233

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ gzp = { version = "0.11.3", default-features = false, features = [
2828
"snappy_default",
2929
] }
3030
ignore = "0.4.23"
31+
landlock = "0.4.2"
3132
libc = "0.2.155"
3233
linked-hash-map = "0.5.6"
3334
lz4_flex = "0.11.3"
@@ -39,6 +40,7 @@ sevenz-rust2 = { version = "0.13.1", features = ["compress", "aes256"] }
3940
snap = "1.1.1"
4041
tar = "0.4.42"
4142
tempfile = "3.10.1"
43+
thiserror = "2.0.12"
4244
time = { version = "0.3.36", default-features = false }
4345
unrar = { version = "0.5.7", optional = true }
4446
liblzma = "0.4"

src/cli/args.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ pub struct CliArgs {
4949
#[arg(short = 'c', long, global = true)]
5050
pub threads: Option<usize>,
5151

52+
/// Disable the sandbox feature
53+
#[arg(long, global = true)]
54+
pub disable_sandbox: bool,
55+
5256
// Ouch and claps subcommands
5357
#[command(subcommand)]
5458
pub cmd: Subcommand,
@@ -85,6 +89,10 @@ pub enum Subcommand {
8589
/// Archive target files instead of storing symlinks (supported by `tar` and `zip`)
8690
#[arg(long, short = 'S')]
8791
follow_symlinks: bool,
92+
93+
/// Mark sandbox as disabled
94+
#[arg(long, global = true)]
95+
disable_sandbox: bool,
8896
},
8997
/// Decompresses one or more files, optionally into another folder
9098
#[command(visible_alias = "d")]
@@ -104,6 +112,10 @@ pub enum Subcommand {
104112
/// Disable Smart Unpack
105113
#[arg(long)]
106114
no_smart_unpack: bool,
115+
116+
/// Mark sandbox as disabled
117+
#[arg(long, global = true)]
118+
disable_sandbox: bool,
107119
},
108120
/// List contents of an archive
109121
#[command(visible_aliases = ["l", "ls"])]
@@ -115,6 +127,10 @@ pub enum Subcommand {
115127
/// Show archive contents as a tree
116128
#[arg(short, long)]
117129
tree: bool,
130+
131+
/// Mark sandbox as disabled
132+
#[arg(long, global = true)]
133+
disable_sandbox: bool,
118134
},
119135
}
120136

@@ -155,12 +171,14 @@ mod tests {
155171
// This is usually replaced in assertion tests
156172
password: None,
157173
threads: None,
174+
disable_sandbox: false,
158175
cmd: Subcommand::Decompress {
159176
// Put a crazy value here so no test can assert it unintentionally
160177
files: vec!["\x00\x11\x22".into()],
161178
output_dir: None,
162179
remove: false,
163180
no_smart_unpack: false,
181+
disable_sandbox: false,
164182
},
165183
}
166184
}
@@ -175,6 +193,7 @@ mod tests {
175193
output_dir: None,
176194
remove: false,
177195
no_smart_unpack: false,
196+
disable_sandbox: false,
178197
},
179198
..mock_cli_args()
180199
}
@@ -187,6 +206,7 @@ mod tests {
187206
output_dir: None,
188207
remove: false,
189208
no_smart_unpack: false,
209+
disable_sandbox: false,
190210
},
191211
..mock_cli_args()
192212
}
@@ -199,6 +219,7 @@ mod tests {
199219
output_dir: None,
200220
remove: false,
201221
no_smart_unpack: false,
222+
disable_sandbox: false,
202223
},
203224
..mock_cli_args()
204225
}
@@ -214,6 +235,7 @@ mod tests {
214235
fast: false,
215236
slow: false,
216237
follow_symlinks: false,
238+
disable_sandbox: false,
217239
},
218240
..mock_cli_args()
219241
}
@@ -228,6 +250,7 @@ mod tests {
228250
fast: false,
229251
slow: false,
230252
follow_symlinks: false,
253+
disable_sandbox: false,
231254
},
232255
..mock_cli_args()
233256
}
@@ -242,6 +265,7 @@ mod tests {
242265
fast: false,
243266
slow: false,
244267
follow_symlinks: false,
268+
disable_sandbox: false,
245269
},
246270
..mock_cli_args()
247271
}
@@ -267,6 +291,7 @@ mod tests {
267291
fast: false,
268292
slow: false,
269293
follow_symlinks: false,
294+
disable_sandbox: false,
270295
},
271296
format: Some("tar.gz".into()),
272297
..mock_cli_args()

src/commands/compress.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub fn compress_files(
3535
question_policy: QuestionPolicy,
3636
file_visibility_policy: FileVisibilityPolicy,
3737
level: Option<i16>,
38+
disable_sandbox: bool,
3839
) -> crate::Result<bool> {
3940
// If the input files contain a directory, then the total size will be underestimated
4041
let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);

src/commands/decompress.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
utils::{
1919
self,
2020
io::lock_and_flush_output_stdio,
21-
is_path_stdin,
21+
is_path_stdin, landlock,
2222
logger::{info, info_accessible},
2323
nice_directory_display, user_wants_to_continue,
2424
},
@@ -39,6 +39,7 @@ pub struct DecompressOptions<'a> {
3939
pub quiet: bool,
4040
pub password: Option<&'a [u8]>,
4141
pub remove: bool,
42+
pub disable_sandbox: bool,
4243
}
4344

4445
/// Decompress a file
@@ -79,6 +80,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
7980
options.question_policy,
8081
options.is_output_dir_provided,
8182
options.is_smart_unpack,
83+
options.disable_sandbox,
8284
)? {
8385
files
8486
} else {
@@ -176,6 +178,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
176178
options.question_policy,
177179
options.is_output_dir_provided,
178180
options.is_smart_unpack,
181+
options.disable_sandbox,
179182
)? {
180183
files
181184
} else {
@@ -211,6 +214,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
211214
options.question_policy,
212215
options.is_output_dir_provided,
213216
options.is_smart_unpack,
217+
options.disable_sandbox,
214218
)? {
215219
files
216220
} else {
@@ -244,6 +248,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
244248
options.question_policy,
245249
options.is_output_dir_provided,
246250
options.is_smart_unpack,
251+
options.disable_sandbox,
247252
)? {
248253
files
249254
} else {
@@ -287,6 +292,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
287292
options.question_policy,
288293
options.is_output_dir_provided,
289294
options.is_smart_unpack,
295+
options.disable_sandbox,
290296
)? {
291297
files
292298
} else {
@@ -323,7 +329,20 @@ fn execute_decompression(
323329
question_policy: QuestionPolicy,
324330
is_output_dir_provided: bool,
325331
is_smart_unpack: bool,
332+
disable_sandbox: bool,
326333
) -> crate::Result<ControlFlow<(), usize>> {
334+
// init landlock sandbox to restrict file system write access to output_dir
335+
// The output directory iseither specified with the -d option or the current working directory is used
336+
// TODO: restrict acess to the current working directory to allow only creating new files
337+
// TODO: move to unpack and smart_unpack to cover the differetn dirctories used for
338+
// decompression
339+
//if !input_is_stdin && options.remove {
340+
//permit write access to input_file_path
341+
//} else {
342+
//}
343+
344+
landlock::init_sandbox(&[output_dir], disable_sandbox);
345+
327346
if is_smart_unpack {
328347
return smart_unpack(unpack_fn, output_dir, output_file_path, question_policy);
329348
}
@@ -387,6 +406,9 @@ fn smart_unpack(
387406
nice_directory_display(temp_dir_path)
388407
));
389408

409+
//first attempt to restict to the tmp file and allow only to rename it in the parent
410+
//landlock::init_sandbox(Some(temp_dir_path));
411+
390412
let files = unpack_fn(temp_dir_path)?;
391413

392414
let root_contains_only_one_element = fs::read_dir(temp_dir_path)?.take(2).count() == 1;

src/commands/list.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::{
1010
commands::warn_user_about_loading_zip_in_memory,
1111
extension::CompressionFormat::{self, *},
1212
list::{self, FileInArchive, ListOptions},
13-
utils::{io::lock_and_flush_output_stdio, user_wants_to_continue},
13+
utils::{io::lock_and_flush_output_stdio, landlock, user_wants_to_continue},
1414
QuestionAction, QuestionPolicy, BUFFER_CAPACITY,
1515
};
1616

@@ -22,7 +22,14 @@ pub fn list_archive_contents(
2222
list_options: ListOptions,
2323
question_policy: QuestionPolicy,
2424
password: Option<&[u8]>,
25+
disable_sandbox: bool,
2526
) -> crate::Result<()> {
27+
//rar uses a temporary file which needs to be defined early to be permitted in landlock
28+
let mut temp_file = tempfile::NamedTempFile::new()?;
29+
30+
// Initialize landlock sandbox with write access restricted to /tmp/<tmp_file> as required by some formats
31+
landlock::init_sandbox(&[temp_file.path()], disable_sandbox);
32+
2633
let reader = fs::File::open(archive_path)?;
2734

2835
// Zip archives are special, because they require io::Seek, so it requires it's logic separated

src/commands/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ pub fn run(
6969
fast,
7070
slow,
7171
follow_symlinks,
72+
disable_sandbox,
7273
} => {
7374
// After cleaning, if there are no input files left, exit
7475
if files.is_empty() {
@@ -116,6 +117,7 @@ pub fn run(
116117
question_policy,
117118
file_visibility_policy,
118119
level,
120+
args.disable_sandbox,
119121
);
120122

121123
if let Ok(true) = compress_result {
@@ -151,6 +153,7 @@ pub fn run(
151153
output_dir,
152154
remove,
153155
no_smart_unpack,
156+
disable_sandbox,
154157
} => {
155158
let mut output_paths = vec![];
156159
let mut formats = vec![];
@@ -216,10 +219,12 @@ pub fn run(
216219
<[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")
217220
}),
218221
remove,
222+
disable_sandbox: args.disable_sandbox,
219223
})
220224
})
221225
}
222-
Subcommand::List { archives: files, tree } => {
226+
// check again if we need to provide disable_sandbox as argument here
227+
Subcommand::List { archives: files, tree, disable_sandbox} => {
223228
let mut formats = vec![];
224229

225230
if let Some(format) = args.format {
@@ -257,9 +262,9 @@ pub fn run(
257262
args.password
258263
.as_deref()
259264
.map(|str| <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")),
265+
args.disable_sandbox,
260266
)?;
261267
}
262-
263268
Ok(())
264269
}
265270
}

0 commit comments

Comments
 (0)