crun

crun is “a fast and low-memory footprint OCI Container Runtime fully written in C” (containers/crun README) — a low-level container runtime, like runc, that reads an OCI runtime-spec bundle (config.json + rootfs/) and turns it into a running Linux process by making the namespace, cgroup, capability, seccomp, and mount syscalls. It was created by Giuseppe Scrivano at Red Hat (written, by his account, during the company’s Christmas shutdown) and is part of the containers organisation alongside Podman, Buildah, conmon, and CRI-O (Red Hat — An introduction to crun). crun exists alongside runc, not as a fork of it: it is an independent reimplementation whose two genuine differentiators are a lower memory footprint and faster startup (no Go runtime, a small native binary) and a usable C library, libcrun, that lets a program embed an OCI runtime in-process instead of fork/execing an external one. It is the default low-level runtime in modern Podman and OpenShift (4.18+), and the default in Fedora since Fedora 31 and in RHEL 9 (RHEL 9 — Selecting a container runtime). Because crun implements the same OCI spec as runc, the shared low-level mechanics — the create→start→execve sequence, the nsexec namespace dance, the cgroup-driver choice — are identical to runc and are covered in runc; this note covers what is genuinely crun-specific, and is honest about what is merely marketing. Versions here are pinned to crun 1.28 (2026-05-27, the latest release as of writing).

Mental Model — Same Contract, Different Implementation Language

flowchart TB
  SPEC["OCI Runtime Spec<br/>(config.json + rootfs/)"]
  subgraph RUNC["runc (Go)"]
    GO["Go runtime + goroutines"]
    NSEXEC1["nsexec (small C bootstrap)<br/>re-exec before Go starts"]
    LC1["libcontainer (Go package)<br/>NOT embeddable in C/Python"]
  end
  subgraph CRUN["crun (C)"]
    LIBCRUN["libcrun (C library)<br/>embeddable in-process"]
    CLI["crun CLI<br/>thin wrapper over libcrun"]
    HANDLERS["run.oci.handler<br/>wasm / krun handlers"]
  end
  SPEC --> RUNC
  SPEC --> CRUN
  RUNC --> PROC["container process<br/>(identical syscalls)"]
  CRUN --> PROC

crun and runc both consume the same OCI config.json and produce the same kind of container — the difference is in the implementation, not the output. What it shows: runc is Go, so it must re-exec a tiny C nsexec bootstrap before the Go runtime spins up threads (because the namespace/setns dance is unsafe once Go is multi-threaded — see runc); its work lives in libcontainer, a Go package that a C or Python program cannot embed. crun is C end to end: its work lives in libcrun, a real C shared library, and the crun CLI is a thin wrapper over it. The insight to take: because the OCI spec is the contract, swapping runc for crun changes the how (language, memory, embeddability, extra handlers) but never the what — the same pod spec runs identically. The two boxes you should care about are libcrun (embeddable) and the run.oci.handler extension (wasm/krun) — those are crun’s real additions; everything else is a faster, smaller reimplementation of runc’s behaviour.

What Is Genuinely Different From runc — and What Is Not

The honest accounting, because crun is routinely oversold:

Genuinely different (1): no Go runtime → smaller binary, lower memory, faster startup. This is the headline and it is real. crun is C with few dependencies; runc carries the Go runtime. The numbers, from crun’s own README (crun README): running 100 containers sequentially with /bin/true, crun took 0:01.69 versus runc’s 0:3.34“−49.4%”, roughly twice as fast. crun also runs under far tighter memory limits: the README demonstrates podman --runtime crun run --memory 512k succeeding where --runtime runc --memory 4M fails outright. Red Hat’s introduction adds the binary-size and resident-memory figures: the crun binary is “~300k” against runc’s “~15M” (“about 50 times larger”), and in a 100-container run crun’s peak resident set was 3,752 KB versus runc’s 15,120 KB (An introduction to crun). The Red Hat figures date to 2020; treat the ratios (≈2× faster, ≈4× less RSS, ≈50× smaller binary) as durable and the absolute bytes as dated, since both binaries grow with each release — re-measure on your own versions before capacity planning.

Genuinely different (2): libcrun, a real embeddable C library. crun “aims to be also usable as a library that can be easily included in programs without requiring an external process for managing OCI containers” (crun README). This is architecturally significant and not something runc offers: runc’s logic lives in libcontainer, a Go package, which a C, C++, or Python program cannot link against. libcrun can be built as a shared object (./configure --enable-shared) and called in-process. Red Hat’s stated motivation is to integrate it with conmon, the container monitor used by Podman and CRI-O, “rather than executing an OCI runtime” as a separate process — eliminating a fork/exec per container (An introduction to crun). The repository’s own description now reads “a fast and lightweight fully featured OCI runtime and C library for running containers,” making the library a first-class purpose, not an afterthought.

Genuinely different (3): alternative run.oci.handler execution backends — wasm and krun. crun extends the OCI flow with handlers selected by an OCI annotation, letting the same runtime launch fundamentally different workloads (detailed below). runc has no equivalent built-in.

NOT a real differentiator: “cgroup v2 native.” crun was indeed designed cgroup-v2-first and its v2 support has long been mature, but framing this as a current advantage over runc is a dated selling point. Per runc’s own documentation, runc gained experimental v2 support in v1.0.0-rc91 and has fully supported cgroup v2 since v1.0.0-rc93 — before its 1.0.0 stable release (runc cgroup-v2 docs). So in the 2026 landscape both runtimes are fully cgroup-v2 capable; crun’s edge here is historical (it was v2-first and its v2 code path is arguably cleaner), not a capability gap. crun does retain a small convenience: per its man page, “if the cgroup configuration found is for cgroup v1, crun attempts a conversion when running on a cgroup v2 system” (crun.1.md) — a pragmatic auto-translation that smooths v1-spec bundles onto v2 hosts.

NOT a differentiator: OCI feature coverage. crun is fully OCI-runtime-spec compliant and supports the same surface as runc — namespaces, capabilities, seccomp, SELinux/AppArmor labels, OCI hooks, sd-notify. A bundle that runs under runc runs under crun. The remaining reasons to stay on runc are familiarity and the Go-ecosystem fit (non-Kubernetes tools that import libcontainer), not any capability crun lacks.

The run.oci.handler Extension — wasm and krun

crun’s most distinctive feature (as opposed to its smaller-faster qualities) is the handler mechanism: a run.oci.handler annotation in the OCI config.json redirects how crun execs the container, so one runtime binary can launch native processes, WebAssembly modules, or KVM-isolated microVMs.

WebAssembly / WASI handler

With the wasm handler, crun “run[s] the wasm handler for container [and] allows running wasm workload natively” (crun.1.md). The container image carries a .wasm (or .wat, auto-compiled) entrypoint, and crun hands it to an embedded WebAssembly engine instead of execveing an ELF binary. crun natively supports wasmedge, wasmer, wasmtime, and wamr as backends, but “crun can support only one of them at a time,” chosen at build time (crun wasm-wasi-on-kubernetes docs). The selection is by annotation: the OCI config must contain either run.oci.handler: wasm or the container-image convention module.wasm.image/variant=compat (with wasm-smart/compat-smart variants for service-mesh sidecar cases). This is how a Kubernetes cluster runs WASM pods on crun without a separate WASM runtime: the CRI implementation propagates the annotation, and crun routes the workload to its WASM engine (crun wasm docs).

krun handler — KVM-isolated microVMs

The krun handler turns crun into a launcher for hardware-virtualized containers. “When krun is specified, the libkrun.so shared object is loaded and it is used to launch the container using libkrun” (krun(1)). libkrun integrates a Virtual Machine Monitor (VMM, the userspace half of a hypervisor) with the minimum emulated devices needed, and runs the container’s processes inside an isolated KVM environment — a microVM — rather than as host-kernel namespaced processes. The microVM is configured through OCI annotations or a .krun_vm.json file at the root of the container image (annotations take precedence when both are present). This gives a VM-strength isolation boundary (a second kernel between the workload and the host, conceptually like Kata Containers — see runc’s alternatives section) while still consuming an ordinary OCI bundle. krun is shipped as a sub-package of crun, so it is the same runtime family with a different handler, not a separate runtime to install.

The two handlers share a theme: crun is positioned not just as a faster runc, but as a substrate that can dispatch a container spec to native execution, a WASM sandbox, or a microVM — selected per workload by annotation.

Configuration / Usage

Running a container directly with crun

crun’s CLI mirrors runc’s (deliberately — they implement the same spec), so the runc walk-through transfers verbatim. The minimal flow:

# 1. Build an OCI bundle
$ mkdir -p mycontainer/rootfs
$ docker export $(docker create alpine:latest) | tar -C mycontainer/rootfs -xf -
$ cd mycontainer
$ crun spec                 # write a template config.json (same format as runc)
 
# 2. Run it
$ sudo crun run mycontainer # create + start + execve, as one call

crun spec, crun create <id>, crun start <id>, crun run <id>, crun exec <id>, crun kill <id>, crun delete <id>, and crun state <id> are the same verbs as runc; the create/start split exists for the same orchestrator reason (a window to attach networking and stdio between bundle materialization and process launch). The detailed semantics of each verb live in runc — they are spec-defined, not crun-specific.

Cgroup driver

# cgroupfs backend (default): crun writes /sys/fs/cgroup directly
$ sudo crun run mycontainer
 
# systemd backend: crun asks systemd (via D-Bus) to create a transient scope/slice
$ sudo crun --systemd-cgroup run mycontainer

Per the man page, --systemd-cgroup means “use systemd for configuring cgroups; if not specified, the cgroup is created directly using the cgroupfs backend” (crun.1.md). The same driver-must-match-the-host rule as runc applies: on a systemd-managed host (RHEL/Fedora), the kubelet and crun must both use the systemd driver, or limits can silently fail to apply (the failure mode is identical to runc’s — see runc).

Building libcrun and a Podman invocation

# build crun WITH the shared library (libcrun.so) for embedding
$ ./autogen.sh && ./configure --enable-shared && make && sudo make install
 
# tell Podman to use crun explicitly (it is already the default on Fedora/RHEL 9)
$ podman --runtime /usr/bin/crun run --rm --memory 512k fedora echo it works
it works

The --enable-shared flag is what produces libcrun; the default build statically links it into the crun binary only (crun README). The --memory 512k invocation is the README’s own demonstration that crun’s tiny footprint lets a container run under a memory limit that defeats runc.

Dependencies (what “few dependencies” means concretely)

crun’s Fedora build needs essentially: a C toolchain, libcap, libseccomp, libmount (via python-libmount at build), systemd-devel (for sd-notify and the systemd cgroup driver), and json-c-devel (crun README). Notably, crun 1.28 replaced its JSON parser, switching from YAJL to json-c (visible in the dependency list and the 1.28 release notes), which is why current builds depend on json-c-devel.

Uncertain

Verify: that crun 1.28 (2026-05-27) is precisely the YAJL→json-c switchover release and that 1.28 includes the CVE-2026-47766 rootfs-device-symlink fix. Reason: the json-c dependency is confirmed in the 1.28 README, and a release summary attributes the YAJL→json-c migration and the CVE fix to 1.28, but the changelog/NEWS entry was not read line-by-line during this task. To resolve: read NEWS.md at the 1.28 tag and the CVE-2026-47766 advisory. uncertain

Failure Modes

  • default OCI runtime "crun" not found. Podman is configured to use crun but the binary is absent or not on PATH (common on minimal images or after a manual install to a non-standard prefix). The fix is to install the crun package or point --runtime at the binary. This is a packaging issue, not a crun defect.
  • WASM handler builds support exactly one engine. Because “crun can support only one of them at a time”, a cluster that needs both, say, wasmedge and wasmtime workloads cannot serve them from a single crun build — you would need separate builds/runtimes selected via RuntimeClass. A pod annotated run.oci.handler: wasm will fail if the installed crun was not built with any WASM engine.
  • krun requires libkrun.so and KVM. The krun handler dlopen’s libkrun.so at runtime; if it is missing, or /dev/kvm is unavailable (nested-virtualization-disabled hosts, some CI), the container fails to start. krun is only meaningful where KVM is present.
  • cgroup-driver mismatch. Identical to runc: crun on cgroupfs while systemd owns the hierarchy leads to limits that race or silently don’t apply. Verify with cat /sys/fs/cgroup/.../memory.max for the container’s cgroup. (See runc for the full diagnosis.)
  • v1-bundle on a v2 host. crun auto-converts a cgroup-v1 resource spec to v2 when running on a v2 system, but the translation is best-effort; a bundle relying on a v1-only knob with no v2 equivalent may not get the intended limit. Prefer authoring v2-native config.json resources.

Alternatives and When to Choose Them

The OCI Runtime Spec makes every low-level runtime a drop-in for every other, so this is a how, not a what, decision. The full alternatives catalogue — runc, youki (Rust), gVisor/runsc (userspace-kernel sandbox), Kata (VM-isolated) — lives in runc and is not duplicated here. The crun-relevant summary:

  • Choose crun over runc for high pod density (the per-container memory saving of the runtime helper adds up across hundreds of pods), on hosts where it is already the default (Fedora, RHEL 9, OpenShift 4.18+, recent Podman), when you want to embed a runtime via libcrun, or when you need its wasm/krun handlers. There is no significant reason not to choose crun — it is fully OCI compliant.
  • Choose runc over crun mainly for ecosystem familiarity and for the few Go tools that still import libcontainer directly. The capability surfaces are equivalent.
  • Choose youki only for a deliberate Rust-in-the-stack reason; for production, runc and crun are the proven picks.
  • For stronger isolation than namespaces give, crun’s krun handler (libkrun/KVM microVM) is one option; gVisor (runsc) and Kata are the others — compared honestly in runc.

See Low-Level vs High-Level Container Runtimes for where crun (low-level) sits relative to containerd/CRI-O (high-level), and Open Container Initiative Runtime Spec for the contract that makes the swap transparent.

Production Notes

crun’s path to ubiquity went through Red Hat’s container stack. It became the default in Fedora 31 and has driven hundreds of thousands of Fedora installations (An introduction to crun); it is the default runtime in RHEL 9, where runc is deprecated and slated for removal in a future major release (RHEL 9 runtime selection guide); and OpenShift 4.18 made crun the default low-level runtime (under CRI-O) for newly created containers, with runc still supported and selectable and in-place 4.17→4.18 upgrades keeping the prior runtime (OCP 4.18 release notes). Podman selects crun over runc by default when both are present. Each of these switches was operationally transparent precisely because the OCI contract held — existing pod specs ran identically — which is the whole argument for a standardized runtime layer: a distribution can swap the implementation for a smaller, faster one without touching a single workload. The same neutrality means a cloud kubelet could move from runc to crun in a future release with no user-visible effect. Beyond the size/speed win, the strategic value Red Hat cites for libcrun is the ability for conmon to drive containers in-process rather than fork/execing a runtime per container — a structural efficiency that only a C library, not a Go package, makes possible.

See Also