Documentation
¶
Overview ¶
Package isolationprobe verifies what a sandbox runtime is from what the kernel serving a workload says about itself (Task 20439).
executor.IsVirtualizedRuntime and IsUserspaceKernelRuntime read a runtime's *name*, and placement honoured a project's demand for a virtual machine or a userspace kernel on that name alone. A daemon.json alias, or a RuntimeClass whose handler is runc, ran such a project on the host kernel with nothing to say so. This package is the half that looks: the probe a sandbox runs under the runtime it claims, and the rules that turn what the probe saw into a verdict.
The probe ¶
`cloop isolation-probe` reads, from inside the sandbox, what only the kernel serving it can answer: uname(2), /proc/version, the boot ID that kernel drew when it booted, its uptime, the CPU's hypervisor flag, the machine's DMI vendor and product, and gVisor's markers — its fixed kernel build and the boot log only the Sentry writes. The binary is the hub's or the agent's own, bound into the sandbox read-only, so an image cannot substitute a probe that says what it likes: the image supplies a root filesystem, and nothing the probe reads comes from it.
The verdict ¶
Classify compares that evidence with the executing host's own kernel, which the hub reads directly for its own container executor and an agent reports for its device. The boot ID settles most cases outright: a runc container reads its host's, because there is only one kernel, while a VM and a gVisor sandbox each draw their own. What a different kernel is — a VM's or gVisor's — is then read from markers neither can hide, and anything else is "unknown", which is never mistaken for isolation.
A hypervisor flag or a cloud vendor's DMI string is not evidence on its own. Both hosts this was built against are virtual machines themselves, so a runc container on either shows both, and only the comparison with the host's own kernel tells that container from a Kata guest.
Index ¶
Constants ¶
const Arg = "isolation-probe"
Arg is the argument that puts the cloop binary into probe mode: `cloop isolation-probe`. main dispatches on it before anything else in the binary runs, so the probe needs no configuration, no home directory and no writable filesystem — a sandbox gives it none of those.
const GVisorBuildString = "#1 SMP Sun Jan 10 15:06:54 PST 2016"
GVisorBuildString is the kernel build string gVisor's Sentry reports as uname -v, in every release: the Sentry is not a Linux build and stamps the same one regardless of the release it claims to be.
const MaxOutput = 64 << 10
MaxOutput bounds how much of a probe's output a reader takes. Evidence is a few hundred bytes; the bound is for a sandbox that prints something else.
const Schema = 1
Schema is the version of Evidence this build writes. A reader accepts any version it can decode; fields are only ever added.
Variables ¶
var ErrNoEvidence = errors.New("isolationprobe: the probe printed no evidence")
ErrNoEvidence is returned by Decode when the output holds no probe line.
Functions ¶
func IsInvocation ¶
IsInvocation reports whether args — os.Args — ask for probe mode: exactly `cloop isolation-probe`, nothing before or after it.
Exact on purpose. The mode is entered before the CLI parses anything, so it must not swallow an invocation meant for a command: anything more than the bare argument goes to the CLI, which knows no such command and says so.
func Main ¶
Main is probe mode: collect the evidence the kernel gives this process and write it to stdout as one line. It returns the process's exit code.
It touches nothing but the kernel interfaces Collect reads. A sandbox runs it with a read-only root filesystem, no network, no capabilities and an unknown user — and as a binary bound in from outside an image whose own files it never opens.
Types ¶
type Evidence ¶
type Evidence struct {
// Schema is the version of this struct the writer knew.
Schema int `json:"schema"`
// Release, Version and Machine are uname(2)'s fields: "6.8.0-79-generic",
// "#79-Ubuntu SMP PREEMPT_DYNAMIC Tue Aug 12 14:42:46 UTC 2025", "x86_64".
Release string `json:"release,omitempty"`
Version string `json:"version,omitempty"`
Machine string `json:"machine,omitempty"`
// ProcVersion is /proc/version, which names the compiler and the builder
// as well — two kernels can share a release and differ here.
ProcVersion string `json:"proc_version,omitempty"`
// BootID is /proc/sys/kernel/random/boot_id: a random UUID a Linux kernel
// draws once per boot. A container sharing its host's kernel reads the
// host's; a VM's guest kernel and gVisor's Sentry each draw their own.
BootID string `json:"boot_id,omitempty"`
// UptimeSeconds is /proc/uptime's first field: how long the kernel serving
// the reader has been up. A sandbox's own kernel was booted for it.
UptimeSeconds float64 `json:"uptime_seconds,omitempty"`
// Hypervisor reports whether /proc/cpuinfo's flags include "hypervisor",
// the CPUID bit a hypervisor sets for its guests. A guest of any VM sets
// it — including a runc container on a host that is itself a cloud VM.
Hypervisor bool `json:"hypervisor,omitempty"`
// HypervisorType is /sys/hypervisor/type ("xen") where the kernel has one.
HypervisorType string `json:"hypervisor_type,omitempty"`
// DMIVendor and DMIProduct are /sys/class/dmi/id/sys_vendor and
// product_name: "QEMU" and "Standard PC (Q35 + ICH9, 2009)" in a QEMU
// guest, "Cloud Hypervisor", or the cloud's name on a cloud VM.
DMIVendor string `json:"dmi_vendor,omitempty"`
DMIProduct string `json:"dmi_product,omitempty"`
// GVisor names the gVisor markers the probe found: gVisor's fixed kernel
// build string, a release naming it, a command line naming it, and the
// boot log only the Sentry writes ("Starting gVisor...").
GVisor []string `json:"gvisor,omitempty"`
// Unreadable lists what the probe could not read and why, for the
// essential fields only — a sandbox that hides its kernel says so.
Unreadable []string `json:"unreadable,omitempty"`
}
Evidence is what one kernel says about itself: read by the probe from inside a sandbox, or by a hub or an agent about the kernel it runs on.
func Collect ¶
func Collect() Evidence
Collect gathers the evidence the running kernel gives this process.
func Decode ¶
Decode finds the probe's line in a sandbox's output and returns the evidence on it, sanitized. Only the first MaxOutput bytes are read.
The last probe line wins, and anything around it is ignored: a runtime may write a warning to the same stream, and a probe that cannot run at all — "exec format error", a cloop too old to know the mode — prints none, which the error then quotes so the reader learns which.
type Kind ¶
type Kind string
Kind is what a probe found serving a sandbox's system calls.
const ( // KindHostKernel: the executing host's own kernel — the sandbox is a // container, whatever its runtime is called. KindHostKernel Kind = "host-kernel" // KindUserspaceKernel: gVisor's Sentry, a kernel implemented in a host // process. KindUserspaceKernel Kind = "userspace-kernel" // KindVM: a guest kernel of the sandbox's own, behind a hypervisor. KindVM Kind = "vm" // KindUnknown: nothing the probe saw settles it. Never read as isolation. KindUnknown Kind = "unknown" )
type Reference ¶
type Reference struct {
// Kernel is the host's evidence. Nil when there is none.
Kernel *Evidence `json:"kernel,omitempty"`
// Source says who read it.
Source ReferenceSource `json:"source,omitempty"`
// EngineKernel is the kernel release the container engine reports for its
// own machine (`docker info`'s KernelVersion), when it was asked. It must
// match Kernel's release: an engine on another machine — DOCKER_HOST at a
// remote daemon — runs its containers on a kernel the reference does not
// describe.
EngineKernel string `json:"engine_kernel,omitempty"`
}
Reference is the executing host's own kernel, which a probe's evidence is compared with.
type ReferenceSource ¶
type ReferenceSource string
ReferenceSource says where the host's own kernel evidence came from.
const ( // SourceHub: the hub read its own kernel, for a container engine on the // hub's machine. SourceHub ReferenceSource = "hub" // SourceAgent: a device's agent read the kernel of the machine it runs on. SourceAgent ReferenceSource = "agent" // SourceClusterNode: a Pod on the same node, under the cluster's default // runtime, read the node's kernel. Within the executor Role's verbs a hub // cannot read a Node object, so this is how it learns a node's kernel — // and it is the one reference that could itself be a sandbox, if the // cluster's default runtime is one. Classify's uptime rule is what keeps // such a reference from making the host kernel look like a VM. SourceClusterNode ReferenceSource = "node" )
type Status ¶
type Status string
Status is how a runtime's claim stands against the evidence.
const ( // StatusVerified: the evidence shows at least the isolation claimed. StatusVerified Status = "verified" // StatusContradicted: the evidence shows less than the claim — a runtime // named for a VM serving from the host kernel or from gVisor, one named // for gVisor serving from the host kernel. StatusContradicted Status = "contradicted" // StatusUnverified: nothing settles it — never probed, or an unknown // verdict. StatusUnverified Status = "unverified" )
func Judge ¶
Judge compares what a runtime claims with what a probe found.
"At least the isolation claimed" is the rule, so the comparison is not symmetric: a runtime named for gVisor found to be a VM is verified, because a guest kernel keeps syscalls off the host kernel too, while a runtime named for a VM found to be gVisor is contradicted, because there is no hypervisor. A runtime that claims nothing — a plain container — is verified by any verdict but unknown: it promised nothing a verdict could break.
type Verdict ¶
type Verdict struct {
Kind Kind `json:"kind"`
// Detail names the evidence in a sentence an operator can act on.
Detail string `json:"detail"`
}
Verdict is what Classify concluded and the evidence it rests on.
func Classify ¶
Classify decides what served the probe, from its evidence and the host's.
The order is the argument:
- The same boot ID as the host is the same kernel instance. Nothing else produces it, so this wins over every marker — a host kernel someone built and named "gvisor" is still the host's.
- Without boot IDs on both sides, the host's exact build (release, build string, /proc/version) is read as the host's kernel. That under-describes a VM booting the host's own kernel image — the safe direction: a refusal an operator can investigate, never isolation that is not there.
- gVisor's markers. The Sentry's boot log and build string are not something a Linux kernel writes, so they need no reference.
- A kernel other than the host's, with a hypervisor's mark on it — the CPU's hypervisor flag, a hypervisor's DMI vendor, /sys/hypervisor — and younger than the host's kernel is a VM. Only here, once the kernel is known to differ: on its own the flag says only that *some* hypervisor is underneath, and on a cloud VM a runc container sees the cloud's. And only younger: a VM serving a sandbox booted after the machine it runs on, so a "different" kernel that has been up longer than the reference is the reference that is wrong — a cluster whose default runtime is itself a VM hands its reference Pod one, and an agent inside a VM that drives an outer host's engine reads its own guest kernel. Either would otherwise make the outer host kernel look like isolation.
- Anything else is unknown.
Nothing here asks how long the reference has been up in absolute terms: a node an autoscaler — or a CI runner — booted minutes ago is still the host, and the boot ID still says so.