Skip to content

Commit 4e8dbcf

Browse files
authored
fix(sandbox): harden seccomp, inference routing, and process limits (#869)
* fix(sandbox): block AF_NETLINK in seccomp unconditionally Move AF_NETLINK to the unconditional socket-domain block list alongside AF_PACKET, AF_BLUETOOTH, and AF_VSOCK. Previously it was only blocked in NetworkMode::Block, leaving it accessible in Proxy mode where network namespace isolation already scopes netlink to the sandbox's own veth — making this a defense-in-depth hardening rather than a live exposure. Closes OS-94 * fix(sandbox): scope inference.local interception to port 443 The pre-OPA interception for inference.local matched on hostname alone, allowing any port to bypass OPA policy evaluation — including under deny-all (network_policies: {}). Add a port check so only port 443 takes the interception path; all other ports on inference.local now fall through to OPA and are subject to normal policy evaluation. Closes OS-95 * fix(sandbox): enforce RLIMIT_NPROC to prevent fork bomb DoS Set a hard limit of 512 processes per UID in harden_child_process(), applied before privilege drop so the sandbox user cannot raise it. Prevents unrestricted fork() from exhausting the process table — most relevant for local dev mode where K8s pod cgroup pids.max is absent. Closes OS-96 * chore(sandbox): fix rustfmt formatting for seccomp blocked_domains --------- Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
1 parent 3bc8e44 commit 4e8dbcf

3 files changed

Lines changed: 24 additions & 3 deletions

File tree

crates/openshell-sandbox/src/process.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,22 @@ pub(crate) fn harden_child_process() -> Result<()> {
4949
));
5050
}
5151

52+
// Limit process creation to prevent fork bombs. 512 processes per UID is
53+
// sufficient for typical agent workloads (shell, compilers, language servers)
54+
// while preventing runaway forking. Set as a hard limit so the sandbox user
55+
// cannot raise it after privilege drop.
56+
let nproc_limit = libc::rlimit {
57+
rlim_cur: 512,
58+
rlim_max: 512,
59+
};
60+
let rc = unsafe { libc::setrlimit(libc::RLIMIT_NPROC, &nproc_limit) };
61+
if rc != 0 {
62+
return Err(miette::miette!(
63+
"Failed to set RLIMIT_NPROC: {}",
64+
std::io::Error::last_os_error()
65+
));
66+
}
67+
5268
#[cfg(target_os = "linux")]
5369
{
5470
let rc = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) };

crates/openshell-sandbox/src/proxy.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use tracing::{debug, warn};
2727

2828
const MAX_HEADER_BYTES: usize = 8192;
2929
const INFERENCE_LOCAL_HOST: &str = "inference.local";
30+
const INFERENCE_LOCAL_PORT: u16 = 443;
3031

3132
/// Maximum total bytes for a streaming inference response body (32 MiB).
3233
const MAX_STREAMING_BODY: usize = 32 * 1024 * 1024;
@@ -354,7 +355,7 @@ async fn handle_tcp_connection(
354355
let (host, port) = parse_target(target)?;
355356
let host_lc = host.to_ascii_lowercase();
356357

357-
if host_lc == INFERENCE_LOCAL_HOST {
358+
if host_lc == INFERENCE_LOCAL_HOST && port == INFERENCE_LOCAL_PORT {
358359
respond(&mut client, b"HTTP/1.1 200 Connection Established\r\n\r\n").await?;
359360
let outcome = handle_inference_interception(
360361
client,

crates/openshell-sandbox/src/sandbox/linux/seccomp.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,15 @@ fn build_filter_rules(allow_inet: bool) -> Result<BTreeMap<i64, Vec<SeccompRule>
112112
let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();
113113

114114
// --- Socket domain blocks ---
115-
let mut blocked_domains = vec![libc::AF_PACKET, libc::AF_BLUETOOTH, libc::AF_VSOCK];
115+
let mut blocked_domains = vec![
116+
libc::AF_PACKET,
117+
libc::AF_BLUETOOTH,
118+
libc::AF_VSOCK,
119+
libc::AF_NETLINK,
120+
];
116121
if !allow_inet {
117122
blocked_domains.push(libc::AF_INET);
118123
blocked_domains.push(libc::AF_INET6);
119-
blocked_domains.push(libc::AF_NETLINK);
120124
}
121125

122126
for domain in blocked_domains {

0 commit comments

Comments
 (0)