{"id":47745114,"url":"https://github.com/multikernel/sandlock","last_synced_at":"2026-05-26T01:06:35.547Z","repository":{"id":344281045,"uuid":"1181262173","full_name":"multikernel/sandlock","owner":"multikernel","description":"The lightest AI sandbox. A process-based sandbox for Linux, no container, no VM, no root.","archived":false,"fork":false,"pushed_at":"2026-05-22T23:07:18.000Z","size":2957,"stargazers_count":166,"open_issues_count":7,"forks_count":19,"subscribers_count":4,"default_branch":"main","last_synced_at":"2026-05-22T23:37:12.232Z","etag":null,"topics":["ai-agents","landlock","linux","rust","sandboxing","seccomp"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/multikernel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-13T23:31:16.000Z","updated_at":"2026-05-22T22:47:09.000Z","dependencies_parsed_at":null,"dependency_job_id":"1fdd91d8-1fbd-40f2-bde9-e59448197a7d","html_url":"https://github.com/multikernel/sandlock","commit_stats":null,"previous_names":["multikernel/sandlock"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/multikernel/sandlock","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/multikernel%2Fsandlock","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/multikernel%2Fsandlock/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/multikernel%2Fsandlock/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/multikernel%2Fsandlock/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/multikernel","download_url":"https://codeload.github.com/multikernel/sandlock/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/multikernel%2Fsandlock/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33499292,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-25T14:31:05.219Z","status":"ssl_error","status_checked_at":"2026-05-25T14:31:02.878Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","landlock","linux","rust","sandboxing","seccomp"],"created_at":"2026-04-03T00:37:50.229Z","updated_at":"2026-05-26T01:06:35.528Z","avatar_url":"https://github.com/multikernel.png","language":"Rust","funding_links":[],"categories":["Sandboxing \u0026 Isolation","Rust"],"sub_categories":[],"readme":"# Sandlock\n\nLightweight process sandbox for Linux. Confines untrusted code using\n**Landlock** (filesystem + network + IPC), **seccomp-bpf** (syscall filtering),\nand **seccomp user notification** (resource limits, IP enforcement, /proc\nvirtualization). No root, no cgroups, no containers.\n\n```\nsandlock run -w /tmp -r /usr -r /lib -m 512M -- python3 untrusted.py\n```\n\n## Why Sandlock?\n\nContainers and VMs are powerful but heavy. Sandlock targets the gap: strict\nconfinement without image builds or root privileges. Built-in COW filesystem\nprotects your working directory automatically.\n\n| Feature | Sandlock | Container | MicroVM (Firecracker) |\n|---|---|---|---|\n| Root required | No | Yes* | Yes (KVM) |\n| Image build | No | Yes | Yes |\n| Startup time | ~5 ms | ~200 ms | ~100 ms |\n| Kernel | Shared | Shared | Separate guest |\n| Filesystem isolation | Landlock + seccomp COW | Overlay | Block-level |\n| Network isolation | Landlock + seccomp notif | Network namespace | TAP device |\n| HTTP-level ACL | Method + host + path rules | N/A | N/A |\n| Syscall filtering | seccomp-bpf | seccomp | N/A |\n| Resource limits | seccomp notif + SIGSTOP | cgroup v2 | VM config |\n\n\\* Rootless containers exist but require user namespace support and `/etc/subuid` configuration.\n\n## Architecture\n\nSandlock is implemented in **Rust** for performance and safety:\n\n- **sandlock-core** — Rust library: Landlock, seccomp, supervisor, COW, pipeline\n- **sandlock-cli** — Rust CLI binary (`sandlock run ...`)\n- **sandlock-ffi** — C ABI shared library (`libsandlock_ffi.so`)\n- **Python SDK** — ctypes bindings to the FFI library\n\n```\n                    ┌─────────────┐\n                    │  Python SDK │  ctypes FFI\n                    │  (sandlock) │──────────────┐\n                    └─────────────┘              │\n                                                 ▼\n┌──────────────┐    ┌──────────────────────────────┐\n│ sandlock CLI │───\u003e│       libsandlock_ffi.so      │\n└──────────────┘    └──────────────┬───────────────┘\n                                   │\n                    ┌──────────────▼───────────────┐\n                    │        sandlock-core          │\n                    │  Landlock · seccomp · COW ·   │\n                    │  pipeline · policy_fn · vDSO  │\n                    └──────────────────────────────┘\n```\n\n## Requirements\n\n- **Linux 6.12+** (Landlock ABI v6), **Rust 1.70+** (to build)\n- **Python 3.8+** (optional, for Python SDK)\n- No root, no cgroups\n\n| Feature | Minimum kernel |\n|---|---|\n| seccomp user notification | 5.6 |\n| Landlock filesystem rules | 5.13 |\n| Landlock TCP port rules | 6.7 (ABI v4) |\n| Landlock IPC scoping | 6.12 (ABI v6) |\n\n## Install\n\n### From source\n\n```bash\n# Build the Rust binary and shared library\ncargo build --release\n\n# Install Python SDK (auto-builds Rust FFI library)\ncd python \u0026\u0026 pip install -e .\n```\n\n### CLI only\n\n```bash\ncargo install --path crates/sandlock-cli\n```\n\n## Quick Start\n\n### CLI\n\n```bash\n# Basic confinement\nsandlock run -r /usr -r /lib -w /tmp -- ls /tmp\n\n# Interactive shell\nsandlock run -i -r /usr -r /lib -r /lib64 -r /bin -r /etc -w /tmp -- /bin/sh\n\n# Resource limits + timeout\nsandlock run -m 512M -P 20 -t 30 -- ./compute.sh\n\n# Outbound allowlist — restrict to one host on one port\nsandlock run --net-allow api.openai.com:443 -r /usr -r /lib -r /etc -- python3 agent.py\n\n# Multiple ports for one host, plus a separate any-IP port\nsandlock run --net-allow github.com:22,443 --net-allow :8080 \\\n  -r /usr -r /lib -r /etc -- python3 agent.py\n\n# Wildcard port — `host:*` permits every port to the host\nsandlock run --net-allow github.com:* -r /usr -r /lib -r /etc -- ssh user@github.com\n\n# Unrestricted outbound — `:*` opens any host and any TCP port. For full\n# egress add a UDP wildcard via the `udp://*:*` scheme.\nsandlock run --net-allow :* --net-allow udp://*:* \\\n  -r /usr -r /lib -r /etc -- ./client\n\n# UDP — scheme prefix gates the protocol and scopes the destination\n# (e.g. DNS to 1.1.1.1, plus TCP HTTPS to anywhere)\nsandlock run --net-allow udp://1.1.1.1:53 --net-allow :443 \\\n  -r /usr -r /lib -r /etc -- ./client\n\n# Ping — kernel ping socket (SOCK_DGRAM) gated by net.ipv4.ping_group_range\nsandlock run --net-allow icmp://github.com -r /usr -r /lib -r /etc -- ping github.com\n\n# HTTP-level ACL (method + host + path rules via transparent proxy)\n# HTTP rules with concrete hosts auto-extend --net-allow with host:80,443\nsandlock run \\\n  --http-allow \"GET docs.python.org/*\" \\\n  --http-allow \"POST api.openai.com/v1/chat/completions\" \\\n  --http-deny \"* */admin/*\" \\\n  -r /usr -r /lib -r /etc -- python3 agent.py\n\n# HTTPS MITM with user-provided CA (enables ACL on port 443)\n# Generate a CA, add the cert to the sandbox's trust store\n# (e.g. /etc/ssl/certs/), then pass both files here.\nsandlock run \\\n  --http-allow \"POST api.openai.com/v1/*\" \\\n  --http-ca ca.pem --http-key ca-key.pem \\\n  -r /usr -r /lib -r /etc -- python3 agent.py\n\n# Server listening on a port (Landlock --net-bind, separate from --net-allow)\nsandlock run --net-bind 8080 -r /usr -r /lib -r /etc -- python3 server.py\n\n# Clean environment\nsandlock run --clean-env --env CC=gcc \\\n  -r /usr -r /lib -w /tmp -- make\n\n# Deterministic execution (frozen time + seeded randomness)\nsandlock run --time-start \"2000-01-01T00:00:00Z\" --random-seed 42 -- ./build.sh\n\n# Port virtualization (multiple sandboxes can bind the same port)\nsandlock run --port-remap --net-bind 6379 -r /usr -r /lib -r /etc -- redis-server --port 6379\n\n# Port virtualization with named sandboxes (enables network discovery)\nsandlock run --name api.local --port-remap --net-bind 8080 -r /usr -r /lib -r /etc -- python3 server.py\nsandlock run --name web.local --port-remap --net-bind 8080 -r /usr -r /lib -r /etc -- python3 server.py\n\n# List all running sandboxes\nsandlock list\n\n# Kill a running sandbox by name\nsandlock kill web.local\n\n# Chroot with per-sandbox mount (no kernel bind mount needed)\nsandlock run --chroot ./rootfs --fs-mount /work:/tmp/sandbox/work -- /bin/sh\n\n# COW filesystem (writes captured, committed on success)\nsandlock run --workdir /opt/project -r /usr -r /lib -- python3 task.py\n\n# Dry-run (show what files would change, then discard)\nsandlock run --dry-run --workdir . -w . -r /usr -r /lib -r /bin -r /etc -- make build\n\n# Use a saved profile\nsandlock run -p build -- make -j4\n\n# No-supervisor mode (Landlock + deny-only seccomp, no supervisor process)\nsandlock run --no-supervisor -r /usr -r /lib -r /lib64 -r /bin -w /tmp -- ./script.sh\n\n# Nested sandboxing: confine sandlock's own supervisor\nsandlock run --no-supervisor -r /proc -r /usr -r /lib -r /lib64 -r /bin -r /etc -w /tmp -- \\\n  sandlock run -r /usr -w /tmp -- untrusted-command\n```\n\n### Python API\n\n```python\nfrom sandlock import Sandbox, confine\n\nsandbox = Sandbox(\n    fs_writable=[\"/tmp/sandbox\"],\n    fs_readable=[\"/usr\", \"/lib\", \"/etc\"],\n    max_memory=\"256M\",\n    max_processes=10,\n    clean_env=True,\n)\n\n# Run a command (with optional timeout in seconds)\nresult = sandbox.run([\"python3\", \"-c\", \"print('hello')\"], timeout=30)\nassert result.success\nassert b\"hello\" in result.stdout\n\n# HTTP ACL: only allow specific API calls\nagent = Sandbox(\n    fs_readable=[\"/usr\", \"/lib\", \"/etc\"],\n    http_allow=[\"POST api.openai.com/v1/chat/completions\"],\n    http_deny=[\"* */admin/*\"],\n)\nresult = agent.run([\"python3\", \"agent.py\"])\n\n# Chroot with per-sandbox mount (Docker-style -v, no root needed)\nchrooted = Sandbox(\n    chroot=\"/opt/rootfs\",\n    fs_mount={\"/work\": \"/tmp/sandbox-1/work\"},  # maps /work inside chroot\n    fs_readable=[\"/usr\", \"/bin\", \"/lib\", \"/etc\"],\n    cwd=\"/work\",\n)\nresult = chrooted.run([\"python3\", \"task.py\"])\n\n# Port virtualization: query port mappings while sandbox is running\nsb = Sandbox(port_remap=True, fs_readable=[\"/usr\", \"/lib\", \"/etc\"], name=\"api.local\")\n# sb.ports() returns {virtual_port: real_port} while running\n\n# Confine the current process (Landlock filesystem only, irreversible)\nconfine(Sandbox(fs_readable=[\"/usr\", \"/lib\"], fs_writable=[\"/tmp\"]))\n\n# Dry-run: see what files would change, then discard\nsandbox = Sandbox(fs_writable=[\".\"], workdir=\".\", fs_readable=[\"/usr\", \"/lib\", \"/bin\", \"/etc\"])\nresult = sandbox.dry_run([\"make\", \"build\"])\nfor c in result.changes:\n    print(f\"{c.kind}  {c.path}\")  # A=added, M=modified, D=deleted\n```\n\n### Pipeline\n\nChain sandboxed stages with the `|` operator — each stage has its own\nindependent sandbox config. Data flows through kernel pipes.\n\n```python\nfrom sandlock import Sandbox\n\ntrusted = Sandbox(fs_readable=[\"/usr\", \"/lib\", \"/bin\", \"/etc\", \"/opt/data\"])\nrestricted = Sandbox(fs_readable=[\"/usr\", \"/lib\", \"/bin\", \"/etc\"])\n\n# Reader can access data, processor cannot\nresult = (\n    trusted.cmd([\"cat\", \"/opt/data/secret.csv\"])\n    | restricted.cmd([\"tr\", \"a-z\", \"A-Z\"])\n).run()\nassert b\"SECRET\" in result.stdout\n```\n\n**XOA pattern** (eXecute Over Architecture) — planner generates code,\nexecutor runs it with data access but no network:\n\n```python\nplanner = Sandbox(fs_readable=[\"/usr\", \"/lib\", \"/bin\", \"/etc\"])\nexecutor = Sandbox(fs_readable=[\"/usr\", \"/lib\", \"/bin\", \"/etc\", \"/data\"])\n\nresult = (\n    planner.cmd([\"python3\", \"-c\", \"print('cat /data/input.txt')\"])\n    | executor.cmd([\"sh\"])\n).run()\n```\n\n### Dynamic Policy (policy_fn)\n\nInspect syscall events at runtime and adjust permissions on the fly.\nEvents carry syscall name, category, PID, network destination (for\n`connect`/`sendto`/`bind`), and `argv` (for `execve`). The callback\nreturns a verdict to allow, deny, or audit.\n\n```python\nfrom sandlock import Sandbox\nimport errno\n\ndef on_event(event, ctx):\n    # Block download tools by argv\n    if event.syscall == \"execve\" and event.argv_contains(\"curl\"):\n        return True  # deny\n\n    # Deny connections to a specific IP\n    if event.syscall == \"connect\" and event.host == \"10.0.0.5\":\n        return errno.EACCES\n\n    # Lock down once the program has finished starting up\n    if event.syscall == \"execve\":\n        ctx.restrict_network([])           # block all network\n        ctx.deny_path(\"/etc/shadow\")       # dynamic fs deny\n\n    # Audit every file access (allow but flag)\n    if event.category == \"file\":\n        return \"audit\"\n\n    return 0  # allow\n\nsandbox = Sandbox(\n    fs_readable=[\"/usr\", \"/lib\", \"/etc\"],\n    net_allow=[\"api.example.com:443\"],\n    policy_fn=on_event,\n)\nresult = sandbox.run([\"python3\", \"agent.py\"])\n```\n\n**Verdicts:** `0`/`False` = allow, `True`/`-1` = deny (EPERM),\npositive int = deny with errno, `\"audit\"`/`-2` = allow + flag.\n\n**Event fields:** `syscall`, `category` (file/network/process/memory),\n`pid`, `parent_pid`, `host`, `port`, `argv`, `denied`.\n\n\u003e **TOCTOU NOTE** Per `seccomp_unotify(2)`, the kernel\n\u003e re-reads user-memory pointers after `Continue`. Sandlock handles this\n\u003e in two places:\n\u003e\n\u003e - **Path strings are not exposed on events.** Path-based access control\n\u003e   belongs in static Landlock rules (`fs_readable` / `fs_writable` /\n\u003e   `fs_denied`) — kernel-enforced and TOCTOU-immune. Use\n\u003e   `ctx.deny_path()` for runtime additions.\n\u003e - **`event.argv` is exposed and TOCTOU-safe.** Before exposing\n\u003e   `argv` to `policy_fn` or returning `Continue` for an\n\u003e   `execve`, the supervisor freezes every task in `ProcessIndex`,\n\u003e   including peer processes that may alias argv through shared memory.\n\u003e   With `policy_fn` active, fork-like syscalls are traced for one\n\u003e   ptrace creation event, so children are registered in `ProcessIndex`\n\u003e   before they can run user code. If the freeze or creation tracking\n\u003e   cannot be established (e.g., YAMA blocks ptrace), the syscall is\n\u003e   denied with `EPERM`; the safety invariant is never silently relaxed.\n\n**Context methods:**\n- `ctx.restrict_network(ips)` / `ctx.grant_network(ips)` — network control\n- `ctx.restrict_max_memory(bytes)` / `ctx.restrict_max_processes(n)` — resource limits\n- `ctx.deny_path(path)` / `ctx.allow_path(path)` — dynamic filesystem restriction\n- `ctx.restrict_pid_network(pid, ips)` — per-PID network override\n\n**Held syscalls** (child blocked until callback returns): `execve`,\n`connect`, `sendto`, `bind`, `openat`.\n\n### Rust API\n\n```rust\nuse sandlock_core::{confine, Confinement, Sandbox, Stage};\nuse sandlock_core::sandbox::ByteSize;\nuse sandlock_core::policy_fn::Verdict;\n\n// Basic run\nlet mut sandbox = Sandbox::builder()\n    .fs_read(\"/usr\").fs_read(\"/lib\")\n    .fs_write(\"/tmp\")\n    .max_memory(ByteSize::mib(256))\n    .name(\"hello-box\")\n    .build()?;\nlet result = sandbox.run(\u0026[\"echo\", \"hello\"]).await?;\nassert!(result.success());\n\n// HTTP ACL: restrict API access at the HTTP level\nlet mut agent = Sandbox::builder()\n    .fs_read(\"/usr\").fs_read(\"/lib\").fs_read(\"/etc\")\n    .http_allow(\"POST api.openai.com/v1/chat/completions\")\n    .http_deny(\"* */admin/*\")\n    .name(\"agent-box\")\n    .build()?;\nlet result = agent.run(\u0026[\"python3\", \"agent.py\"]).await?;\n\n// Confine the current process (Landlock filesystem only, irreversible)\nlet confinement = Confinement::builder()\n    .fs_read(\"/usr\").fs_read(\"/lib\")\n    .fs_write(\"/tmp\")\n    .build();\nconfine(\u0026confinement)?;\n\n// Pipeline\nlet producer = Sandbox::builder()\n    .fs_read(\"/usr\").fs_read(\"/lib\").fs_read(\"/bin\")\n    .build()?;\nlet consumer = producer.clone();\nlet result = (\n    Stage::new(\u0026producer, \u0026[\"echo\", \"hello\"])\n    | Stage::new(\u0026consumer, \u0026[\"tr\", \"a-z\", \"A-Z\"])\n).run(None).await?;\n\n// Dynamic policy\nlet mut dynamic = Sandbox::builder()\n    .fs_read(\"/usr\").fs_read(\"/lib\")\n    .policy_fn(|event, ctx| {\n        if event.argv_contains(\"curl\") {\n            return Verdict::Deny;\n        }\n        if event.syscall == \"execve\" {\n            ctx.restrict_network(\u0026[]);\n            ctx.deny_path(\"/etc/shadow\");\n        }\n        Verdict::Allow\n    })\n    .build()?;\nlet result = dynamic.run(\u0026[\"python3\", \"agent.py\"]).await?;\n```\n\n## Profiles\n\nSave reusable sandbox profiles as TOML files in\n`~/.config/sandlock/profiles/`. Profiles use a sectioned schema; top-level\nflat keys such as `fs_readable = [...]` are rejected. Pass a sandbox instance\nname with `--name` when you need a stable virtual hostname.\n\n```toml\n# ~/.config/sandlock/profiles/build.toml\n[program]\nexec = \"make\"\nargs = [\"-j4\"]\nclean_env = true\nenv = { CC = \"gcc\", LANG = \"C.UTF-8\" }\n\n[filesystem]\nread = [\"/usr\", \"/lib\", \"/lib64\", \"/bin\", \"/etc\"]\nwrite = [\"/tmp/work\"]\n\n[limits]\nmemory = \"512M\"\nprocesses = 50\n\n[syscalls]\nextra_deny = []\n```\n\n```bash\nsandlock profile list\nsandlock profile show build\nsandlock run -p build        # uses [program].exec + args\nsandlock run -p build -- make test  # trailing command overrides [program]\n```\n\n## How It Works\n\nSandlock applies confinement in sequence after `fork()`:\n\n```\nParent                              Child\n  │  fork()                           │\n  │──────────────────────────────────\u003e│\n  │                                   ├─ 1. setpgid(0,0)\n  │                                   ├─ 2. Optional: chdir(cwd)\n  │                                   ├─ 3. NO_NEW_PRIVS\n  │                                   ├─ 4. Landlock (fs + net + IPC)\n  │                                   ├─ 5. seccomp filter (deny + notif)\n  │                                   │     └─ send notif fd ──\u003e Parent\n  │  receive notif fd                 ├─ 6. Wait for \"ready\" signal\n  │  start supervisor (tokio)         ├─ 7. Close fds 3+\n  │  optional: vDSO patching          └─ 8. exec(cmd)\n  │  optional: policy_fn thread\n  │  optional: CPU throttle task\n```\n\n### Seccomp Supervisor\n\nThe async notification supervisor (tokio) handles intercepted syscalls:\n\n| Syscall | Handler |\n|---|---|\n| `clone/fork/vfork` | Process count enforcement |\n| `mmap/munmap/brk/mremap` | Memory limit tracking |\n| `connect/sendto/sendmsg` | IP allowlist + on-behalf execution + HTTP ACL redirect |\n| `bind` | On-behalf bind + port remapping |\n| `openat` | /proc virtualization, COW interception |\n| `unlinkat/mkdirat/renameat2` | COW write interception |\n| `execve/execveat` | policy_fn hold + vDSO re-patching |\n| `getrandom` | Deterministic PRNG injection |\n| `clock_nanosleep/timer_settime` | Timer adjustment for frozen time |\n| `getdents64` | PID filtering, COW directory merging |\n| `getsockname` | Port remap translation |\n\n### Custom Handlers\n\nDownstream Rust crates can append their own seccomp-notification\nhandlers to the supervisor chain alongside the builtins, registering\nfor any syscall they care about via the `Handler` trait and\n`Sandbox::run_with_handlers`. The builtin chain runs first, so\nuser handlers cannot subvert confinement; the registration step also\nrejects handlers on syscalls in the default blocklist or\n`extra_deny_syscalls`. See\n[`docs/extension-handlers.md`](docs/extension-handlers.md) for the\nfull API, ordering semantics, and state patterns.\n\n### COW Filesystem\n\nTwo modes of copy-on-write filesystem isolation:\n\n**Seccomp COW** (default when `workdir` is set): Intercepts filesystem\nsyscalls via seccomp notification. Writes go to an upper directory;\nreads resolve upper-then-lower. No mount namespace, no root. Committed\non exit, aborted on error.\n\n**OverlayFS COW**: Uses kernel OverlayFS in a user namespace. Requires\nunprivileged user namespaces to be enabled.\n\n**Dry-run mode**: `--dry-run` runs the command, inspects the COW layer\nfor changes (added/modified/deleted files), prints a summary, then\naborts — leaving the workdir completely untouched. Useful for previewing\nwhat a command would do before committing.\n\n### COW Fork \u0026 Map-Reduce\n\nInitialize expensive state once, then fork COW clones that share memory.\nEach clone uses raw `fork(2)` with shared copy-on-write pages. 1000\nclones in ~530ms, ~1,900 forks/sec.\n\nEach clone's stdout is captured via its own pipe. `reduce()` reads all\npipes and feeds combined output to a reducer's stdin — fully pipe-based\ndata flow with no temp files.\n\n```python\nfrom sandlock import Sandbox\n\ndef init():\n    global model, data\n    model = load_model()          # 2 GB, loaded once\n    data = preprocess_dataset()\n\ndef work(clone_id):\n    shard = data[clone_id::4]\n    print(sum(shard))             # stdout → per-clone pipe\n\n# Map: fork 4 clones with a separate sandbox config\nmapper = Sandbox(\n    fs_readable=[\"/usr\", \"/lib\", \"/bin\", \"/etc\", \"/data\"],\n    init_fn=init,\n    work_fn=work,\n)\nclones = mapper.fork(4)\n\n# Reduce: pipe clone outputs to reducer stdin\nreducer = Sandbox(fs_readable=[\"/usr\", \"/lib\", \"/bin\", \"/etc\"])\nresult = reducer.reduce(\n    [\"python3\", \"-c\", \"import sys; print(sum(int(l) for l in sys.stdin))\"],\n    clones,\n)\nprint(result.stdout)  # b\"total\\n\"\n```\n\n```rust\nlet mut mapper = Sandbox::builder()\n    .fs_read(\"/usr\").fs_read(\"/lib\").fs_read(\"/bin\").fs_read(\"/etc\")\n    .fs_read(\"/data\")\n    .name(\"mapper\")\n    .init_fn(|| { load_data(); })\n    .work_fn(|id| { println!(\"{}\", compute(id)); })\n    .build()?;\nlet mut clones = mapper.fork(4).await?;\n\nlet reducer = Sandbox::builder()\n    .fs_read(\"/usr\").fs_read(\"/lib\").fs_read(\"/bin\").fs_read(\"/etc\")\n    .name(\"reducer\")\n    .build()?;\nlet result = reducer.reduce(\n    \u0026[\"python3\", \"-c\", \"import sys; print(sum(int(l) for l in sys.stdin))\"],\n    \u0026mut clones,\n).await?;\n```\n\nMap and reduce run in separate sandboxes with independent configs —\nthe mapper has data access, the reducer doesn't. Each clone inherits\nLandlock + seccomp confinement. `CLONE_ID=0..N-1` is set automatically.\n\n### Network Model\n\nOutbound traffic is gated by a single endpoint allowlist that names\n**protocol × destination**. Each `--net-allow` rule is one of:\n\n```\n--net-allow \u003cspec\u003e          repeatable; no rules = deny all outbound\n  bare form  host:port[,port,...] / :port / *:port / host:* / :* / *:*   (TCP)\n  tcp://     same suffix grammar — explicit TCP\n  udp://     same suffix grammar — UDP (`udp://*:*` opens any UDP)\n  icmp://    host or `*`, no port — kernel ping socket (SOCK_DGRAM)\n```\n\nMultiple rules are OR'd. A destination is permitted iff some rule\nmatches the **same protocol** as the socket plus the destination IP\nand port (port is N/A for ICMP).\n\n**Protocol gating** falls out of rule presence per scheme:\n\n  * No UDP rule → UDP socket creation is denied at the seccomp layer.\n  * No ICMP rule → kernel ping socket creation (SOCK_DGRAM + IPPROTO_ICMP)\n    is denied at the seccomp layer.\n  * Raw ICMP (SOCK_RAW + IPPROTO_ICMP) is **never exposed** — packet\n    crafting is out of scope. Workloads that need ping should rely on\n    the host's `net.ipv4.ping_group_range` and use the dgram path\n    above (`--net-allow icmp://...`).\n  * TCP is always permitted at the syscall level; destinations are\n    governed by Landlock and/or the on-behalf path.\n\n**Defaults.** With no `--net-allow` and no HTTP ACL flags, Landlock\ndenies every TCP `connect()`, UDP / ICMP / raw socket creation are\ndenied at the seccomp layer, and there is no on-behalf path active.\nFor unrestricted TCP egress, opt in explicitly with\n`--net-allow :*`; for any UDP, add `--net-allow udp://*:*`.\n\n**Resolution.** Concrete hostnames are resolved once at sandbox start\nand pinned in a synthetic `/etc/hosts` (across all protocols). The\nsynthetic file replaces the real one only when at least one rule has\na concrete host; pure `:port` / `udp://*:*` / `icmp://*` rules leave\nthe real `/etc/hosts` and DNS visible.\n\n**Wildcards.** Hostnames are matched literally — `--net-allow\n*.example.com:443` is **not** supported, list each domain you need.\nThe `*` token is allowed as the host (alias for empty: `*:port` ≡\n`:port`) and as the port for TCP/UDP rules (`host:*`, `:*`, `*:*`,\n`udp://*:*`). Mixing `*` with concrete ports (`host:80,*`) is\nrejected. When any TCP rule uses the all-ports wildcard, Landlock no\nlonger filters TCP connect at the kernel level (it cannot express\n\"every port\" without enumerating 65535 rules); the on-behalf path\nbecomes the sole enforcer, and for `:*` it short-circuits to\nallow-all.\n\n**Implementation.** Two enforcement paths:\n\n  * **Direct path** — pure `:port` TCP policies (no concrete host)\n    and no HTTP ACL. Landlock enforces the TCP port allowlist at the\n    kernel level; no per-syscall overhead. UDP and ICMP are not\n    covered by Landlock and always use the on-behalf path when allowed.\n  * **On-behalf path** — any concrete host, any HTTP ACL rule, or any\n    UDP / ICMP rule. Seccomp traps `connect()`, `sendto()`, `sendmsg()`,\n    and `sendmmsg()`; the supervisor dups the child fd, queries\n    `getsockopt(SOL_SOCKET, SO_PROTOCOL)` to learn whether the socket\n    is TCP / UDP / ICMP, then checks the destination against that\n    protocol's resolved allowlist before performing the syscall.\n    The HTTP/HTTPS proxy redirect (when configured) happens here too.\n\n**HTTP / HTTPS interception.** `--http-allow` / `--http-deny` route\nmatching ports through a transparent proxy. Each rule with a concrete\nhost auto-extends `--net-allow` with `host:80` (and `host:443` when\n`--http-ca` is set) so the proxy's intercept ports are reachable;\nwildcard hosts auto-add `:80` / `:443` (any IP). All auto-added\nentries are TCP. HTTPS MITM is opt-in: pass `--http-ca \u003ccert\u003e` and\n`--http-key \u003ckey\u003e` for a CA *you generate* and trust inside the\nsandbox (typically install the cert into the workload's\n`/etc/ssl/certs/`). Without `--http-ca`, port 443 is not intercepted\n— `--net-allow host:443` permits raw TLS to the host with no content\ninspection.\n\n**Bind.** `--net-bind \u003cport\u003e` is independent from `--net-allow` and\ngoverns server-side `bind()`. Landlock enforces it (TCP only);\n`--port-remap` adds on-behalf virtualization for binding.\n\n**AF_UNIX sockets** are governed by Landlock's\n`LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET`, independent from `--net-allow`.\n\n### Port Virtualization\n\nEach sandbox gets a full virtual port space. Multiple sandboxes can bind\nthe same port without conflicts. The supervisor performs `bind()` on behalf\nof the child via `pidfd_getfd` (TOCTOU-safe). When a port conflicts, a\ndifferent real port is allocated transparently. `/proc/net/tcp` is filtered\nto only show the sandbox's own ports.\n\nWhen `--port-remap` is enabled, the sandbox registers its state in a\nshared registry (`/dev/shm`). Use `sandlock list` to see all running\nsandboxes and `sandlock kill` to stop them:\n\n```\n$ sandlock list\nNAME                    PID  PORTS\napi.local            12345  8080\nweb.local            12346  8080 -\u003e 35299\n\n$ sandlock kill web.local\nKilled sandbox 'web.local' (PID 12346)\n```\n\nThis enables external reverse proxies (nginx, envoy) to route traffic\nby name to the correct real port.\n\n## Performance\n\nBenchmarked on a typical Linux workstation:\n\n| Workload | Bare metal | Sandlock | Docker | Sandlock overhead |\n|---|---|---|---|---|\n| `/bin/echo` startup | 2 ms | 7 ms | 307 ms | 5 ms (44x faster than Docker) |\n| Redis SET (100K ops) | 82K rps | 80K rps | 52K rps | 97.1% of bare metal |\n| Redis GET (100K ops) | 79K rps | 77K rps | 53K rps | 97.1% of bare metal |\n| Redis p99 latency | 0.5 ms | 0.6 ms | 1.5 ms | ~2.5x lower than Docker |\n| COW fork ×1000 | — | 530 ms | — | 530μs/fork, ~1,900 forks/sec |\n\n## Testing\n\n```bash\n# Rust tests\ncargo test --release\n\n# Python tests\ncd python \u0026\u0026 pip install -e . \u0026\u0026 pytest tests/\n```\n\n## Sandbox Reference\n\nThe full `Sandbox` configuration reference — every field, default,\nand grouping — lives in [`docs/sandbox-reference.md`](docs/sandbox-reference.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmultikernel%2Fsandlock","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmultikernel%2Fsandlock","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmultikernel%2Fsandlock/lists"}