{"id":47745114,"url":"https://github.com/multikernel/sandlock","last_synced_at":"2026-04-07T01:00:19.594Z","repository":{"id":344281045,"uuid":"1181262173","full_name":"multikernel/sandlock","owner":"multikernel","description":"Lightweight process-based sandbox for Linux, no container, no VM, no root.","archived":false,"fork":false,"pushed_at":"2026-04-03T01:54:46.000Z","size":2117,"stargazers_count":36,"open_issues_count":4,"forks_count":5,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-03T09:29:26.307Z","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-04-03T01:54:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/multikernel/sandlock","commit_stats":null,"previous_names":["multikernel/sandlock"],"tags_count":9,"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":31418287,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T20:09:54.854Z","status":"ssl_error","status_checked_at":"2026-04-04T20:09:44.350Z","response_time":60,"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-04-07T01:00:19.462Z","avatar_url":"https://github.com/multikernel.png","language":"Rust","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| 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# Domain-based network isolation\nsandlock run --net-allow-host api.openai.com -r /usr -r /lib -r /etc -- python3 agent.py\n\n# TCP port restrictions (Landlock)\nsandlock run --net-bind 8080 --net-connect 443 -r /usr -r /lib -r /etc -- python3 server.py\n\n# IPC scoping + clean environment\nsandlock run --isolate-ipc --isolate-signals --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# 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\n### Python API\n\n```python\nfrom sandlock import Sandbox, Policy\n\npolicy = Policy(\n    fs_writable=[\"/tmp/sandbox\"],\n    fs_readable=[\"/usr\", \"/lib\", \"/etc\"],\n    max_memory=\"256M\",\n    max_processes=10,\n    isolate_ipc=True,\n    clean_env=True,\n)\n\n# Run a command\nresult = Sandbox(policy).run([\"python3\", \"-c\", \"print('hello')\"])\nassert result.success\nassert b\"hello\" in result.stdout\n\n# Dry-run: see what files would change, then discard\npolicy = Policy(fs_writable=[\".\"], workdir=\".\", fs_readable=[\"/usr\", \"/lib\", \"/bin\", \"/etc\"])\nresult = Sandbox(policy).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 policy. Data flows through kernel pipes.\n\n```python\nfrom sandlock import Sandbox, Policy\n\ntrusted = Policy(fs_readable=[\"/usr\", \"/lib\", \"/bin\", \"/etc\", \"/opt/data\"])\nrestricted = Policy(fs_readable=[\"/usr\", \"/lib\", \"/bin\", \"/etc\"])\n\n# Reader can access data, processor cannot\nresult = (\n    Sandbox(trusted).cmd([\"cat\", \"/opt/data/secret.csv\"])\n    | Sandbox(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 = Policy(fs_readable=[\"/usr\", \"/lib\", \"/bin\", \"/etc\"])\nexecutor = Policy(fs_readable=[\"/usr\", \"/lib\", \"/bin\", \"/etc\", \"/data\"])\n\nresult = (\n    Sandbox(planner).cmd([\"python3\", \"-c\", \"print('cat /data/input.txt')\"])\n    | Sandbox(executor).cmd([\"sh\"])\n).run()\n```\n\n### Dynamic Policy (policy_fn)\n\nInspect syscall events at runtime and adjust permissions on the fly.\nEach event includes rich metadata: path, host, port, argv, category,\nparent PID. The callback returns a verdict to allow, deny, or audit.\n\n```python\nfrom sandlock import Sandbox, Policy\nimport errno\n\ndef on_event(event, ctx):\n    # Block download tools\n    if event.syscall == \"execve\" and event.argv_contains(\"curl\"):\n        return True  # deny\n\n    # Custom errno for sensitive files\n    if event.category == \"file\" and event.path_contains(\"/secret\"):\n        return errno.EACCES\n\n    # Restrict network after setup phase\n    if event.syscall == \"execve\" and event.path_contains(\"untrusted\"):\n        ctx.restrict_network([])\n        ctx.deny_path(\"/etc/shadow\")\n\n    # Audit file access (allow but flag)\n    if event.category == \"file\":\n        return \"audit\"\n\n    return 0  # allow\n\npolicy = Policy(\n    fs_readable=[\"/usr\", \"/lib\", \"/etc\"],\n    net_allow_hosts=[\"api.example.com\"],\n)\nresult = Sandbox(policy, policy_fn=on_event).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`, `path`, `host`, `port`, `argv`, `denied`.\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::{Policy, Sandbox, Pipeline, Stage};\n\n// Basic run\nlet policy = Policy::builder()\n    .fs_read(\"/usr\").fs_read(\"/lib\")\n    .fs_write(\"/tmp\")\n    .max_memory(ByteSize::mib(256))\n    .build()?;\nlet result = Sandbox::run(\u0026policy, \u0026[\"echo\", \"hello\"]).await?;\nassert!(result.success());\n\n// Pipeline\nlet result = (\n    Stage::new(\u0026policy_a, \u0026[\"echo\", \"hello\"])\n    | Stage::new(\u0026policy_b, \u0026[\"tr\", \"a-z\", \"A-Z\"])\n).run(None).await?;\n\n// Dynamic policy\nuse sandlock_core::policy_fn::Verdict;\nlet policy = Policy::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()?;\n```\n\n## Profiles\n\nSave reusable policies as TOML files in `~/.config/sandlock/profiles/`:\n\n```toml\n# ~/.config/sandlock/profiles/build.toml\nfs_writable = [\"/tmp/work\"]\nfs_readable = [\"/usr\", \"/lib\", \"/lib64\", \"/bin\", \"/etc\"]\nclean_env = true\nisolate_ipc = true\nmax_memory = \"512M\"\nmax_processes = 50\n\n[env]\nCC = \"gcc\"\nLANG = \"C.UTF-8\"\n```\n\n```bash\nsandlock profile list\nsandlock profile show build\nsandlock run -p build -- make -j4\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(workdir)\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 |\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### 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 fork uses raw `fork(2)` (bypasses seccomp notification) for minimal\noverhead. 1000 clones 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, Policy\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 separate policies\nmapper = Sandbox(data_policy, init_fn=init, work_fn=work)\nclones = mapper.fork(4)\n\n# Reduce: pipe clone outputs to reducer stdin\nresult = Sandbox(reduce_policy).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::new_with_fns(\u0026map_policy,\n    || { load_data(); },\n    |id| { println!(\"{}\", compute(id)); },\n)?;\nlet mut clones = mapper.fork(4).await?;\n\nlet reducer = Sandbox::new(\u0026reduce_policy)?;\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 policies —\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### 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\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## Policy Reference\n\n```python\nPolicy(\n    # Filesystem (Landlock)\n    fs_writable=[\"/tmp\"],          # Read/write access\n    fs_readable=[\"/usr\", \"/lib\"],  # Read-only access\n    fs_denied=[\"/proc/kcore\"],     # Explicitly denied\n\n    # Syscall filtering (seccomp)\n    deny_syscalls=None,            # None = default blocklist\n    allow_syscalls=None,           # Allowlist mode (stricter)\n\n    # Network\n    net_allow_hosts=[\"api.example.com\"],  # Domain allowlist\n    net_bind=[8080],               # TCP bind ports (Landlock ABI v4+)\n    net_connect=[443],             # TCP connect ports\n\n    # Socket restrictions\n    no_raw_sockets=True,           # Block SOCK_RAW (default)\n    no_udp=False,                  # Block SOCK_DGRAM\n\n    # IPC scoping (Landlock ABI v6+)\n    isolate_ipc=False,             # Block abstract UNIX sockets to host\n    isolate_signals=False,         # Block signals to host processes\n\n    # Resources\n    max_memory=\"512M\",             # Memory limit\n    max_processes=64,              # Fork count limit\n    max_cpu=50,                    # CPU throttle (% of one core)\n    max_open_files=256,            # fd limit\n    port_remap=False,              # Virtual port space\n\n    # Deterministic execution\n    time_start=\"2000-01-01T00:00:00\",  # Frozen time\n    random_seed=42,                # Deterministic getrandom()\n    no_randomize_memory=False,     # Disable ASLR\n    no_huge_pages=False,           # Disable THP\n    no_coredump=False,             # Disable core dumps\n\n    # Environment\n    clean_env=False,               # Minimal env\n    env={\"KEY\": \"value\"},          # Override env vars\n\n    # COW isolation\n    workdir=None,                  # Working directory + COW\n    fs_isolation=FsIsolation.NONE, # NONE | OVERLAYFS | BRANCHFS\n    on_exit=BranchAction.COMMIT,   # COMMIT | ABORT | KEEP\n    on_error=BranchAction.ABORT,\n\n    # Misc\n    chroot=None,\n    close_fds=True,\n    privileged=False,              # UID 0 in user namespace\n)\n```\n","funding_links":[],"categories":["Sandboxing \u0026 Isolation"],"sub_categories":[],"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"}