From 33786b37fffc3f3e47eac8f5a34b1e2fbfd7eb95 Mon Sep 17 00:00:00 2001 From: Ryan Whitworth Date: Tue, 31 Mar 2026 22:07:02 -0400 Subject: [PATCH 1/3] Replace kill(0) PID check with flock to prevent TOCTOU race --- .../openshell-bootstrap/src/runtime_apple.rs | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/crates/openshell-bootstrap/src/runtime_apple.rs b/crates/openshell-bootstrap/src/runtime_apple.rs index e3f6235c..9decba07 100644 --- a/crates/openshell-bootstrap/src/runtime_apple.rs +++ b/crates/openshell-bootstrap/src/runtime_apple.rs @@ -93,25 +93,36 @@ impl AppleContainerRuntime { Ok(()) } - /// Check if a gateway process is running by reading the PID file. + /// Check if a gateway process is running by probing the PID file lock. pub async fn check_existing(&self, name: &str) -> Result> { let pid_path = self.pid_path(name); if !pid_path.exists() { return Ok(None); } - let pid_str = std::fs::read_to_string(&pid_path).unwrap_or_default(); - let pid: i32 = match pid_str.trim().parse() { - Ok(p) => p, - Err(_) => return Ok(None), + // Use flock to determine if the gateway process is still holding the + // PID file lock. This avoids the TOCTOU race between reading the PID + // and checking with kill(pid, 0). + let running = match std::fs::File::open(&pid_path) { + Ok(file) => { + let fd = std::os::unix::io::AsRawFd::as_raw_fd(&file); + // LOCK_EX | LOCK_NB: try non-blocking exclusive lock. + // If we get it, no process holds it → not running. + let got_lock = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) } == 0; + if got_lock { + // Release the lock we just acquired. + unsafe { libc::flock(fd, libc::LOCK_UN) }; + false + } else { + true + } + } + Err(_) => false, }; - // Check if process is alive. - let alive = unsafe { libc::kill(pid, 0) } == 0; - Ok(Some(ExistingGateway { container_exists: true, - container_running: alive, + container_running: running, storage_exists: self.data_dir(name).exists(), container_image: Some("native".to_string()), })) @@ -187,6 +198,14 @@ impl AppleContainerRuntime { .into_diagnostic() .wrap_err("failed to write PID file")?; + // Hold an exclusive lock on the PID file for the process lifetime. + // check_existing() uses flock to determine if the gateway is running. + let pid_file = std::fs::File::open(&pid_path).into_diagnostic()?; + let fd = std::os::unix::io::AsRawFd::as_raw_fd(&pid_file); + unsafe { libc::flock(fd, libc::LOCK_EX) }; + // pid_file is intentionally leaked to hold the lock until process exit. + std::mem::forget(pid_file); + Ok(()) } From 71e544e2e245f728c06175244f2b8dd6e8d736ea Mon Sep 17 00:00:00 2001 From: Ryan Whitworth Date: Tue, 31 Mar 2026 22:27:59 -0400 Subject: [PATCH 2/3] Warn when port forwarding binds to all network interfaces --- crates/openshell-core/src/forward.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/openshell-core/src/forward.rs b/crates/openshell-core/src/forward.rs index c7b63fef..0f1a851e 100644 --- a/crates/openshell-core/src/forward.rs +++ b/crates/openshell-core/src/forward.rs @@ -292,6 +292,12 @@ impl ForwardSpec { if port == 0 { return Err(miette::miette!("port must be between 1 and 65535")); } + if addr == "0.0.0.0" || addr == "::" { + eprintln!( + "warning: port forwarding binds to all interfaces ({}); use 127.0.0.1 to restrict to localhost", + addr + ); + } return Ok(Self { bind_addr: addr.to_string(), port, From 9e8ff073e281e753ba72bb27b65c13eae4a8cd5b Mon Sep 17 00:00:00 2001 From: Ryan Whitworth Date: Tue, 31 Mar 2026 22:28:46 -0400 Subject: [PATCH 3/3] Run example container as non-root user --- examples/private-ip-routing/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/private-ip-routing/Dockerfile b/examples/private-ip-routing/Dockerfile index 614b3b47..5c604158 100644 --- a/examples/private-ip-routing/Dockerfile +++ b/examples/private-ip-routing/Dockerfile @@ -2,6 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 FROM python:3.13-slim +RUN useradd -r -s /usr/sbin/nologin appuser COPY server.py /app/server.py EXPOSE 8080 +USER appuser CMD ["python", "/app/server.py"]