diff --git a/crates/openshell-sandbox/src/process.rs b/crates/openshell-sandbox/src/process.rs index 4e9225929..85a57b4e7 100644 --- a/crates/openshell-sandbox/src/process.rs +++ b/crates/openshell-sandbox/src/process.rs @@ -49,6 +49,22 @@ pub(crate) fn harden_child_process() -> Result<()> { )); } + // Limit process creation to prevent fork bombs. 512 processes per UID is + // sufficient for typical agent workloads (shell, compilers, language servers) + // while preventing runaway forking. Set as a hard limit so the sandbox user + // cannot raise it after privilege drop. + let nproc_limit = libc::rlimit { + rlim_cur: 512, + rlim_max: 512, + }; + let rc = unsafe { libc::setrlimit(libc::RLIMIT_NPROC, &nproc_limit) }; + if rc != 0 { + return Err(miette::miette!( + "Failed to set RLIMIT_NPROC: {}", + std::io::Error::last_os_error() + )); + } + #[cfg(target_os = "linux")] { let rc = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) }; diff --git a/crates/openshell-sandbox/src/proxy.rs b/crates/openshell-sandbox/src/proxy.rs index 6f85e848e..f91f2c551 100644 --- a/crates/openshell-sandbox/src/proxy.rs +++ b/crates/openshell-sandbox/src/proxy.rs @@ -27,6 +27,7 @@ use tracing::{debug, warn}; const MAX_HEADER_BYTES: usize = 8192; const INFERENCE_LOCAL_HOST: &str = "inference.local"; +const INFERENCE_LOCAL_PORT: u16 = 443; /// Maximum total bytes for a streaming inference response body (32 MiB). const MAX_STREAMING_BODY: usize = 32 * 1024 * 1024; @@ -354,7 +355,7 @@ async fn handle_tcp_connection( let (host, port) = parse_target(target)?; let host_lc = host.to_ascii_lowercase(); - if host_lc == INFERENCE_LOCAL_HOST { + if host_lc == INFERENCE_LOCAL_HOST && port == INFERENCE_LOCAL_PORT { respond(&mut client, b"HTTP/1.1 200 Connection Established\r\n\r\n").await?; let outcome = handle_inference_interception( client, diff --git a/crates/openshell-sandbox/src/sandbox/linux/seccomp.rs b/crates/openshell-sandbox/src/sandbox/linux/seccomp.rs index 854134cbf..7b6f0f412 100644 --- a/crates/openshell-sandbox/src/sandbox/linux/seccomp.rs +++ b/crates/openshell-sandbox/src/sandbox/linux/seccomp.rs @@ -112,11 +112,15 @@ fn build_filter_rules(allow_inet: bool) -> Result let mut rules: BTreeMap> = BTreeMap::new(); // --- Socket domain blocks --- - let mut blocked_domains = vec![libc::AF_PACKET, libc::AF_BLUETOOTH, libc::AF_VSOCK]; + let mut blocked_domains = vec![ + libc::AF_PACKET, + libc::AF_BLUETOOTH, + libc::AF_VSOCK, + libc::AF_NETLINK, + ]; if !allow_inet { blocked_domains.push(libc::AF_INET); blocked_domains.push(libc::AF_INET6); - blocked_domains.push(libc::AF_NETLINK); } for domain in blocked_domains {