commit 57176d45cacb98f1968daa8f8b2efd2735da2731 from: murilo ijanc date: Wed Mar 25 17:06:13 2026 UTC Add pledge(2) and unveil(2) sandboxing for tp and tpd tpd: unveil data dir (rwc), resolv.conf (r) when DNS needed, then pledge stdio rpath wpath cpath fattr inet unix dns. tp: unveil socket path (rw), then pledge stdio unix rpath. commit - ee25588324ca61275782a3628dd1838dae58e69e commit + 57176d45cacb98f1968daa8f8b2efd2735da2731 blob - e33c357ece3cd40a5f2251efccdfabf280c5fcc6 blob + 860d1c9f93b33a9120cd97b1cb15d07d5a1e3f30 --- src/bin/tp.rs +++ src/bin/tp.rs @@ -10,6 +10,8 @@ use std::path::PathBuf; #[path = "../base58.rs"] mod base58; +#[path = "../sandbox.rs"] +mod sandbox; fn default_socket() -> PathBuf { PathBuf::from("/var/tesseras-paste/daemon.sock") @@ -157,6 +159,11 @@ fn main() { } }; + // ── Sandbox ───────────────────────────────────── + sandbox::do_unveil(&sock_path, "rw"); + sandbox::unveil_lock(); + sandbox::do_pledge("stdio unix rpath"); + let stream = match UnixStream::connect(&sock_path) { Ok(s) => s, Err(e) => { blob - 2b7fdb20b84927b84ab1f95b3ec03e41879a2a33 blob + 2d8c01daacf821dc3737a8403a4c3eb477669dfd --- src/bin/tpd.rs +++ src/bin/tpd.rs @@ -18,6 +18,8 @@ mod ops; mod paste; #[path = "../protocol.rs"] mod protocol; +#[path = "../sandbox.rs"] +mod sandbox; #[path = "../store.rs"] mod store; @@ -186,6 +188,22 @@ fn main() { let id = node.id_hex(); eprintln!("tpd {addr} id={:.8}", id); + // ── Sandbox ───────────────────────────────────── + // Apply unveil(2) to restrict filesystem visibility, + // then pledge(2) to restrict syscalls. + sandbox::do_unveil(&dir, "rwc"); + if sock_path.parent() != Some(dir.as_ref()) { + if let Some(parent) = sock_path.parent() { + sandbox::do_unveil(parent, "rwc"); + } + } + if !no_auto_bootstrap || !bootstrap.is_empty() { + sandbox::do_unveil(std::path::Path::new("/etc/resolv.conf"), "r"); + } + sandbox::unveil_lock(); + + sandbox::do_pledge("stdio rpath wpath cpath fattr inet unix dns"); + // If no explicit peers given and auto-bootstrap is enabled, // discover peers via DNS SRV (_tesseras._udp.tesseras.net). if bootstrap.is_empty() && !no_auto_bootstrap { blob - /dev/null blob + 13f6a87526a8ddefa78fa907d9235b0a35f7deec (mode 644) --- /dev/null +++ src/sandbox.rs @@ -0,0 +1,77 @@ +//! OpenBSD pledge(2) and unveil(2) wrappers. + +use std::ffi::{CString, c_char}; +use std::path::Path; + +unsafe extern "C" { + fn pledge(promises: *const c_char, execpromises: *const c_char) -> i32; + fn unveil(path: *const c_char, permissions: *const c_char) -> i32; +} + +/// Valid pledge promises on OpenBSD. +const VALID_PROMISES: &[&str] = &[ + "audio", "bpf", "chown", "cpath", "disklabel", "dns", "dpath", + "error", "exec", "fattr", "flock", "getpw", "id", "inet", "mcast", + "pf", "proc", "ps", "recvfd", "route", "rpath", "sendfd", "settime", + "stdio", "tape", "tmppath", "tty", "unix", "unveil", "video", + "vminfo", "vmm", "wpath", "wroute", +]; + +/// Valid unveil permission characters. +const VALID_PERMS: &[u8] = b"rwcx"; + +/// Restrict the process to the given pledge promises. +pub fn do_pledge(promises: &str) { + for word in promises.split_whitespace() { + if !VALID_PROMISES.contains(&word) { + log::error!("pledge: unknown promise: {word}"); + std::process::exit(1); + } + } + let c = CString::new(promises).unwrap_or_else(|_| { + log::error!("pledge: promises contain NUL byte"); + std::process::exit(1); + }); + let ret = unsafe { pledge(c.as_ptr(), std::ptr::null()) }; + if ret != 0 { + let err = std::io::Error::last_os_error(); + log::error!("pledge failed: {err}"); + std::process::exit(1); + } + log::debug!("pledge applied"); +} + +/// Add a path to the unveil whitelist with the given permissions. +/// Permissions: "r" read, "w" write, "c" create, "x" execute. +pub fn do_unveil(path: &Path, perms: &str) { + if perms.is_empty() || !perms.as_bytes().iter().all(|b| VALID_PERMS.contains(b)) { + log::error!("unveil: invalid permissions"); + std::process::exit(1); + } + let p = CString::new(path.as_os_str().as_encoded_bytes()).unwrap_or_else(|_| { + log::error!("unveil: path contains NUL byte"); + std::process::exit(1); + }); + let f = CString::new(perms).unwrap_or_else(|_| { + log::error!("unveil: permissions contain NUL byte"); + std::process::exit(1); + }); + let ret = unsafe { unveil(p.as_ptr(), f.as_ptr()) }; + if ret != 0 { + let err = std::io::Error::last_os_error(); + log::error!("unveil failed: {err}"); + std::process::exit(1); + } + log::debug!("unveil: path added"); +} + +/// Lock the unveil list — no further unveil calls allowed. +pub fn unveil_lock() { + let ret = unsafe { unveil(std::ptr::null(), std::ptr::null()) }; + if ret != 0 { + let err = std::io::Error::last_os_error(); + log::error!("unveil lock failed: {err}"); + std::process::exit(1); + } + log::debug!("unveil locked"); +}