Skip to content

validation: check the device rules by trying them - #816

Open
dangowrt wants to merge 5 commits into
opencontainers:masterfrom
dangowrt:devices-on-812
Open

dangowrt wants to merge 5 commits into
opencontainers:masterfrom
dangowrt:devices-on-812

Conversation

@dangowrt

@dangowrt dangowrt commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

This builds on #812 and should be read after it. It is that branch plus one commit, validation: check the device rules by trying them, which is the only part that is new here; the four commits below it are #812's and will disappear from this diff once that merges.

linux_cgroups_devices reads the configured rules back out of devices.list, which is a cgroup v1 interface. #812 skips the test on a unified hierarchy for that reason, and the wording there is careful to say that the restrictions are still applied and still enforced, by an attached BPF_CGROUP_DEVICE program in the runtimes that implement it. Only the window the test looks through is gone, not the property it is checking, so skipping it is a poor final answer. This replaces the skip with a check that works on both hierarchies.

The three rules the test already configures name no device that the bundle puts in the container, so nothing about them can be observed from the inside. Two device nodes fix that: /dev/allowed at c 10:229, which the first of those rules permits, and /dev/denied at c 10:230, which only the generator's leading deny-all covers. The rules themselves are untouched, so the cgroup v1 path reads exactly what it read before, and linux.devices entries do not become cgroup rules in any runtime I checked. On a unified hierarchy runtimetest works out what the list permits by applying the entries in the listed order, which is the one requirement the spec states about this field, and compares that with what opening each node actually does. Only EPERM means the device controller refused, and it comes back before the driver behind the node is consulted, so nothing has to be loaded for the answer to be meaningful. Devices a runtime is required to supply are skipped, because runtimes add rules of their own for those and they say nothing about the configured list.

Verified as root on a cgroup v2 host against two runtimes, both reporting ok for the allowed node and the denied node: crun 1.29.1, and uxc, the OpenWrt runtime whose cgroup v2 device support is an eBPF emulation of the cgroup v1 semantics. Two independent implementations agreeing is what makes me think the test measures the property rather than one runtime's quirk. It has not been run against runc, because the machine with the privileges to test device enforcement does not have runc on it, so I want to be explicit about that rather than leave it implied.

The reason I am reasonably confident about runc anyway, and the reason the fixture uses these particular numbers, is that runc appends its own allow rules after the ones from config.json:

// Append the default allowed devices to the end of the list.
for _, device := range defaultDevs {
	c.Resources.Devices = append(c.Resources.Devices, &device.Rule)
}

The comment on AllowedDevices concedes that this is "at the very least 'questionable' (if not outright wrong) according to the runtime-spec" and that users "cannot disable this behaviour". Those appended rules are eleven allows: c *:* m and b *:* m, which grant mknod alone, then c 1:3, c 1:8, c 1:7, c 5:0, c 1:5, c 1:9, c 136:*, c 5:2 and c 10:200. None of them grants read access to c 10:229 or c 10:230, so neither node is re-allowed behind the configured list, and the wildcard m rules mean creating both nodes still works under the leading deny-all. A fixture that denied one of the default devices instead would have failed against runc for reasons that are runc's rather than the test's, which is what these numbers avoid. If a reviewer with runc to hand finds otherwise I would very much like to know.

cmd/runtimetest/main_test.go covers the rule evaluation itself: wildcards, type mismatches, a deny that does not mention read, and a later entry overriding an earlier one.

The device-ordering question this raises is now opencontainers/runtime-spec#1321.

FindCgroup() refused to work on a host that only mounts the unified
hierarchy, and CgroupV2 was a stub whose methods carried no receivers,
so the type never satisfied the Cgroup interface. Every validation test
that reads resource limits back out of the cgroup filesystem failed with
"cgroupv2 is not supported yet" on any current distribution.

Read the unified control files instead: memory.{max,low,swap.max},
cpu.max, cpuset.{cpus,mems}, pids.max, hugetlb.<size>.max and io.max.
memory.swap.max limits swap alone while the configuration field covers
memory plus swap, so the memory limit is added back. cpuset files fall
back to the effective set, which is where the unified hierarchy reports
an inherited value.

Values the unified hierarchy does not carry are left unset rather than
guessed, so that a caller can tell them apart from a real zero. That
covers swappiness, disableOOMKiller, useHierarchy, kernel, kernelTCP
and the block IO weights, and also cpu.shares, which runtimes map onto
cpu.weight through a lossy conversion the runtime-spec does not define.
The devices and network controllers have no unified counterpart at all:
device access is enforced by an eBPF program that cannot be read back,
and net_cls and net_prio were never ported, so both report that rather
than a missing file.

Version() lets a caller ask which hierarchy it is talking to.

Relates to opencontainers#807.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
The cgroup test bundles ask for memory.swappiness, memory.disableOOMKiller,
memory.kernel, memory.kernelTCP, blockIO.leafWeight and the block IO
weights on every host. None of those exist in the unified hierarchy, and
the create operation requires a runtime that cannot apply a property to
generate an error and not create the container. A correct cgroup v2 only
runtime therefore fails these tests by doing exactly what the spec asks
of it.

Set those fields only where the hierarchy can carry them. cpu.shares goes
the same way: runtimes map it onto cpu.weight with a lossy conversion the
runtime-spec does not define, so the configured value cannot be recovered
and asserting on it would test a convention rather than the spec.

Compare the remaining fields with a helper that reports a value neither
side carries as a diagnostic instead of a failure, so that one unsupported
field no longer hides the checks that follow it: the block IO and cpu
validators used to return early and silently drop every later assertion.
linux_cgroups_relative_cpus grows the same behaviour by using the shared
CPU validator its siblings already use rather than its own copy of it.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
linux.resources.network and linux.resources.devices have no counterpart
in the unified hierarchy at all. net_cls and net_prio were never ported,
and device access is enforced by an eBPF program that cannot be read back
from the filesystem, so there is nothing for these tests to compare the
configuration against.

Skip the four tests where the host runs a unified hierarchy, the way the
suite already skips tests that do not apply to the platform it runs on.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Both delete tests build the cgroup directory out of a literal
/sys/fs/cgroup/pids. On a unified hierarchy that directory never exists,
so delete_only_create_resources fails when it tries to populate it and
delete_resources passes without testing anything: the path it expects the
runtime to have removed was never there.

Ask the Cgroup implementation where the cgroup lives instead, which also
picks up the real mount point rather than assuming one. Move a process
with cgroup.procs, which both hierarchies provide, rather than with the
cgroup v1 only tasks file.

Fixes opencontainers#807

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
The unified hierarchy applies the same device rules cgroup v1 did, but it
publishes no list to read them back from, so linux_cgroups_devices has
nothing to compare against and is skipped there. The rules are still in
force; only this test's way of looking at them is gone.

Look at them from inside the container instead. The three rules the test
already configures name no device that the bundle puts in the container,
so add a node the first of them allows and one that only the generator's
leading deny-all covers. The rules themselves are untouched, which leaves
the cgroup v1 path reading exactly what it read before.

runtimetest works out what the list permits by applying the entries in
the listed order, as a runtime is required to, and compares that with
what opening the node does. Only EPERM means the device controller
refused, and it is reported before the driver behind the node is
consulted, so nothing has to be loaded for the answer to be meaningful.
Devices a runtime must supply are left alone, since runtimes add rules of
their own for those.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
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