From 933f1b38537727deffc6c79d7a734e53e328bc51 Mon Sep 17 00:00:00 2001 From: Jan Bauer Nielsen Date: Mon, 1 Jun 2026 10:35:43 +0200 Subject: [PATCH 1/3] fix(sandbox/linux): allow open_port=0 to disable Landlock TCP in ProxyOnly mode Testcontainers maps Docker containers to random ephemeral ports on localhost. Maven Surefire binds to 127.0.0.1:0 for inter-process coordination. Neither can be expressed as explicit port rules in nono's ProxyOnly configuration. Landlock network rules match on exact port number with no wildcard and no IP filter. Port 0 is not a catch-all: ConnectTcp/0 matches connects to destination port 0 only; BindTcp/0 matches bind(port=0) calls only. There is no Landlock primitive for 'allow 127.0.0.1:* only'. When open_port = [0] is set in ProxyOnly mode, skip handle_access(AccessNet) on the Landlock ruleset entirely. Without AccessNet handling the kernel imposes no TCP restrictions; the proxy becomes the sole domain enforcer via HTTPS_PROXY / HTTP_PROXY / ALL_PROXY. The same applies to the seccomp fallback path on kernels without Landlock v4+. Disclosed tradeoff: this removes kernel-level TCP enforcement entirely, not just for localhost. Non-HTTP TCP clients (psql, redis-cli, nc, raw sockets) can reach remote hosts directly. ProxyOnly mode protects HTTP/HTTPS traffic via standard library proxy support; raw TCP bypass requires hardcoded IPs. Proper localhost-only restriction would require network namespaces or cgroup BPF. open_port = [0] in Blocked mode (no proxy) is still rejected with an error. Closes #611 Signed-off-by: Jan Bauer Nielsen --- crates/nono/src/capability.rs | 12 ++++- crates/nono/src/sandbox/linux.rs | 93 +++++++++++++++++++++++++++----- 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/crates/nono/src/capability.rs b/crates/nono/src/capability.rs index e786b7d83..cfee53d4b 100644 --- a/crates/nono/src/capability.rs +++ b/crates/nono/src/capability.rs @@ -1066,8 +1066,16 @@ impl CapabilitySet { /// `127.0.0.1:port`. Works across all network modes. /// /// On macOS: outbound is per-port via Seatbelt; bind/inbound is blanket - /// (same tradeoff as `--allow-bind`). - /// On Linux: per-port ConnectTcp + BindTcp via Landlock. + /// (same tradeoff as `--allow-bind`). Port `0` generates a `localhost:*` + /// wildcard rule, allowing any localhost port. + /// + /// On Linux: per-port ConnectTcp + BindTcp via Landlock. Port `0` has no + /// wildcard semantics in Landlock — `ConnectTcp/0` only matches connects to + /// destination port 0, and `BindTcp/0` only permits `bind(port=0)` (OS ephemeral + /// assignment). Neither covers connecting to a random high port such as a + /// Testcontainers mapping. Instead, port `0` causes Landlock network handling to be + /// skipped entirely, leaving the proxy as the sole domain enforcer. This is only + /// permitted in `ProxyOnly` mode; `Blocked` mode rejects port `0` at sandbox init. #[must_use] pub fn allow_localhost_port(mut self, port: u16) -> Self { self.localhost_ports.push(port); diff --git a/crates/nono/src/sandbox/linux.rs b/crates/nono/src/sandbox/linux.rs index a6c98ea48..4ac1498b6 100644 --- a/crates/nono/src/sandbox/linux.rs +++ b/crates/nono/src/sandbox/linux.rs @@ -479,14 +479,49 @@ pub fn apply_with_abi(caps: &CapabilitySet, abi: &DetectedAbi) -> Result Result Result Result Date: Tue, 2 Jun 2026 07:21:56 +0200 Subject: [PATCH 2/3] fix(sandbox/linux): isolate sandbox application in test via fork `test_accept_localhost_port_wildcard_zero_in_proxy_only_mode` called `apply_with_abi()` directly, which invokes `restrict_self()` and permanently sandboxes the test-runner process. Tests running later in the same process could fail spuriously due to the unexpected Landlock restrictions. Fix: wrap the call in a `fork()`/`waitpid()` child, matching the pattern used in `probe_abstract_socket_with_landlock` (line 2630). Signed-off-by: Jan Bauer Nielsen Co-Authored-By: Claude Sonnet 4.6 --- crates/nono/src/sandbox/linux.rs | 34 +++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/nono/src/sandbox/linux.rs b/crates/nono/src/sandbox/linux.rs index 4ac1498b6..c5ecc7eff 100644 --- a/crates/nono/src/sandbox/linux.rs +++ b/crates/nono/src/sandbox/linux.rs @@ -3292,9 +3292,37 @@ mod tests { } let mut caps = CapabilitySet::new().proxy_only(8080); caps.add_localhost_port(0); - // Should not return an error; port 0 is the Landlock wildcard. - apply_with_abi(&caps, &detected) - .expect("port 0 in ProxyOnly mode must be accepted on Linux"); + + // Fork a child to isolate the irreversible sandbox application. + // apply_with_abi() calls restrict_self(), which permanently sandboxes the + // calling process. Running it in a child prevents corrupting the test runner. + // SAFETY: fork() is used in a test helper; the child exits via _exit without + // running parent process destructors after fork. + let child_pid = unsafe { libc::fork() }; + assert!(child_pid >= 0, "fork() failed"); + + if child_pid == 0 { + let exit_code = match apply_with_abi(&caps, &detected) { + Ok(_) => 0, + Err(_) => 1, + }; + // SAFETY: _exit terminates the child without running parent destructors. + unsafe { libc::_exit(exit_code) }; + } + + let mut child_status = 0; + // SAFETY: child_pid is the PID returned by fork() in the parent branch. + let waited = unsafe { libc::waitpid(child_pid, &mut child_status, 0) }; + assert_eq!(waited, child_pid, "waitpid() failed"); + assert!( + libc::WIFEXITED(child_status), + "child did not exit normally" + ); + assert_eq!( + libc::WEXITSTATUS(child_status), + 0, + "port 0 in ProxyOnly mode must be accepted on Linux" + ); } #[test] From b7ccecfa4ea8ca700b00006dcb6ce14f0419a78b Mon Sep 17 00:00:00 2001 From: Jan Bauer Nielsen Date: Tue, 2 Jun 2026 07:24:02 +0200 Subject: [PATCH 3/3] style: fix rustfmt violations in port-zero sandbox tests Collapse single-argument assert! calls to one line as rustfmt requires. Signed-off-by: Jan Bauer Nielsen Co-Authored-By: Claude Sonnet 4.6 --- crates/nono/src/sandbox/linux.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/nono/src/sandbox/linux.rs b/crates/nono/src/sandbox/linux.rs index c5ecc7eff..526cae894 100644 --- a/crates/nono/src/sandbox/linux.rs +++ b/crates/nono/src/sandbox/linux.rs @@ -3272,10 +3272,7 @@ mod tests { caps.add_localhost_port(0); let err = apply_with_abi(&caps, &detected).expect_err("port 0 wildcard must be rejected"); let msg = format!("{err}"); - assert!( - msg.contains("ProxyOnly mode"), - "unexpected error: {msg}" - ); + assert!(msg.contains("ProxyOnly mode"), "unexpected error: {msg}"); } /// Accepts `open_port: [0]` in ProxyOnly mode on Linux (Landlock v4+) — the proxy @@ -3314,10 +3311,7 @@ mod tests { // SAFETY: child_pid is the PID returned by fork() in the parent branch. let waited = unsafe { libc::waitpid(child_pid, &mut child_status, 0) }; assert_eq!(waited, child_pid, "waitpid() failed"); - assert!( - libc::WIFEXITED(child_status), - "child did not exit normally" - ); + assert!(libc::WIFEXITED(child_status), "child did not exit normally"); assert_eq!( libc::WEXITSTATUS(child_status), 0,