Skip to content

Commit 2d60946

Browse files
oech3Ecordonnier
andcommitted
coreutils: Protect against env -a for security
Co-authored-by: Etienne Cordonnier <ecordonnier@snap.com>
1 parent d202b45 commit 2d60946

File tree

5 files changed

+53
-4
lines changed

5 files changed

+53
-4
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ advapi32-sys
88
aho-corasick
99
backtrace
1010
blake2b_simd
11+
rustix
1112

1213
# * uutils project
1314
uutils
@@ -360,6 +361,7 @@ uutests
360361
uutils
361362

362363
# * function names
364+
execfn
363365
getcwd
364366

365367
# * other

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 & 1 deletion
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"
@@ -428,6 +428,9 @@ rlimit = "0.11.0"
428428
rstest = "0.26.0"
429429
rustc-hash = "2.1.1"
430430
rust-ini = "0.21.0"
431+
# binary name of coreutils can be hijacked by overriding getauxval via LD_PRELOAD
432+
# So we use param and avoid libc backend
433+
rustix = { version = "1.1.4", features = ["param"] }
431434
same-file = "1.0.6"
432435
self_cell = "1.0.4"
433436
selinux = "=0.6.0"
@@ -595,6 +598,9 @@ who = { optional = true, version = "0.7.0", package = "uu_who", path = "src/uu/w
595598
whoami = { optional = true, version = "0.7.0", package = "uu_whoami", path = "src/uu/whoami" }
596599
yes = { optional = true, version = "0.7.0", package = "uu_yes", path = "src/uu/yes" }
597600

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

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)