use anyhow::{Context, Result, bail};
use std::ffi::OsStr;
use std::fs::{self, File, OpenOptions};
use std::os::fd::AsRawFd;
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
use std::path::{Path, PathBuf};
use std::process::Command;
const SUDO: &str = "/usr/bin/sudo";
const INSTALL: &str = "/usr/bin/install";
const RM: &str = "/usr/bin/rm";
const MKDIR: &str = "/usr/bin/mkdir";
const TOUCH: &str = "/usr/bin/touch";
const MV: &str = "/usr/bin/mv";
const CHMOD: &str = "/usr/bin/chmod";
const RESTORECON_CANDIDATES: &[&str] = &[
"/usr/sbin/restorecon",
"/usr/bin/restorecon",
"/sbin/restorecon",
];
const CANONICAL_PREFIX: &str = "/usr/local";
#[derive(Clone, Copy)]
pub enum Escalation {
Allowed,
Forbidden,
}
impl Escalation {
pub fn for_prefix(prefix: &Path) -> Self {
if prefix == Path::new(CANONICAL_PREFIX) {
Self::Allowed
} else {
Self::Forbidden
}
}
pub fn probe_destination(self, dir: &Path) -> Result<bool> {
self.escalate_for(dir)
}
fn escalate_for(self, dir: &Path) -> Result<bool> {
let needs = needs_privilege(dir);
match (self, needs) {
(_, false) => Ok(false),
(Self::Allowed, true) => Ok(true),
(Self::Forbidden, true) => bail!(
"refusing to write to {} with elevated privileges: only {CANONICAL_PREFIX} \
is supported as a privileged prefix (its parents are root-owned and cannot \
be swapped mid-operation). Use a writable --prefix such as ~/.local instead.",
dir.display()
),
}
}
}
const PROBE_ATTEMPTS: u32 = 8;
fn dir_writable(dir: &Path) -> bool {
if fs::create_dir_all(dir).is_err() {
return false;
}
let pid = std::process::id();
for attempt in 0..PROBE_ATTEMPTS {
let probe = dir.join(format!(".cargo-lbin-write-probe.{pid}.{attempt}"));
match OpenOptions::new().write(true).create_new(true).open(&probe) {
Ok(file) => {
drop(file);
let _ = fs::remove_file(&probe);
return true;
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(_) => return false,
}
}
false
}
pub fn needs_privilege(dir: &Path) -> bool {
!dir_writable(dir)
}
fn escalate_for_paths(policy: Escalation, paths: &[&Path]) -> Result<bool> {
let mut escalate = false;
for parent in paths.iter().filter_map(|p| p.parent()) {
escalate |= policy.escalate_for(parent)?;
}
Ok(escalate)
}
fn run(escalate: bool, program: &str, args: &[&OsStr]) -> Result<()> {
let spawned = if escalate {
format!("{SUDO} {program}")
} else {
program.to_owned()
};
let mut cmd = if escalate {
let mut c = Command::new(SUDO);
c.arg(program);
c
} else {
Command::new(program)
};
cmd.args(args);
let status = cmd
.status()
.with_context(|| format!("failed to spawn {spawned}"))?;
if !status.success() {
bail!("{spawned} exited with {status}");
}
Ok(())
}
#[derive(Debug)]
pub struct VerifiedSource {
file: File,
}
impl VerifiedSource {
pub fn open(path: &Path) -> Result<Self> {
let file = OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)
.with_context(|| format!("opening staged binary {}", path.display()))?;
let meta = file
.metadata()
.with_context(|| format!("fstat on staged binary {}", path.display()))?;
if !meta.is_file() {
bail!("staged {} is not a regular file", path.display());
}
let euid = unsafe { libc::geteuid() };
if meta.uid() != euid {
bail!(
"staged {} is owned by uid {}, not the invoking user ({euid})",
path.display(),
meta.uid()
);
}
Ok(Self { file })
}
fn proc_path(&self) -> PathBuf {
PathBuf::from(format!(
"/proc/{}/fd/{}",
std::process::id(),
self.file.as_raw_fd()
))
}
}
#[derive(Debug)]
pub struct SealedSource {
file: File,
}
impl SealedSource {
pub fn from_bytes(contents: &[u8]) -> Result<Self> {
use std::io::Write;
use std::os::fd::FromRawFd;
let fd = unsafe {
libc::memfd_create(
c"cargo-lbin-manifest".as_ptr(),
libc::MFD_CLOEXEC | libc::MFD_ALLOW_SEALING,
)
};
if fd < 0 {
return Err(std::io::Error::last_os_error()).context("memfd_create");
}
let mut file = unsafe { File::from_raw_fd(fd) };
file.write_all(contents)
.context("writing sealed manifest buffer")?;
let seals =
libc::F_SEAL_WRITE | libc::F_SEAL_GROW | libc::F_SEAL_SHRINK | libc::F_SEAL_SEAL;
if unsafe { libc::fcntl(file.as_raw_fd(), libc::F_ADD_SEALS, seals) } != 0 {
return Err(std::io::Error::last_os_error()).context("sealing manifest buffer");
}
{
use std::os::unix::fs::FileExt;
let len = file
.metadata()
.context("fstat on sealed manifest buffer")?
.len();
if len != contents.len() as u64 {
bail!("sealed manifest buffer was tampered with before sealing");
}
let mut check = vec![0u8; contents.len()];
file.read_exact_at(&mut check, 0)
.context("reading back sealed manifest buffer")?;
if check != contents {
bail!("sealed manifest buffer was tampered with before sealing");
}
}
Ok(Self { file })
}
fn proc_path(&self) -> PathBuf {
PathBuf::from(format!(
"/proc/{}/fd/{}",
std::process::id(),
self.file.as_raw_fd()
))
}
}
fn install_from_proc(policy: Escalation, proc_path: &Path, dest: &Path, mode: &str) -> Result<()> {
let parent = dest
.parent()
.context("destination has no parent directory")?;
let escalate = policy.escalate_for(parent)?;
let mode_flag = format!("-Dm{mode}");
run(
escalate,
INSTALL,
&[mode_flag.as_ref(), proc_path.as_os_str(), dest.as_os_str()],
)
}
pub fn install_verified(
policy: Escalation,
src: &VerifiedSource,
dest: &Path,
mode: &str,
) -> Result<()> {
install_atomic(policy, &src.proc_path(), dest, mode)
}
fn install_atomic(policy: Escalation, proc_path: &Path, dest: &Path, mode: &str) -> Result<()> {
let parent = dest
.parent()
.context("destination has no parent directory")?;
let name = dest
.file_name()
.and_then(|n| n.to_str())
.context("destination has no file name")?;
let tmp = parent.join(format!(".{name}.{}.tmp", std::process::id()));
install_from_proc(policy, proc_path, &tmp, mode)?;
let escalate = policy.escalate_for(parent)?;
let moved = run(
escalate,
MV,
&[
"-fT".as_ref(),
"--".as_ref(),
tmp.as_os_str(),
dest.as_os_str(),
],
);
if moved.is_err() {
let _ = run(
escalate,
RM,
&["-f".as_ref(), "--".as_ref(), tmp.as_os_str()],
);
}
moved
}
pub fn install_sealed(
policy: Escalation,
src: &SealedSource,
dest: &Path,
mode: &str,
) -> Result<()> {
install_atomic(policy, &src.proc_path(), dest, mode)
}
pub fn remove_files(policy: Escalation, paths: &[&Path]) -> Result<()> {
if paths.is_empty() {
return Ok(());
}
let escalate = escalate_for_paths(policy, paths)?;
let mut args: Vec<&OsStr> = vec!["-f".as_ref(), "--".as_ref()];
args.extend(paths.iter().map(|p| p.as_os_str()));
run(escalate, RM, &args)
}
pub fn ensure_lock_file(policy: Escalation, path: &Path) -> Result<()> {
let parent = path.parent().context("lock path has no parent directory")?;
let escalate = policy.escalate_for(parent)?;
run(escalate, MKDIR, &["-p".as_ref(), parent.as_os_str()])?;
run(escalate, TOUCH, &[path.as_os_str()])?;
run(escalate, CHMOD, &["0755".as_ref(), parent.as_os_str()])?;
run(escalate, CHMOD, &["0644".as_ref(), path.as_os_str()])?;
Ok(())
}
pub fn restorecon(policy: Escalation, paths: &[&Path]) {
let Some(program) = RESTORECON_CANDIDATES
.iter()
.find(|c| Path::new(c).is_file())
else {
return;
};
if paths.is_empty() {
return;
}
let Ok(escalate) = escalate_for_paths(policy, paths) else {
return;
};
let mut args: Vec<&OsStr> = Vec::with_capacity(paths.len());
args.extend(paths.iter().map(|p| p.as_os_str()));
let _ = run(escalate, program, &args);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_probe_never_destroys_existing_files() {
let dir = std::env::temp_dir().join("cargo-lbin-test-probe");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let pid = std::process::id();
let squatted = dir.join(format!(".cargo-lbin-write-probe.{pid}.0"));
fs::write(&squatted, b"precious").unwrap();
let target = dir.join("symlink-target");
fs::write(&target, b"target-content").unwrap();
let link = dir.join(format!(".cargo-lbin-write-probe.{pid}.1"));
std::os::unix::fs::symlink(&target, &link).unwrap();
assert!(dir_writable(&dir), "retry suffixes must sidestep squats");
assert_eq!(fs::read(&squatted).unwrap(), b"precious");
assert_eq!(fs::read(&target).unwrap(), b"target-content");
assert!(link.symlink_metadata().unwrap().file_type().is_symlink());
for attempt in 0..PROBE_ATTEMPTS {
let name = dir.join(format!(".cargo-lbin-write-probe.{pid}.{attempt}"));
if name.symlink_metadata().is_err() {
fs::write(&name, b"squat").unwrap();
}
}
assert!(!dir_writable(&dir));
assert_eq!(fs::read(&squatted).unwrap(), b"precious");
assert_eq!(fs::read(&target).unwrap(), b"target-content");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn verified_source_rejects_symlinks_and_accepts_regular_files() {
let dir = std::env::temp_dir().join("cargo-lbin-test-verified");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let real = dir.join("real");
fs::write(&real, b"binary").unwrap();
assert!(VerifiedSource::open(&real).is_ok());
let link = dir.join("link");
std::os::unix::fs::symlink("/etc/hostname", &link).unwrap();
let err = VerifiedSource::open(&link).unwrap_err();
assert!(err.to_string().contains("opening staged binary"), "{err:#}");
assert!(VerifiedSource::open(&dir).is_err(), "directories rejected");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn escalation_policy_only_for_canonical_prefix() {
let writable = std::env::temp_dir().join("cargo-lbin-test-esc-ok");
let _ = fs::remove_dir_all(&writable);
fs::create_dir_all(&writable).unwrap();
assert!(!Escalation::Forbidden.escalate_for(&writable).unwrap());
assert!(!Escalation::Allowed.escalate_for(&writable).unwrap());
let hostile = Path::new("/proc/cargo-lbin-nonexistent-esc/dir");
if needs_privilege(hostile) {
assert!(Escalation::Allowed.escalate_for(hostile).unwrap());
let err = Escalation::Forbidden
.escalate_for(hostile)
.unwrap_err()
.to_string();
assert!(err.contains("only /usr/local"), "{err}");
}
assert!(matches!(
Escalation::for_prefix(Path::new(CANONICAL_PREFIX)),
Escalation::Allowed
));
assert!(matches!(
Escalation::for_prefix(Path::new("/tmp/whatever")),
Escalation::Forbidden
));
let _ = fs::remove_dir_all(&writable);
}
#[test]
fn forbidden_policy_blocks_lock_escalation() {
let hostile = Path::new("/proc/cargo-lbin-nonexistent-lock/share/cargo-lbin/lock");
if needs_privilege(hostile.parent().unwrap()) {
let err = ensure_lock_file(Escalation::Forbidden, hostile)
.unwrap_err()
.to_string();
assert!(err.contains("only /usr/local"), "{err}");
}
}
#[test]
fn ensure_lock_file_forces_world_readable_modes() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join("cargo-lbin-test-umask");
let _ = fs::remove_dir_all(&dir);
let state = dir.join("share/cargo-lbin");
fs::create_dir_all(&state).unwrap();
fs::set_permissions(&state, fs::Permissions::from_mode(0o700)).unwrap();
let lock = state.join("lock");
fs::write(&lock, b"").unwrap();
fs::set_permissions(&lock, fs::Permissions::from_mode(0o600)).unwrap();
ensure_lock_file(Escalation::Allowed, &lock).unwrap();
let dir_mode = fs::metadata(&state).unwrap().permissions().mode() & 0o777;
let lock_mode = fs::metadata(&lock).unwrap().permissions().mode() & 0o777;
assert_eq!(dir_mode, 0o755, "state dir must be world-traversable");
assert_eq!(
lock_mode, 0o644,
"lock must be world-readable for shared flock"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn sealed_source_is_immutable_even_for_owner() {
use std::io::Write;
let sealed = SealedSource::from_bytes(b"TRUSTED").unwrap();
let reopened = OpenOptions::new().write(true).open(sealed.proc_path());
let mutated = match reopened {
Ok(mut f) => f.write_all(b"FORGED!").is_ok(),
Err(_) => false,
};
assert!(!mutated, "seal failed: content was mutated");
assert_eq!(fs::read(sealed.proc_path()).unwrap(), b"TRUSTED");
}
#[test]
fn proc_path_points_at_our_open_descriptor() {
let dir = std::env::temp_dir().join("cargo-lbin-test-procfd");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let src = dir.join("src");
fs::write(&src, b"CONTENT").unwrap();
let verified = VerifiedSource::open(&src).unwrap();
fs::remove_file(&src).unwrap();
std::os::unix::fs::symlink("/etc/hostname", &src).unwrap();
let read = fs::read(verified.proc_path()).unwrap();
assert_eq!(read, b"CONTENT");
let _ = fs::remove_dir_all(&dir);
}
}