Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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:
Expand Down
152 changes: 133 additions & 19 deletions grove.hml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};

// =============================================================================
Expand Down Expand Up @@ -178,19 +179,55 @@ 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";
// Always test bwrap directly - sysctl values can be misleading
// when AppArmor, SELinux, or other security policies block namespaces
if (!CONFIG["bwrap_available"]) {
return false;
}
// 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;
// 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() {
// 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;
}
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.
Expand Down Expand Up @@ -253,12 +290,35 @@ 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;
}
}

// 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;
}
}

Expand Down Expand Up @@ -467,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]);
}
}
}

Expand All @@ -492,11 +574,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 ` +
`--unshare-net ` +
// Network isolation (conditional - may not work on all systems)
net_flag +
`--unshare-ipc ` +
`--unshare-uts ` +
`--unshare-cgroup ` +
Expand Down Expand Up @@ -563,11 +651,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
Expand Down Expand Up @@ -644,7 +740,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"]
});
}

Expand All @@ -653,9 +750,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 ` +
`--unshare-net ` +
// Network isolation (conditional - may not work on all systems)
net_flag +
`--unshare-ipc ` +
`--uid 65534 ` +
`--gid 65534 ` +
Expand Down Expand Up @@ -713,6 +816,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"]) {
Expand Down Expand Up @@ -983,6 +1091,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"]) {
Expand Down