#![cfg(target_os = "linux")]
use std::ffi::CString;
use std::os::unix::io::RawFd;
pub fn run_helper(args: &[String]) -> ! {
let parsed = match parse_args(args) {
Ok(p) => p,
Err(msg) => {
eprintln!("envseal __sandbox_helper: {msg}");
std::process::exit(127);
}
};
if let Err(msg) = setup_private_mounts() {
eprintln!("envseal __sandbox_helper: mount setup failed: {msg}");
std::process::exit(127);
}
{
let target = std::ffi::CString::new("/tmp").expect("/tmp is a valid CStr literal");
let rc = unsafe { libc::chdir(target.as_ptr()) };
if rc != 0 {
let err = std::io::Error::last_os_error();
eprintln!("envseal __sandbox_helper: chdir(\"/tmp\") failed: {err}");
std::process::exit(127);
}
}
if let (Some(fd), Some(env_name)) = (parsed.secret_fd, parsed.secret_env_name.as_deref()) {
match read_secret_from_fd(fd) {
Ok(value) => {
let value = zeroize::Zeroizing::new(value);
if let Err(msg) = setenv_async_signal_safe(env_name, &value) {
eprintln!("envseal __sandbox_helper: setenv failed: {msg}");
std::process::exit(127);
}
}
Err(msg) => {
eprintln!("envseal __sandbox_helper: read secret-fd failed: {msg}");
std::process::exit(127);
}
}
}
match exec_target(&parsed) {
Err(msg) => {
eprintln!("envseal __sandbox_helper: exec failed: {msg}");
std::process::exit(127);
}
}
}
fn read_secret_from_fd(fd: RawFd) -> Result<Vec<u8>, String> {
use std::io::Read;
use std::os::fd::FromRawFd;
const MAX_SECRET_BYTES: u64 = 64 * 1024;
let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
let mut buf = Vec::with_capacity(1024);
file.by_ref()
.take(MAX_SECRET_BYTES)
.read_to_end(&mut buf)
.map_err(|e| format!("read secret-fd: {e}"))?;
Ok(buf)
}
fn setenv_async_signal_safe(name: &str, value: &[u8]) -> Result<(), String> {
use std::ffi::CString;
let name_c = CString::new(name).map_err(|_| "env name contains NUL".to_string())?;
let value_c = CString::new(value).map_err(|_| "secret value contains NUL".to_string())?;
let rc = unsafe { libc::setenv(name_c.as_ptr(), value_c.as_ptr(), 1) };
if rc != 0 {
return Err(std::io::Error::last_os_error().to_string());
}
Ok(())
}
struct ParsedArgs {
target_fd: RawFd,
arg0: String,
rest_argv: Vec<String>,
secret_fd: Option<RawFd>,
secret_env_name: Option<String>,
}
fn parse_args(args: &[String]) -> Result<ParsedArgs, String> {
let sep = args
.iter()
.position(|a| a == "--")
.ok_or_else(|| "missing `--` separator before target argv".to_string())?;
let head = &args[..sep];
let tail = &args[sep + 1..];
let mut found_target_fd: Option<RawFd> = None;
let mut found_arg0: Option<String> = None;
let mut found_secret_fd: Option<RawFd> = None;
let mut found_secret_env_name: Option<String> = None;
let mut iter = head.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--target-fd" => {
let v = iter
.next()
.ok_or_else(|| "--target-fd requires a value".to_string())?;
found_target_fd = Some(
v.parse::<RawFd>()
.map_err(|e| format!("invalid --target-fd {v:?}: {e}"))?,
);
}
"--arg0" => {
found_arg0 = Some(
iter.next()
.ok_or_else(|| "--arg0 requires a value".to_string())?
.clone(),
);
}
"--secret-fd" => {
let v = iter
.next()
.ok_or_else(|| "--secret-fd requires a value".to_string())?;
found_secret_fd = Some(
v.parse::<RawFd>()
.map_err(|e| format!("invalid --secret-fd {v:?}: {e}"))?,
);
}
"--secret-env-name" => {
found_secret_env_name = Some(
iter.next()
.ok_or_else(|| "--secret-env-name requires a value".to_string())?
.clone(),
);
}
other => return Err(format!("unrecognized helper flag: {other}")),
}
}
if found_secret_fd.is_some() != found_secret_env_name.is_some() {
return Err(
"--secret-fd and --secret-env-name must be passed together (or neither)".to_string(),
);
}
Ok(ParsedArgs {
target_fd: found_target_fd.ok_or_else(|| "--target-fd is required".to_string())?,
arg0: found_arg0.ok_or_else(|| "--arg0 is required".to_string())?,
rest_argv: tail.to_vec(),
secret_fd: found_secret_fd,
secret_env_name: found_secret_env_name,
})
}
fn setup_private_mounts() -> Result<(), String> {
if !in_private_mount_ns() {
return Err("refusing to modify mounts: not in a private mount namespace".to_string());
}
let root = CString::new("/").map_err(|e| e.to_string())?;
let none = CString::new("none").map_err(|e| e.to_string())?;
let tmp = CString::new("/tmp").map_err(|e| e.to_string())?;
let tmpfs = CString::new("tmpfs").map_err(|e| e.to_string())?;
let dev_shm = CString::new("/dev/shm").map_err(|e| e.to_string())?;
let var_tmp = CString::new("/var/tmp").map_err(|e| e.to_string())?;
let run = CString::new("/run").map_err(|e| e.to_string())?;
let var_run = CString::new("/var/run").map_err(|e| e.to_string())?;
let var_lock = CString::new("/var/lock").map_err(|e| e.to_string())?;
let opts = CString::new("size=256m,mode=1777").map_err(|e| e.to_string())?;
unsafe {
let ret = libc::mount(
none.as_ptr(),
root.as_ptr(),
std::ptr::null(),
libc::MS_REC | libc::MS_PRIVATE,
std::ptr::null(),
);
if ret != 0 {
let err = std::io::Error::last_os_error();
return Err(format!("MS_PRIVATE on /: {err}"));
}
let ret = libc::mount(
none.as_ptr(),
root.as_ptr(),
std::ptr::null(),
libc::MS_REMOUNT | libc::MS_RDONLY | libc::MS_BIND,
std::ptr::null(),
);
if ret != 0 {
let err = std::io::Error::last_os_error();
return Err(format!("MS_REMOUNT | MS_RDONLY on /: {err}"));
}
for mount_point in [&tmp, &dev_shm, &var_tmp] {
let ret = libc::mount(
tmpfs.as_ptr(),
mount_point.as_ptr(),
tmpfs.as_ptr(),
0,
opts.as_ptr().cast::<libc::c_void>(),
);
if ret != 0 {
let err = std::io::Error::last_os_error();
return Err(format!(
"mount tmpfs on {}: {err}",
mount_point.to_string_lossy()
));
}
}
for mount_point in [&run, &var_run, &var_lock] {
let _ = libc::mount(
tmpfs.as_ptr(),
mount_point.as_ptr(),
tmpfs.as_ptr(),
0,
opts.as_ptr().cast::<libc::c_void>(),
);
}
}
Ok(())
}
fn in_private_mount_ns() -> bool {
let self_ns = std::fs::read_link("/proc/self/ns/mnt");
let init_ns = std::fs::read_link("/proc/1/ns/mnt");
match (self_ns, init_ns) {
(Ok(a), Ok(b)) => a != b,
_ => false,
}
}
fn exec_target(parsed: &ParsedArgs) -> Result<std::convert::Infallible, String> {
let exec_path = format!("/proc/self/fd/{}", parsed.target_fd);
let exec_path_c = CString::new(exec_path.as_bytes()).map_err(|e| e.to_string())?;
let arg0_c = CString::new(parsed.arg0.as_bytes()).map_err(|e| e.to_string())?;
let mut argv_cstrings: Vec<CString> = Vec::with_capacity(parsed.rest_argv.len() + 1);
argv_cstrings.push(arg0_c);
for a in &parsed.rest_argv {
argv_cstrings.push(CString::new(a.as_bytes()).map_err(|e| e.to_string())?);
}
let mut argv_ptrs: Vec<*const libc::c_char> =
argv_cstrings.iter().map(|c| c.as_ptr()).collect();
argv_ptrs.push(std::ptr::null());
unsafe {
libc::execv(exec_path_c.as_ptr(), argv_ptrs.as_ptr());
}
let err = std::io::Error::last_os_error();
Err(format!("execv({exec_path}): {err}"))
}