Back to blog

kern v0.9.0-rc3, the engine moves inside the language

Two stability candidates landed after rc1, and the rc1 post covered neither. rc2 turns the whole OWASP injection family into a syntactic category: a query or command built from user input no longer type-checks. rc3 takes the container engine off the host: it pulls and runs real OCI images with no docker, podman or runc anywhere on the machine. Both are additive. No existing Kern program breaks.

944
Tests, 100% pass
17/21
v1.0 gates closed
5
Injection classes, now compile errors
0
Container daemons on the host

The story rc1 did not tell

The previous post stopped at v0.9.0-rc1. Two more candidates have landed since, and each one is the kind of change that resets the six-month soak clock: not because anything broke, but because the compile-time refusal surface grew (rc2) and a host dependency was removed wholesale (rc3). The public Kern API is back-compatible in both. No existing program needs a line changed.

Why the soak clock restarts, and why that is the honest call. v0.9.x exists to prove six months of zero breaking changes before v1.0. rc2 and rc3 add no breaking changes, but they each change the set of programs the compiler accepts or the dependencies a deploy requires. Counting that as a fresh soak window is the conservative reading of the promise. 17 of 21 v1.0 gates are now closed or have their harness shipped; the four that remain are the foundation incorporation, the real-account KMS exercise, the parser-fuzzer 24 h run, and the soak itself. Three of those four are not code.

Injection prevention as a syntactic category (rc2)

Most languages treat injection as a runtime discipline: parameterize your queries, escape your shell arguments, validate your URLs, and hope every call site remembered. rc2 takes a different position. The promise is that an AST shape an attacker would need to land the bug does not exist in valid Kern source. Five orthogonal compile-time rules carry it.

The foundation is a family of @<kind>_literal annotations. A parameter decorated with one of them must receive a string literal AST node at every call site. A value assembled from user input is a different AST node, so it does not type-check.

injection.kern
import db.postgres as pg

async fn find_user(conn: pg.Conn, id: str) -> Result<Row>:
    # @sql_literal: the query text must be a literal.
    # User input only ever arrives as a bound parameter.
    return await pg.query(conn, "SELECT * FROM users WHERE id = $1", [id])?

async fn broken(conn: pg.Conn, id: str):
    # pg.query(conn, "SELECT * FROM users WHERE id = " + id)
    #   COMPILE ERROR: @sql_literal expects a string literal,
    #   got a concatenation. The classic SQLi shape will not parse
    #   into a valid program.
    pass

A self-contained container engine, no docker, no podman (rc3)

In rc1 the native container runtime sandboxed a process with namespaces, cgroups and overlayfs, but the broader subsystem still shelled out to a host-installed Docker or Podman to pull and run images. rc3 closes that gap. The engine is now compiled into the language. It has been verified end to end against debian:bookworm-slim and alpine:3.19 from the Docker registry, on hosts that have had docker, podman, skopeo, runc, iproute2 and iptables explicitly purged.

infra.kern
import sys.container as ctr

async fn main() -> Result<str>:
    # Pull + run a real image. No docker, no podman on the host.
    # Signature checked against a cosign-compatible Ed25519 key,
    # manifest digest pinned to the blob we just fetched.
    out = await ctr.oci_run_image_verified(
        image:  "alpine:3.19",
        pubkey: env("KERN_OCI_PUBKEY"),
        cmd:    ["echo", "hello from a kern-pulled container"]
    )?
    print(out)
    return Ok(out)

What that one call does without touching the host toolchain:

The dependency list that a production Kern deploy no longer needs is concrete: docker, containerd, runc, crun, podman, buildah, skopeo, iptables, nftables, iproute2 and libarchive-dev. What remains is the Kern binary, a Linux kernel 5.15 or newer with cgroup v2 and user namespaces, and libc.

The macOS path is honest about the constraint. The native engine is Linux-only by kernel design. On macOS the runtime returns a clear "containers require Linux" error rather than silently falling back to Docker Desktop. Develop against a Linux VM locally, target a Linux host in production.

Supply-chain tripwires that keep the shellout gone (rc3)

Removing the shellout once is easy. Keeping it gone across future commits is the real work, so rc3 ships the gates that make a regression fail loudly.

The bootstrap stopped trying to allocate 145 GB

This one is on main, heading into rc4, and it is worth stating plainly because the old number was embarrassing. The self-bootstrap, the step where the Kern compiler compiles itself, used to climb toward 145 GB of memory and OOM on anything short of a very large machine. It now self-bootstraps in about a minute in a couple of gigabytes of RAM. That is the difference between "needs a dedicated build box" and "runs on a laptop or a normal CI container." A few changes did most of the work:

Also on main, heading into rc4

What v0.9.x is for

v0.9.0-rc3 is not v1.0. The point of the v0.9.x line is soak time:

Every item above is on the ROADMAP with a checkbox. None are aspirational; all are blockers.

By the numbers

Try v0.9.0-rc3

Install on Linux or macOS. Single binary, no runtime dependencies.

terminal
curl -fsSL https://kern-lang.eu/install.sh | bash
Get Started View on Codeberg