Skip to content

Match OCI runtimes by runtime, not by spelling - #98

Merged
Amitgb14 merged 2 commits into
mainfrom
fix/containerd-shim-runtime-names
Aug 9, 2026
Merged

Match OCI runtimes by runtime, not by spelling#98
Amitgb14 merged 2 commits into
mainfrom
fix/containerd-shim-runtime-names

Conversation

@Amitgb14

@Amitgb14 Amitgb14 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Found by running #89's phases 1–2 on a real Rocky Linux 10.2 host.

sandbox-cli run --runtime runc is refused there — on a machine whose default
runtime is runc:

runtime "runc" is not registered with the Docker daemon
  available runtimes: io.containerd.runc.v2

One daemon, two vocabularies for the same runtime, in one command:

$ docker info --format '{{.DefaultRuntime}}'
runc
$ docker info --format '{{json .Runtimes}}'
{"io.containerd.runc.v2":{"path":"runc", …

.Runtimes is keyed by the containerd shim name. Every list in
isolation.go is written in runtime names and parseRuntimeNames takes the
JSON keys verbatim, so the two were compared as plain strings and never matched.

The fix

runtimeName reduces io.containerd.<runtime>.v<major> to <runtime> and
leaves everything else alone. StrongerRuntime, notHostDefault and
runtimeHint ask through it.

Reducing rather than expanding, because the mapping only goes one way: a shim
name yields exactly one runtime name, while a runtime name does not say which
shim major a host registered. So the raw name stays what gets displayed and
handed back to the engine — the refusal above still lists
io.containerd.runc.v2, since that is what the user will see in docker info
and must write in daemon.json.

The pattern is matched exactly rather than by splitting on dots: the prefix
alone is not enough, the .v<digits> suffix has to be there, and the separator
is taken from the right. A runtime genuinely named with dots keeps its name.

Why it is worth more than the error message

The two failure directions were not equal.

Unable to see io.containerd.runc.v2, the tool called runc unavailable and
flagged an ordinary run as being on an unusual runtime — wrong labels.

Unable to see io.containerd.kata.v2, it would not have counted a registered
Kata as a kernel of its own at all — a boundary reported absent while
installed
. isolation.go documents these lists as failing only in the safe
direction ("nothing here can claim a boundary a run did not get"); a shim name
broke that, in the direction the comment rules out.

That second half is inference, not observation: this host has no Kata, and how a
given installer keys its daemon.json entry is unverified. #89's phase 3 will
answer it for gVisor, since runsc install writes the entry itself. The fix
costs nothing either way and makes both lists mean what they say on a
containerd-backed daemon, which is now the ordinary case on RHEL-family hosts.

Testing

Every added function is pure, so the whole matrix runs on a machine with neither
Kata nor gVisor installed — which is the point, since the hosts that have them
are the hosts nobody has.

Note for #88

ClassifyRuntimeGap on feat/prod-demands-a-kernel compares
containsRuntime(s.All, effective) by string too. It inherits StrongerRuntime
and notHostDefault from this change when the branch next merges main, but
that one call wants SameRuntime as well — otherwise --runtime kata against a
daemon listing io.containerd.kata.v2 reads as GapMissing.

`sandbox-cli run --runtime runc` is refused on Rocky Linux 10.2 — a host whose
default runtime is runc:

    runtime "runc" is not registered with the Docker daemon
      available runtimes: io.containerd.runc.v2

One daemon, two vocabularies for the same runtime, in one command:

    $ docker info --format '{{.DefaultRuntime}}'
    runc
    $ docker info --format '{{json .Runtimes}}'
    {"io.containerd.runc.v2":{"path":"runc", …

`.Runtimes` is keyed by the containerd **shim** name. Every list here is written
in runtime names, and parseRuntimeNames takes the JSON keys verbatim, so the two
were compared as plain strings and never matched.

runtimeName reduces `io.containerd.<runtime>.v<major>` to `<runtime>` and leaves
everything else alone; StrongerRuntime, notHostDefault and runtimeHint ask
through it. The raw name is still what gets displayed and handed back to the
engine, because the mapping only goes one way — a shim name yields one runtime
name, a runtime name does not say which shim major a host registered.

The failure directions were not equal, which is why this is worth more than the
error message it fixes. Unable to see `io.containerd.runc.v2`, the tool called
runc unavailable and flagged an ordinary run as being on an unusual runtime:
wrong labels. Unable to see `io.containerd.kata.v2`, it would not have counted a
registered Kata as a kernel of its own at all — a boundary reported absent while
installed, which is the opposite of the direction these lists are documented to
fail in.

Found by running phase 1/2 of #89 on a real Rocky host. Everything added is a
pure function of a name, so the matrix is tested without Kata or gVisor
installed — which matters, because the hosts that have them are the hosts nobody
has.
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sandbox-cli Ready Ready Preview Aug 9, 2026 10:09am

…metric

Three review findings on this PR, and the first is the same class of mistake it
was written to fix.

**Accepting a spelling is only safe if the spelling sent is one the engine
knows.** runtimeHint was loosened to match by runtime, but nothing wrote the
matched key back, so BuildArgs still handed docker the user's original string.
A request for io.containerd.runsc.v1 against a host registering v2 passed the
preflight and then died at launch with "unknown or invalid runtime name" — the
terse message this check exists to replace, now with the helpful one suppressed.
Under fleet or --detach that is `exit status 125` and nothing else.

resolveRuntime returns the engine's own key for the requested runtime and the
caller assigns it to spec.Runtime. Exact match first, normalised second: a
spelling the engine itself uses is the one to send.

**The preflight was inert under podman.** It asked `{{json .Runtimes}}`
directly, a field podman does not have, so .Output() errored and the check
returned nil on every podman run. It now goes through runtimeNames, which has
the podman branch doctor already uses — and does not *refuse* there, because
podman reports only the runtime it is using and absence from that list is not
evidence of absence from the host.

**Normalisation is right for one list and wrong for the other.** The two fail in
opposite directions by design, so normalising both was not symmetry, it was a
regression in one of them: an admin can point io.containerd.runc.v2 at
sysbox-runc, and folding every shim-shaped runc name into the shared-kernel set
suppressed the RUNTIME column for exactly the sessions it exists to reveal.
strongerRuntimes still normalises, because a shim name is still that runtime and
missing a real boundary is the failure that matters there. sharedKernelRuntimes
looks up raw, so a name this file has not seen literally is shown and not
characterised.
@Amitgb14
Amitgb14 merged commit caf698e into main Aug 9, 2026
10 checks passed
Amitgb14 added a commit that referenced this pull request Aug 9, 2026
Merges main and fixes the one comparison in ClassifyRuntimeGap that still read
names as strings.

`containsRuntime` decides GapMissing, and GapMissing is a prod refusal. A
containerd-backed daemon keys .Runtimes by shim name — `io.containerd.runc.v2`
on Rocky Linux 10.2, in the same `docker info` that calls its default `runc` —
so `--runtime kata` against a host listing `io.containerd.kata.v2` read as
missing and prod refused a machine that had the kernel it was demanding.

It now asks SameRuntime, which #98 added for exactly this and which the rest of
this file already uses. The permissive direction is unchanged: it can only find
a runtime the engine really listed, and a name no entry means is still
GapMissing, so prod still refuses before the launch fails.

This matters before #89 phase 3 rather than after it. That checklist walks the
gate with `--runtime` values against a real daemon, and on a containerd host it
would have been measuring a comparison already known to be wrong here — the same
trap that would have had its `--runtime runc` case pass for the wrong reason.

The merge conflict in isolation.go was two rewrites of notHostDefault's doc
comment over an identical body: main's account of why normalisation is
deliberately asymmetric between the two lists, and this branch's account of why
both callers need the unrecognised-name direction. Both are kept.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant