From 17f2bbc4032f86e665b1c7161f6007b1ef0a005b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 17:47:31 +0000 Subject: [PATCH 1/6] Auto-detect and handle network namespace failures in secure mode When running secure mode externally (in containers, VMs, or systems with restricted kernel configs), bwrap --unshare-net can fail with: "bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted" This change: - Adds check_network_namespace_support() to detect this at startup - Auto-disables network isolation when unsupported (other sandbox features remain) - Adds GROVE_SANDBOX_NETWORK env var for manual control - Updates startup logging and /version endpoint to show network isolation status - Documents the issue and workarounds in CLAUDE.md --- CLAUDE.md | 24 +++++++++++++++++++++ grove.hml | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9c44714..d262078 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -252,6 +252,7 @@ Grove accepts environment variables: | `HEMLOCKC_PATH` | hemlockc | Path to hemlockc binary (for type checking) | | `GROVE_SECURE_MODE` | false | Enable bubblewrap sandboxing | | `GROVE_BWRAP_PATH` | bwrap | Path to bubblewrap binary | +| `GROVE_SANDBOX_NETWORK` | auto | Network isolation (auto-detected; set to 0 to disable) | | `GROVE_SANDBOX_MEMORY_MB` | 256 | Memory limit per execution (MB) | | `GROVE_SANDBOX_PIDS_MAX` | 50 | Max processes per execution | | `GROVE_RATE_LIMIT` | 60 | Max requests per minute per IP | @@ -289,6 +290,29 @@ Additional protections: GROVE_SECURE_MODE=1 hemlock grove.hml ``` +### Network Isolation Troubleshooting + +Network isolation (`--unshare-net`) may fail on some systems with the error: +``` +bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted +``` + +This happens when the kernel doesn't allow unprivileged users to configure network interfaces in a new network namespace. Common causes: +- Running inside Docker/LXC containers without `--privileged` +- Kernel security restrictions (AppArmor, SELinux) +- Missing kernel capabilities + +Grove auto-detects this at startup and disables network isolation while keeping other sandbox features. You can also manually control it: +```bash +# Disable network isolation (sandbox can access network) +GROVE_SECURE_MODE=1 GROVE_SANDBOX_NETWORK=0 hemlock grove.hml + +# Force network isolation (will fail if not supported) +GROVE_SECURE_MODE=1 GROVE_SANDBOX_NETWORK=1 hemlock grove.hml +``` + +Note: Without network isolation, sandboxed code can make outbound network requests. Consider using a firewall or running Grove on an isolated network. + ### Setup Script Run `./setup-sandbox.sh` to check your system's security configuration: diff --git a/grove.hml b/grove.hml index 77d27eb..bf7dd0b 100644 --- a/grove.hml +++ b/grove.hml @@ -37,7 +37,8 @@ let CONFIG = { bwrap_path: "bwrap", // Path to bubblewrap binary bwrap_available: false, // Detected at startup sandbox_memory_mb: 256, // Memory limit for sandboxed processes - sandbox_pids_max: 50 // Max processes in sandbox + sandbox_pids_max: 50, // Max processes in sandbox + sandbox_network: true // Enable network isolation (disable if kernel doesn't support) }; // ============================================================================= @@ -193,6 +194,33 @@ fn check_user_namespaces() { return false; } +fn check_network_namespace_support() { + // Check if network namespace isolation works + // This can fail on some systems due to: + // - Kernel restrictions on unprivileged network namespaces + // - Running inside containers/VMs with limited capabilities + // - AppArmor/SELinux policies + if (!CONFIG["bwrap_available"]) { + return false; + } + // Try a simple bwrap command with network isolation + let test = run_capture([ + CONFIG["bwrap_path"], + "--unshare-user", + "--unshare-net", + "--uid", "65534", + "--gid", "65534", + "--ro-bind", "/usr", "/usr", + "--ro-bind", "/lib", "/lib", + "--ro-bind", "/bin", "/bin", + "--symlink", "usr/lib64", "/lib64", + "--proc", "/proc", + "--dev", "/dev", + "/bin/true" + ]); + return test["code"] == 0; +} + // Execute a shell command using exec_argv to avoid metacharacter warnings. // This is used when we intentionally need shell features (redirects, pipes, etc.) // Uses ["sh", "-c", command] pattern with direct fork/execvp. @@ -262,6 +290,23 @@ fn load_config() { } } + // Network isolation - can be explicitly disabled if kernel doesn't support it + let sandbox_net = getenv("GROVE_SANDBOX_NETWORK"); + if (sandbox_net != null) { + // Explicit setting from environment + CONFIG["sandbox_network"] = sandbox_net == "1" || to_lower(sandbox_net) == "true"; + } else if (CONFIG["secure_mode"]) { + // Auto-detect network namespace support + let net_supported = check_network_namespace_support(); + if (!net_supported) { + print("WARNING: Network namespace isolation not supported on this system"); + print(" (bwrap: loopback: Failed RTM_NEWADDR is a common cause)"); + print(" Disabling network isolation, other sandbox features remain active"); + print(" Set GROVE_SANDBOX_NETWORK=1 to force enable (may fail)"); + CONFIG["sandbox_network"] = false; + } + } + // Sandbox resource limits let mem = getenv("GROVE_SANDBOX_MEMORY_MB"); if (mem != null) { @@ -496,7 +541,8 @@ fn build_bwrap_command(temp_file, stdin_file, timeout_sec) { // Namespace isolation `--unshare-user ` + `--unshare-pid ` + - `--unshare-net ` + + // Network isolation (conditional - may not work on all systems) + (if (CONFIG["sandbox_network"]) { `--unshare-net `; } else { ""; }) + `--unshare-ipc ` + `--unshare-uts ` + `--unshare-cgroup ` + @@ -644,7 +690,8 @@ fn handle_version() { max_output_size: CONFIG["max_output_size"], secure_mode: CONFIG["secure_mode"], sandbox_memory_mb: CONFIG["sandbox_memory_mb"], - sandbox_pids_max: CONFIG["sandbox_pids_max"] + sandbox_pids_max: CONFIG["sandbox_pids_max"], + sandbox_network: CONFIG["sandbox_network"] }); } @@ -655,7 +702,8 @@ fn build_bwrap_check_command(temp_file) { let bwrap_cmd = `"${CONFIG["bwrap_path"]}" ` + `--unshare-user ` + - `--unshare-net ` + + // Network isolation (conditional - may not work on all systems) + (if (CONFIG["sandbox_network"]) { `--unshare-net `; } else { ""; }) + `--unshare-ipc ` + `--uid 65534 ` + `--gid 65534 ` + @@ -983,6 +1031,12 @@ fn main() { print(" Mode: SECURE (bubblewrap isolation)"); print(` Memory limit: ${CONFIG["sandbox_memory_mb"]}MB`); print(` Process limit: ${CONFIG["sandbox_pids_max"]}`); + if (CONFIG["sandbox_network"]) { + print(" Network: ISOLATED (no outbound connections)"); + } else { + print(" Network: SHARED (sandbox can access network)"); + print(" Set GROVE_SANDBOX_NETWORK=1 to force isolation"); + } } else { print(" Mode: BASIC (hemlock --sandbox only)"); if (CONFIG["bwrap_available"]) { From 104fb7c858a6d0f5df32fe8463524ecec7e11296 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 19:11:21 +0000 Subject: [PATCH 2/6] Fix Hemlock syntax error in conditional network flag --- grove.hml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/grove.hml b/grove.hml index bf7dd0b..4574422 100644 --- a/grove.hml +++ b/grove.hml @@ -537,12 +537,17 @@ fn build_bwrap_command(temp_file, stdin_file, timeout_sec) { // Construct bwrap command // Note: Using bash -c to set ulimits inside the sandbox (sh/dash doesn't support -u) + let net_flag = ""; + if (CONFIG["sandbox_network"]) { + net_flag = "--unshare-net "; + } + let bwrap_cmd = `"${CONFIG["bwrap_path"]}" ` + // Namespace isolation `--unshare-user ` + `--unshare-pid ` + // Network isolation (conditional - may not work on all systems) - (if (CONFIG["sandbox_network"]) { `--unshare-net `; } else { ""; }) + + net_flag + `--unshare-ipc ` + `--unshare-uts ` + `--unshare-cgroup ` + @@ -700,10 +705,15 @@ fn build_bwrap_check_command(temp_file) { // hemlockc only parses, so we can be more restrictive let check_cmd = `"${CONFIG["hemlockc_path"]}" --check "${temp_file}"`; + let net_flag = ""; + if (CONFIG["sandbox_network"]) { + net_flag = "--unshare-net "; + } + let bwrap_cmd = `"${CONFIG["bwrap_path"]}" ` + `--unshare-user ` + // Network isolation (conditional - may not work on all systems) - (if (CONFIG["sandbox_network"]) { `--unshare-net `; } else { ""; }) + + net_flag + `--unshare-ipc ` + `--uid 65534 ` + `--gid 65534 ` + From d29ff621a895d443ba957363edebce11cfbb770e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 19:13:39 +0000 Subject: [PATCH 3/6] Check user namespace support before enabling secure mode --- grove.hml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/grove.hml b/grove.hml index 4574422..28420b7 100644 --- a/grove.hml +++ b/grove.hml @@ -281,12 +281,18 @@ fn load_config() { // Secure mode - enable bubblewrap sandboxing let secure = getenv("GROVE_SECURE_MODE"); if (secure != null && (secure == "1" || to_lower(secure) == "true")) { - if (CONFIG["bwrap_available"]) { - CONFIG["secure_mode"] = true; - } else { + if (!CONFIG["bwrap_available"]) { print("WARNING: GROVE_SECURE_MODE requested but bubblewrap not found"); print(" Install bubblewrap: apt install bubblewrap"); print(" Falling back to basic sandbox mode"); + } else if (!check_user_namespaces()) { + print("WARNING: GROVE_SECURE_MODE requested but user namespaces not working"); + print(" (bwrap: setting up uid map: Permission denied)"); + print(" Check: cat /proc/sys/kernel/unprivileged_userns_clone"); + print(" Or newer bwrap may need: sysctl kernel.unprivileged_userns_clone=1"); + print(" Falling back to basic sandbox mode"); + } else { + CONFIG["secure_mode"] = true; } } From c0e694819bcf0c000dc4f5cdc0b52727c5917385 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 19:14:34 +0000 Subject: [PATCH 4/6] Test bwrap directly instead of trusting sysctl values --- grove.hml | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/grove.hml b/grove.hml index 28420b7..b12b751 100644 --- a/grove.hml +++ b/grove.hml @@ -179,19 +179,28 @@ fn check_bwrap_available() { } fn check_user_namespaces() { - // Check if unprivileged user namespaces are enabled - let result = run_capture(["cat", "/proc/sys/kernel/unprivileged_userns_clone"]); - if (result["code"] == 0) { - let value = trim(result["stdout"]); - return value == "1"; - } - // If file doesn't exist, namespaces might still work (depends on kernel config) - // Try a simple bwrap command to verify - if (CONFIG["bwrap_available"]) { - let test = run_capture([CONFIG["bwrap_path"], "--unshare-user", "--uid", "65534", "--gid", "65534", "/bin/true"]); - return test["code"] == 0; + // Always test bwrap directly - sysctl values can be misleading + // when AppArmor, SELinux, or other security policies block namespaces + if (!CONFIG["bwrap_available"]) { + return false; } - return false; + // Test with a command similar to what we actually run + let test = run_capture([ + CONFIG["bwrap_path"], + "--unshare-user", + "--unshare-pid", + "--unshare-ipc", + "--uid", "65534", + "--gid", "65534", + "--ro-bind", "/usr", "/usr", + "--ro-bind", "/lib", "/lib", + "--ro-bind", "/bin", "/bin", + "--symlink", "usr/lib64", "/lib64", + "--proc", "/proc", + "--dev", "/dev", + "/bin/true" + ]); + return test["code"] == 0; } fn check_network_namespace_support() { From 730e99de001dd1ee79a717c78f7274d174613ca2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 19:20:28 +0000 Subject: [PATCH 5/6] Fix temp file permissions for bwrap sandbox (chmod 644) --- grove.hml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/grove.hml b/grove.hml index b12b751..ccbbda5 100644 --- a/grove.hml +++ b/grove.hml @@ -629,11 +629,19 @@ fn execute_code(code, stdin_data) { let temp_file = `${CONFIG["temp_dir"]}/exec_${ts}_${rand}.hml`; write_file(temp_file, code); + // Make temp file readable by sandboxed user (nobody/65534) + if (CONFIG["secure_mode"]) { + run_capture(["chmod", "644", temp_file]); + } + // Create stdin file if stdin data is provided let stdin_file = null; if (stdin_data != null && stdin_data != "") { stdin_file = `${CONFIG["temp_dir"]}/stdin_${ts}_${rand}.txt`; write_file(stdin_file, stdin_data); + if (CONFIG["secure_mode"]) { + run_capture(["chmod", "644", stdin_file]); + } } // Execute with sandbox using timeout command @@ -786,6 +794,11 @@ fn handle_check(body) { let temp_file = `${CONFIG["temp_dir"]}/check_${ts}_${rand}.hml`; write_file(temp_file, code); + // Make temp file readable by sandboxed user (nobody/65534) + if (CONFIG["secure_mode"]) { + run_capture(["chmod", "644", temp_file]); + } + // Run hemlockc --check with optional sandboxing let result = null; if (CONFIG["secure_mode"]) { From 0f0c0964e4496a74f65aaa024e0f4ea64723008b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 19:23:10 +0000 Subject: [PATCH 6/6] Fix temp directory permission issues for multi-user scenarios --- grove.hml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/grove.hml b/grove.hml index ccbbda5..adcb03b 100644 --- a/grove.hml +++ b/grove.hml @@ -527,7 +527,29 @@ fn html_response(status, body) { fn ensure_temp_dir() { if (!exists(CONFIG["temp_dir"])) { - run_capture(["mkdir", "-p", CONFIG["temp_dir"]]); + let result = run_capture(["mkdir", "-p", CONFIG["temp_dir"]]); + if (result["code"] != 0) { + // Fall back to /tmp if we can't create the configured dir + CONFIG["temp_dir"] = "/tmp/grove-playground"; + run_capture(["mkdir", "-p", CONFIG["temp_dir"]]); + } + // Make directory accessible (for multi-user scenarios) + run_capture(["chmod", "755", CONFIG["temp_dir"]]); + } else { + // Directory exists - check if we can write to it + let test_file = `${CONFIG["temp_dir"]}/.write_test_${time_ms()}`; + let result = run_capture(["touch", test_file]); + if (result["code"] != 0) { + // Can't write - fall back to /tmp + CONFIG["temp_dir"] = "/tmp/grove-playground"; + if (!exists(CONFIG["temp_dir"])) { + run_capture(["mkdir", "-p", CONFIG["temp_dir"]]); + run_capture(["chmod", "755", CONFIG["temp_dir"]]); + } + } else { + // Clean up test file + run_capture(["rm", "-f", test_file]); + } } }