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
This commit is contained in:
oech3
2026-04-05 07:45:42 +09:00
committed by GitHub
parent 4552c0f9fd
commit b6c5dd1325
5 changed files with 52 additions and 5 deletions
+1
View File
@@ -366,6 +366,7 @@ uutests
uutils
# * function names
execfn
getcwd
setpipe
Generated
+1
View File
@@ -576,6 +576,7 @@ dependencies = [
"rstest",
"rstest_reuse",
"rustc-hash",
"rustix",
"selinux",
"sha1",
"tempfile",
+7 -2
View File
@@ -1,7 +1,7 @@
# coreutils (uutils)
# * see the repository LICENSE, README, and CONTRIBUTING files for more information
# spell-checker:ignore (libs) bigdecimal datetime foldhash serde gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner
# spell-checker:ignore (libs) bigdecimal datetime foldhash serde gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner getauxval
[package]
name = "coreutils"
@@ -454,7 +454,9 @@ rstest = "0.26.0"
rstest_reuse = "0.7.0"
rustc-hash = "2.1.1"
rust-ini = "0.21.0"
rustix = "1.1.4"
# binary name of coreutils can be hijacked by overriding getauxval via LD_PRELOAD
# So we use param and avoid libc backend
rustix = { version = "1.1.4", features = ["param"] }
same-file = "1.0.6"
self_cell = "1.0.4"
selinux = "=0.6.0"
@@ -624,6 +626,9 @@ who = { optional = true, version = "0.8.0", package = "uu_who", path = "src/uu/w
whoami = { optional = true, version = "0.8.0", package = "uu_whoami", path = "src/uu/whoami" }
yes = { optional = true, version = "0.8.0", package = "uu_yes", path = "src/uu/yes" }
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
rustix.workspace = true
# this breaks clippy linting with: "tests/by-util/test_factor_benches.rs: No such file or directory (os error 2)"
# factor_benches = { optional = true, version = "0.0.0", package = "uu_factor_benches", path = "tests/benches/factor" }
+29 -3
View File
@@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore prefixcat testcat
// spell-checker:ignore memfd_create prefixcat rsplit testcat
use std::ffi::{OsStr, OsString};
use std::io::{Write, stderr};
@@ -73,15 +73,41 @@ fn get_canonical_util_name(util_name: &str) -> &str {
}
/// Gets the binary path from command line arguments
/// # Panics
/// Panics if the binary path cannot be determined
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn binary_path(args: &mut impl Iterator<Item = OsString>) -> PathBuf {
match args.next() {
Some(ref s) if !s.is_empty() => PathBuf::from(s),
// the fallback is valid only for hardlinks
_ => std::env::current_exe().unwrap(),
}
}
/// Get actual binary path from kernel, not argv0, to prevent `env -a` from bypassing
/// AppArmor, SELinux policies on hard-linked binaries
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn binary_path(args: &mut impl Iterator<Item = OsString>) -> PathBuf {
use std::fs::File;
use std::io::Read;
use std::os::unix::ffi::OsStrExt;
let execfn = rustix::param::linux_execfn();
let execfn_bytes = execfn.to_bytes();
let exec_path = Path::new(OsStr::from_bytes(execfn_bytes));
let argv0 = args.next().unwrap();
let mut shebang_buf = [0u8; 2];
// exec_path is wrong when called from shebang or memfd_create (/proc/self/fd/*)
// argv0 is not full-path when called from PATH
if execfn_bytes.rsplit(|&b| b == b'/').next() == argv0.as_bytes().rsplit(|&b| b == b'/').next()
|| execfn_bytes.starts_with(b"/proc/")
|| (File::open(Path::new(exec_path))
.and_then(|mut f| f.read_exact(&mut shebang_buf))
.is_ok()
&& &shebang_buf == b"#!")
{
argv0.into()
} else {
exec_path.into()
}
}
/// Extracts the binary name from a path
pub fn name(binary_path: &Path) -> Option<&str> {
binary_path.file_stem()?.to_str()
+14
View File
@@ -26,6 +26,20 @@ fn init() {
eprintln!("Setting UUTESTS_BINARY_PATH={TESTS_BINARY}");
}
#[test]
#[cfg(all(feature = "env", any(target_os = "linux", target_os = "android")))]
fn binary_name_protection() {
let ts = TestScenario::new("env");
let bin = ts.bin_path.clone();
ts.ucmd()
.arg("-a")
.arg("hijacked")
.arg(&bin)
.arg("--version")
.succeeds()
.stdout_contains("coreutils");
}
#[test]
#[cfg(feature = "ls")]
fn execution_phrase_double() {