Commit Diff


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");
+}