Skip to content

Commit 7a55505

Browse files
committed
fix(driver-native): spawn the low-level runtime directly instead of via containerd
Corrects an architectural mistake from the previous commit: containerd's Containers/Tasks services (and therefore containerd's own io.containerd.runc.v2 shim) were spawning runc/crun, not OpenShell. This put containerd in the process-supervision path for the sandbox's root process, which is not what "OpenShell drives the runtime, containerd is just used elsewhere" was supposed to mean. This driver -- not containerd -- now spawns the configured low-level OCI runtime (runc by default, crun, or any other OCI-runtime-spec-compatible binary) directly: - New runtime.rs builds a standard OCI bundle (config.json + a mounted rootfs/) from the snapshot containerd prepared, and drives it through the runtime's own create/start/state/kill/delete CLI contract via std::process::Command -- the same integration pattern containerd's shim, CRI-O, and Podman use, just invoked by this driver's process instead of containerd's. - Removed runc_options.rs entirely: it existed only to wrap runtime_binary as a containerd.runc.v1.Options message for containerd's shim to read. With the shim no longer in the picture, selecting runc vs. crun is now simply which binary this driver execs. - containerd is now used only for image pull/unpack (Transfer/Images/ Content) and snapshot management (Snapshots.Prepare) -- image.rs no longer creates a containerd Container or Task at all. - Because no containerd Container/Task references the prepared snapshot anymore, it has nothing protecting it from containerd's background GC. Verified this is a real problem (an otherwise-identical run without protection failed at teardown with "snapshot ... does not exist" because GC had already reaped it) and fixed it with containerd's Leases service: image.rs now creates a lease per sandbox and attaches the snapshot to it, deleting the lease at teardown. - Sandbox tracking (get/list) no longer queries containerd (there is nothing left to query); it scans this driver's own state directory for bundle directories, the same pattern the VM driver uses to rediscover sandboxes on restart, and calls `runtime state` directly for status. Re-verified end to end against a real containerd 2.x + runc/crun install: pull -> chain-ID resolve -> lease-protected snapshot prepare -> bundle mount -> create/start/state/delete directly against both runc and crun (new second integration test) -> stop -> delete, plus confirming a snapshot without a lease is reaped by GC while one with a lease survives. The full openshell-gateway binary was also re-run with --drivers native against live containerd to confirm the wiring still works. Docs, the driver README, and inline comments are updated to describe the corrected division of labor between this driver and containerd. Signed-off-by: Eric Curtin <eric.curtin@docker.com>
1 parent 653842b commit 7a55505

15 files changed

Lines changed: 675 additions & 425 deletions

File tree

architecture/compute-runtimes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ when a sandbox create request asks for GPU resources.
3434
| Podman | Rootless or single-machine deployments. | Container plus nested sandbox namespace. | Uses the Podman REST API, OCI image volumes, and CDI GPU devices when available. |
3535
| Kubernetes | Cluster deployment through Helm. | Pod plus nested sandbox namespace. | Uses Kubernetes API objects, service accounts, secrets, PVC-backed workspace storage, and GPU resources. |
3636
| VM | Experimental microVM isolation. | Per-sandbox libkrun VM. | Managed endpoint-backed driver. The gateway spawns `openshell-driver-vm`, waits for its Unix socket, and then consumes it through the same remote `compute_driver.proto` path used by unmanaged endpoint drivers. The VM driver boots a cached bootstrap `rootfs.ext4`, prepares requested OCI images inside a bootstrap VM with `umoci`, attaches the prepared image disk read-only, and gives each sandbox a writable `overlay.ext4` for merged-root changes and runtime material. The driver persists each accepted launch request beside the overlay and restarts those VMs on driver startup without recreating the overlay. |
37-
| Native | Early/opt-in kernel-primitive isolation against an existing system `containerd`. | Container built from Linux namespaces + cgroups v2. | In-process driver, like Docker/Podman/Kubernetes. Drives a system-provided `containerd` over its own gRPC API — never bundles containerd or a low-level OCI runtime; containerd invokes the configured `runtime_binary` (`runc` by default, `crun`, or another OCI-runtime-spec-compatible binary) through its own shim. Per-sandbox network namespace + veth pair + nftables ruleset (shared `openshell-nft-ruleset` crate, also used by the VM driver). GPU support is kernel-device-node passthrough only. Rootless mode is implemented but not yet functional (defaults off) — see `crates/openshell-driver-native/README.md`. |
37+
| Native | Early/opt-in kernel-primitive isolation against an existing system `containerd`. | Container built from Linux namespaces + cgroups v2. | In-process driver, like Docker/Podman/Kubernetes. The driver itself — not containerd — spawns the configured `runtime_binary` (`runc` by default, `crun`, or another OCI-runtime-spec-compatible binary) directly through its `create`/`start`/`state`/`delete` CLI contract; containerd is used only for image pull/unpack and snapshot management (protected from containerd's GC via a lease, since no containerd `Container`/`Task` is ever created) and never bundled. Sandboxes are tracked by scanning the driver's own state directory rather than querying containerd or the runtime for a global list. Per-sandbox network namespace + veth pair + nftables ruleset (shared `openshell-nft-ruleset` crate, also used by the VM driver). GPU support is kernel-device-node passthrough only. Rootless mode is implemented but not yet functional (defaults off) — see `crates/openshell-driver-native/README.md`. |
3838
| Extension | Out-of-tree drivers operated alongside the gateway. | Whatever boundary the driver implements. | Selected by a non-reserved custom `compute_drivers = ["<name>"]` entry with `[openshell.drivers.<name>].socket_path`, or at launch time by pairing `--drivers <name>` with `--compute-driver-socket=<path>`. Reserved built-in names such as `vm`, `docker`, `podman`, `kubernetes`, and `native` cannot be used as unmanaged socket endpoints. The gateway connects to a UDS the operator already provisioned, runs `GetCapabilities`, logs the advertised `driver_name`, and dispatches all sandbox lifecycle calls through `compute_driver.proto`. The driver process and socket lifecycle are operator-owned; the gateway does not spawn, supervise, or remove unmanaged extension drivers. The trust boundary is the socket's filesystem permissions: the operator must ensure only the gateway uid can read/write it. |
3939

4040
Per-sandbox CPU and memory values currently enter the driver layer through

crates/openshell-driver-native/Cargo.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ openshell-nft-ruleset = { path = "../openshell-nft-ruleset" }
2424
openshell-policy = { path = "../openshell-policy" }
2525

2626
# containerd's own generated gRPC client. This is the "system-provided
27-
# containerd" integration point: the driver never bundles or drives a
28-
# low-level OCI runtime (runc/crun/etc.) itself. It talks to a
29-
# system-installed containerd over its UDS gRPC API, and containerd invokes
30-
# the configured low-level runtime via its own shim.
27+
# containerd" integration point, used only for image pull/unpack and
28+
# snapshot management (see src/image.rs) -- never bundled, installed, or
29+
# managed by this driver. The driver itself, not containerd, spawns the
30+
# configured low-level OCI runtime (runc/crun/etc.) directly (see
31+
# src/runtime.rs); containerd's container/task services and shim are
32+
# never used.
3133
containerd-client = "0.9"
3234
# OCI runtime-spec and image-spec types (namespaces, cgroups v2 resources,
3335
# seccomp, image manifest/config parsing for chain-ID computation).

crates/openshell-driver-native/README.md

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22

33
The native compute driver builds sandbox containers directly from Linux kernel
44
primitives (namespaces, cgroups v2) by driving a **system-provided
5-
`containerd`** rather than a container engine daemon (Docker/Podman) or a
6-
hypervisor. It runs in-process within the gateway server, the same way the
7-
Docker, Podman, and Kubernetes drivers do.
8-
9-
This driver deliberately does not bundle, install, or manage `containerd`
10-
itself, and it never shells out to a low-level OCI runtime (`runc`, `crun`,
11-
...) directly — containerd does that, through its own shim, using whichever
12-
`runtime_binary` is configured. See
5+
`containerd`** for image management and directly invoking a **configurable
6+
low-level OCI runtime** (`runc` by default, `crun`, or any other
7+
OCI-runtime-spec-compatible binary already installed on the host) itself. It
8+
runs in-process within the gateway server, the same way the Docker, Podman,
9+
and Kubernetes drivers do.
10+
11+
**This driver — not containerd — spawns the sandbox's process.** containerd
12+
is used only for image pull/unpack and snapshot management; it never creates
13+
a containerd `Container` or `Task` object, and its shim
14+
(`io.containerd.runc.v2`) is never involved. This driver builds a standard
15+
OCI bundle (`config.json` + a mounted `rootfs/`) from the snapshot containerd
16+
prepared and drives the configured runtime directly through its own
17+
`create`/`start`/`state`/`kill`/`delete` CLI contract. It never bundles,
18+
installs, or manages `containerd` itself, and it never bundles the low-level
19+
runtime either — both must already be present on the host. See
1320
[`docs/reference/sandbox-compute-drivers.mdx`](../../docs/reference/sandbox-compute-drivers.mdx#native-driver)
1421
for the user-facing reference and known gaps.
1522

@@ -19,8 +26,8 @@ for the user-facing reference and known gaps.
1926
graph TB
2027
CLI["openshell CLI"] -->|gRPC| GW["Gateway Server<br/>(openshell-server)"]
2128
GW -->|in-process| ND["NativeComputeDriver"]
22-
ND -->|gRPC<br/>Unix socket| CD["containerd<br/>(system-provided)"]
23-
CD -->|shim: io.containerd.runc.v2<br/>binary_name = runtime_binary| RT["Low-level OCI runtime<br/>runc / crun / ..."]
29+
ND -->|gRPC<br/>Unix socket, image/snapshot/lease only| CD["containerd<br/>(system-provided)"]
30+
ND -->|exec: create/start/state/delete| RT["Low-level OCI runtime<br/>runc / crun / ..."]
2431
RT -->|creates| C["Sandbox Container"]
2532
C -->|bind mount<br/>read-only| SV["Supervisor Binary<br/>/opt/openshell/bin/openshell-sandbox"]
2633
ND -->|ip netns / veth / nft| NET["Per-sandbox network namespace"]
@@ -34,10 +41,13 @@ graph TB
3441
|---|---|
3542
| Registry pull, layer download, unpack | containerd's `Transfer` + `Images` + `Content` services (`image.rs`) |
3643
| Writable per-sandbox rootfs layer | containerd's `Snapshots` service (`Prepare`) |
44+
| Protecting that snapshot from containerd's background GC | containerd's `Leases` service (`image.rs`) — necessary precisely because this driver never registers a `Container`/`Task` that would otherwise reference it |
3745
| OCI runtime spec (namespaces, cgroups v2, capabilities, seccomp defaults) | This crate (`spec.rs`), generated from OpenShell policy |
38-
| Namespace/cgroup/seccomp *enforcement* | The configured low-level runtime (`runc`/`crun`), invoked by containerd's shim — never this crate directly |
46+
| Bundle assembly (mounting the snapshot, writing `config.json`) | This crate (`runtime.rs`) |
47+
| Namespace/cgroup/seccomp *enforcement*, and the actual `create`/`start`/`state`/`kill`/`delete` process invocations | This crate (`runtime.rs`), execing the configured low-level runtime directly — containerd is never involved |
3948
| Network namespace, veth pair, nftables ruleset | This crate (`network.rs`), reusing `openshell-nft-ruleset` |
4049
| GPU device nodes | This crate (`gpu.rs`), kernel device-node passthrough only (see below) |
50+
| Tracking which sandboxes exist | This crate's own state directory (`state_dir/bundles/<sandbox>/`), scanned directly — containerd has no record of these containers at all |
4151

4252
## Isolation Model
4353

@@ -94,17 +104,40 @@ resolved the same way as the Docker/Podman drivers.
94104
subscribing to containerd's own event stream. Functionally correct,
95105
higher-latency than a push subscription would be.
96106
- **CDI GPU injection is device-node-only** (see above).
107+
- **Sandbox discovery is a state-directory scan**, not a containerd query
108+
(there is nothing for containerd to query — see the table above). A
109+
`state_dir` shared or manually edited by something other than this
110+
driver process could produce inconsistent `list_sandboxes` results;
111+
this matches the trust model the VM driver already uses for its own
112+
state directory.
113+
114+
## containerd Leases (why they exist here)
115+
116+
Every other in-tree driver that talks to a container engine registers a
117+
persistent object (a Docker/Podman container, a containerd `Container`) that
118+
the engine itself uses to know a resource is still wanted. This driver
119+
deliberately never creates that containerd-side object — see "What this
120+
crate owns vs. what containerd owns" above — which means the writable
121+
snapshot it prepares has **nothing** protecting it from containerd's
122+
background garbage collector by default. This was not a theoretical
123+
concern: during development, an otherwise-identical flow without a lease
124+
failed at teardown with `snapshot ... does not exist` because GC had
125+
already reaped it. `image.rs`'s `create_lease`/`protect_snapshot_with_lease`/
126+
`delete_lease` functions close that gap by attaching the snapshot to a
127+
lease for the sandbox's lifetime.
97128

98129
## Testing
99130

100131
Unit tests (`cargo test -p openshell-driver-native --lib`) cover OCI spec
101132
generation, chain-ID computation, nftables ruleset generation, GPU device
102-
resolution, and config validation — all pure/deterministic, no containerd
103-
required.
133+
resolution, bundle/state-directory bookkeeping, and config validation — all
134+
pure/deterministic, no containerd required.
104135

105136
`tests/containerd_integration.rs` is a real end-to-end test against a live
106-
containerd (pull → chain-ID resolve → snapshot prepare → container/task
107-
create → start → stop → delete, plus network namespace isolation). It is
108-
`#[ignore]`d by default (requires a reachable containerd socket, `runc`,
109-
`CAP_NET_ADMIN`, and network access) — see the module doc comment in that
110-
file for how to run it.
137+
containerd (pull → chain-ID resolve → lease-protected snapshot prepare →
138+
bundle mount → `create`/`start`/`state`/`delete` directly against the
139+
low-level runtime → stop → delete, plus network namespace isolation). It
140+
runs the full round trip against both `runc` and `crun` to prove the
141+
configurable-runtime design point end to end. It is `#[ignore]`d by default
142+
(requires a reachable containerd socket, the configured runtime, `CAP_NET_ADMIN`,
143+
and network access) — see the module doc comment in that file for how to run it.

crates/openshell-driver-native/src/config.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ pub const DEFAULT_CONTAINERD_SOCKET_PATH: &str = "/run/containerd/containerd.soc
1414
pub const DEFAULT_CONTAINERD_NAMESPACE: &str = "openshell";
1515
/// Default low-level OCI runtime.
1616
///
17-
/// Configurable (e.g. `crun`) — see [`crate::runc_options::RuncOptions`].
18-
/// Never bundled: this must already be installed and resolvable through
19-
/// containerd's shim on the host.
17+
/// Configurable (e.g. `crun`, or any other OCI-runtime-spec-compatible
18+
/// binary). Never bundled: this driver execs it directly (see
19+
/// [`crate::runtime`]), so it must already be installed and resolvable on
20+
/// the gateway host. containerd is never involved in invoking it.
2021
pub const DEFAULT_RUNTIME_BINARY: &str = "runc";
2122
/// Default containerd snapshotter.
2223
pub const DEFAULT_SNAPSHOTTER: &str = "overlayfs";
@@ -43,10 +44,11 @@ pub struct NativeComputeConfig {
4344
pub containerd_socket_path: PathBuf,
4445
/// containerd namespace this driver operates in.
4546
pub containerd_namespace: String,
46-
/// Low-level OCI runtime binary name (or absolute path) containerd's
47-
/// shim should exec. Default `runc`; set to `crun` or another
48-
/// OCI-runtime-spec-compatible binary already installed on the host.
49-
/// This driver never bundles or invokes this binary itself.
47+
/// Low-level OCI runtime binary name (or absolute path) this driver
48+
/// execs directly (see [`crate::runtime`]). Default `runc`; set to
49+
/// `crun` or another OCI-runtime-spec-compatible binary already
50+
/// installed on the host. Never bundled: containerd is never involved
51+
/// in invoking it.
5052
pub runtime_binary: String,
5153
/// containerd snapshotter used for image unpack and per-sandbox
5254
/// writable layers.
@@ -85,8 +87,8 @@ pub struct NativeComputeConfig {
8587
/// (UID/GID 0) to an unprivileged host UID/GID range. This is the
8688
/// mechanism behind the issue's "rootless by default" goal: the
8789
/// sandboxed process would run as root only from its own point of
88-
/// view, never as host root, even though the host containerd/runc
89-
/// invocation itself is not rootless.
90+
/// view, never as host root, even though the runtime invocation
91+
/// this driver performs itself is not rootless.
9092
///
9193
/// **Known gap, defaults to `false` until fixed:** verified against a
9294
/// real containerd + runc during development, enabling this currently

0 commit comments

Comments
 (0)