|
| 1 | +# sandlock-oci |
| 2 | + |
| 3 | +An OCI runtime shim for the [sandlock](https://github.com/multikernel/sandlock) |
| 4 | +sandbox. It implements the OCI Runtime Specification command interface so that |
| 5 | +container runtimes (containerd, CRI-O, Kubernetes) can use sandlock as a |
| 6 | +drop-in low-level runtime, with no kernel namespaces and no cgroups. |
| 7 | + |
| 8 | +Where `runc` builds containers from namespaces, cgroups, and `pivot_root`, |
| 9 | +`sandlock-oci` confines the workload with **Landlock** (filesystem, network, |
| 10 | +and IPC), **seccomp-bpf** (syscall filtering), and **seccomp user |
| 11 | +notification** (resource limits, IP enforcement, and `/proc` virtualization). |
| 12 | +The result is a namespace-less container that runs unprivileged. |
| 13 | + |
| 14 | +## How it fits in |
| 15 | + |
| 16 | +`sandlock-oci` is a thin OCI-compatible front end over `sandlock-core`. It |
| 17 | +parses an OCI bundle, translates `config.json` into a sandlock policy, and |
| 18 | +drives the sandbox lifecycle on behalf of a higher-level runtime. |
| 19 | + |
| 20 | +``` |
| 21 | + containerd / CRI-O / kubelet |
| 22 | + │ (OCI runtime calls) |
| 23 | + ▼ |
| 24 | + sandlock-oci ──fork──> supervisor daemon |
| 25 | + │ │ |
| 26 | + │ ▼ |
| 27 | + │ sandlock-init (confined PID 1 in the sandbox) |
| 28 | + │ │ fork + execve |
| 29 | + │ ▼ |
| 30 | + │ workload (+ any exec'd siblings) |
| 31 | + ▼ |
| 32 | + sandlock-core |
| 33 | + Landlock · seccomp · seccomp-notify · COW |
| 34 | +``` |
| 35 | + |
| 36 | +Each call from the runtime maps to one `sandlock-oci` subcommand. A long-lived |
| 37 | +supervisor daemon owns the sandbox between calls, and an in-sandbox |
| 38 | +`sandlock-init` acts as PID 1 so the workload and every `exec`'d process share |
| 39 | +one sandbox and one seccomp supervisor. |
| 40 | + |
| 41 | +## Lifecycle |
| 42 | + |
| 43 | +``` |
| 44 | +create <id> -b <bundle> spawn the supervisor, build the sandbox, park the |
| 45 | + child before execve, and save state |
| 46 | +start <id> release the parked child so it execve's the workload |
| 47 | +state <id> print state.json, reconciled against liveness |
| 48 | +kill <id> <signal> forward a signal to the workload (or its group with -a) |
| 49 | +delete <id> shut the supervisor down and remove the state dir |
| 50 | +exec <id> <cmd> run a sibling process in the same sandbox |
| 51 | +list list all sandboxes managed by sandlock-oci |
| 52 | +checkpoint <id> snapshot a running sandbox to an image directory |
| 53 | +restore <id> recreate and resume a sandbox from a checkpoint image |
| 54 | +check report kernel Landlock support |
| 55 | +``` |
| 56 | + |
| 57 | +The `create` then `start` split matches the OCI two-phase model: `create` |
| 58 | +forks the child and installs the full policy (Landlock, seccomp-notify, |
| 59 | +resource limits, and the network ACL) with the child parked just before |
| 60 | +execve, and `start` releases it. The supervisor reports the child PID back to |
| 61 | +the caller over a pipe, so there is no sleep or race in the handshake. |
| 62 | + |
| 63 | +## OCI spec translation |
| 64 | + |
| 65 | +`config.json` is mapped to a sandlock policy by intent rather than by |
| 66 | +replaying Linux container primitives: |
| 67 | + |
| 68 | +| OCI field | sandlock mapping | |
| 69 | +|---|---| |
| 70 | +| `root.path` | chroot target for the sandbox | |
| 71 | +| `root.readonly` | grants the rootfs read-only instead of read-write | |
| 72 | +| `mounts` (bind) | `fs_mount` with read-only or read-write from options | |
| 73 | +| `mounts` (tmpfs) | host-backed scratch dir, isolated and cleaned on delete | |
| 74 | +| `mounts` (proc) | host `/proc` mounted read-only, virtualized by seccomp | |
| 75 | +| `mounts` (sysfs) | host `/sys` mounted read-only | |
| 76 | +| `process.cwd` | working directory | |
| 77 | +| `process.env` | environment (started clean, then populated from the spec) | |
| 78 | +| `process.user` | run-as uid/gid, with a user namespace only if needed | |
| 79 | +| `linux.resources.memory.limit` | `max_memory` | |
| 80 | +| `linux.resources.pids.limit` | `max_processes` | |
| 81 | +| `linux.resources.cpu.quota/period` | sub-core `max_cpu` throttle | |
| 82 | +| `linux.resources.cpu.cpus` | CPU affinity via `sched_setaffinity` | |
| 83 | +| `linux.namespaces` | ignored by design | |
| 84 | + |
| 85 | +Supported OCI spec versions are 1.0.x, 1.1.x, and 1.2.x. Bundles declaring any |
| 86 | +other version are rejected at `create` rather than silently mis-mapped. |
| 87 | + |
| 88 | +Mount types with no safe namespace-less equivalent (`devpts`, `mqueue`, |
| 89 | +`cgroup`, `cgroup2`) are skipped. Port binding is remapped so in-container |
| 90 | +servers can `bind()` without colliding on host ports. |
| 91 | + |
| 92 | +## runc compatibility |
| 93 | + |
| 94 | +The CLI accepts the global and per-command flags that containerd and CRI-O |
| 95 | +pass to `runc`, so it can be wired in as a drop-in replacement. Flags that do |
| 96 | +not apply to a namespace-less, cgroup-less runtime are accepted and ignored: |
| 97 | +`--systemd-cgroup`, `--rootless`, `--no-pivot`, `--no-new-keyring`, and the |
| 98 | +`--debug` flag. Fatal errors are appended to the `--log` file in text or JSON |
| 99 | +form so the containerd shim can surface the real failure reason. |
| 100 | + |
| 101 | +The supervisor daemon exits with the workload's status (re-raising the killing |
| 102 | +signal where applicable), so a reaping shim sees a wait-status that mirrors the |
| 103 | +workload, the same as it would from `runc`. |
| 104 | + |
| 105 | +## Usage |
| 106 | + |
| 107 | +Point a runtime at the `sandlock-oci` binary as its OCI runtime, or drive it |
| 108 | +directly against a bundle: |
| 109 | + |
| 110 | +```sh |
| 111 | +sandlock-oci create mycontainer -b /path/to/bundle |
| 112 | +sandlock-oci start mycontainer |
| 113 | +sandlock-oci state mycontainer |
| 114 | +sandlock-oci exec mycontainer sh -c 'echo hello' |
| 115 | +sandlock-oci kill mycontainer SIGTERM |
| 116 | +sandlock-oci delete mycontainer |
| 117 | +``` |
| 118 | + |
| 119 | +State lives under `$XDG_RUNTIME_DIR/sandlock-oci` for unprivileged users and |
| 120 | +`/run/sandlock-oci` for root, overridable with the global `--root` flag. |
| 121 | + |
| 122 | +## Requirements |
| 123 | + |
| 124 | +- Linux 6.12 or newer (Landlock ABI v6) |
| 125 | +- A built `sandlock-core` (this crate depends on it directly) |
| 126 | + |
| 127 | +Run `sandlock-oci check` to confirm the running kernel supports Landlock. |
| 128 | + |
| 129 | +## Limitations |
| 130 | + |
| 131 | +- The workload and all `exec`'d processes share one sandbox and one seccomp |
| 132 | + supervisor, via the in-sandbox `sandlock-init` PID 1. |
| 133 | +- `exec` is non-TTY only. `-t` and `--console-socket` are accepted for runc |
| 134 | + compatibility but ignored, as there is no PTY support yet. |
| 135 | + |
| 136 | +## License |
| 137 | + |
| 138 | +Apache-2.0. See the repository root for details. |
0 commit comments