Skip to content

Commit b6c5dd1

Browse files
authored
coreutils: Protect against env -a for security (#10773)
This prevents an attacker from spoofing argv[0] to bypass apparmor restrictions. - `env -a false ls` now correctly runs `ls` instead of dispatching as `false` - Also works under masked `/proc` (does not rely on /proc/self/exe). Closes #10135
1 parent 4552c0f commit b6c5dd1

File tree

5 files changed

+52
-5
lines changed

5 files changed

+52
-5
lines changed

.vscode/cspell.dictionaries/workspace.wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ uutests
366366
uutils
367367

368368
# * function names
369+
execfn
369370
getcwd
370371
setpipe
371372

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# coreutils (uutils)
22
# * see the repository LICENSE, README, and CONTRIBUTING files for more information
33

4-
# spell-checker:ignore (libs) bigdecimal datetime foldhash serde gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner
4+
# spell-checker:ignore (libs) bigdecimal datetime foldhash serde gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner getauxval
55

66
[package]
77
name = "coreutils"
@@ -454,7 +454,9 @@ rstest = "0.26.0"
454454
rstest_reuse = "0.7.0"
455455
rustc-hash = "2.1.1"
456456
rust-ini = "0.21.0"
457-
rustix = "1.1.4"
457+
# binary name of coreutils can be hijacked by overriding getauxval via LD_PRELOAD
458+
# So we use param and avoid libc backend
459+
rustix = { version = "1.1.4", features = ["param"] }
458460
same-file = "1.0.6"
459461
self_cell = "1.0.4"
460462
selinux = "=0.6.0"
@@ -624,6 +626,9 @@ who = { optional = true, version = "0.8.0", package = "uu_who", path = "src/uu/w
624626
whoami = { optional = true, version = "0.8.0", package = "uu_whoami", path = "src/uu/whoami" }
625627
yes = { optional = true, version = "0.8.0", package = "uu_yes", path = "src/uu/yes" }
626628

629+
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
630+
rustix.workspace = true
631+
627632
# this breaks clippy linting with: "tests/by-util/test_factor_benches.rs: No such file or directory (os error 2)"
628633
# factor_benches = { optional = true, version = "0.0.0", package = "uu_factor_benches", path = "tests/benches/factor" }
629634

src/common/validation.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// spell-checker:ignore prefixcat testcat
6+
// spell-checker:ignore memfd_create prefixcat rsplit testcat
77

88
use std::ffi::{OsStr, OsString};
99
use std::io::{Write, stderr};
@@ -73,15 +73,41 @@ fn get_canonical_util_name(util_name: &str) -> &str {
7373
}
7474

7575
/// Gets the binary path from command line arguments
76-
/// # Panics
7776
/// Panics if the binary path cannot be determined
77+
#[cfg(not(any(target_os = "linux", target_os = "android")))]
7878
pub fn binary_path(args: &mut impl Iterator<Item = OsString>) -> PathBuf {
7979
match args.next() {
8080
Some(ref s) if !s.is_empty() => PathBuf::from(s),
81+
// the fallback is valid only for hardlinks
8182
_ => std::env::current_exe().unwrap(),
8283
}
8384
}
84-
85+
/// Get actual binary path from kernel, not argv0, to prevent `env -a` from bypassing
86+
/// AppArmor, SELinux policies on hard-linked binaries
87+
#[cfg(any(target_os = "linux", target_os = "android"))]
88+
pub fn binary_path(args: &mut impl Iterator<Item = OsString>) -> PathBuf {
89+
use std::fs::File;
90+
use std::io::Read;
91+
use std::os::unix::ffi::OsStrExt;
92+
let execfn = rustix::param::linux_execfn();
93+
let execfn_bytes = execfn.to_bytes();
94+
let exec_path = Path::new(OsStr::from_bytes(execfn_bytes));
95+
let argv0 = args.next().unwrap();
96+
let mut shebang_buf = [0u8; 2];
97+
// exec_path is wrong when called from shebang or memfd_create (/proc/self/fd/*)
98+
// argv0 is not full-path when called from PATH
99+
if execfn_bytes.rsplit(|&b| b == b'/').next() == argv0.as_bytes().rsplit(|&b| b == b'/').next()
100+
|| execfn_bytes.starts_with(b"/proc/")
101+
|| (File::open(Path::new(exec_path))
102+
.and_then(|mut f| f.read_exact(&mut shebang_buf))
103+
.is_ok()
104+
&& &shebang_buf == b"#!")
105+
{
106+
argv0.into()
107+
} else {
108+
exec_path.into()
109+
}
110+
}
85111
/// Extracts the binary name from a path
86112
pub fn name(binary_path: &Path) -> Option<&str> {
87113
binary_path.file_stem()?.to_str()

tests/test_util_name.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ fn init() {
2626
eprintln!("Setting UUTESTS_BINARY_PATH={TESTS_BINARY}");
2727
}
2828

29+
#[test]
30+
#[cfg(all(feature = "env", any(target_os = "linux", target_os = "android")))]
31+
fn binary_name_protection() {
32+
let ts = TestScenario::new("env");
33+
let bin = ts.bin_path.clone();
34+
ts.ucmd()
35+
.arg("-a")
36+
.arg("hijacked")
37+
.arg(&bin)
38+
.arg("--version")
39+
.succeeds()
40+
.stdout_contains("coreutils");
41+
}
42+
2943
#[test]
3044
#[cfg(feature = "ls")]
3145
fn execution_phrase_double() {

0 commit comments

Comments
 (0)