Source: runc
Version: 1.1.5+ds1-1+deb12u1
Severity: normal
Dear Maintainer,
runc 1.1.5 only generates device properties for devices with symlinks
in /dev/{char,block}/ in order to suppress warnings from systemd.
However, devices like those from NVIDIA (/dev/nvidiactl, /dev/nvidia0,
...) lack those symlinks and thus corresponding DeviceAllow rules are
never generated. This effects that NVIDIA devices inside containers can
no longer be accessed with operation not permitted errors after
systemctl daemon-reload has been executed which puts those device
access rules into effect. The bug is fixed upstream in [1]. The
backport requires minor context changes. We confirmed that the backport
fixes the observed behavior by appropriately generating DeviceAllow
rules for NVIDIA devices. NVIDIA devices then remain functional inside
containers also after executing systemctl daemon-reload.
Best regards
Tobias Preclik
[1]
https://github.com/opencontainers/runc/commit/d7208f59105e079ba037aea05f9d0603ba7f779f
-- System Information:
Debian Release: 12.10
APT prefers oldstable-updates
APT policy: (500, 'oldstable-updates'), (500, 'oldstable-security'),
(500, 'oldstable')
Architecture: amd64 (x86_64)
diff -Nru runc-1.1.5+ds1/debian/changelog runc-1.1.5+ds1/debian/changelog
--- runc-1.1.5+ds1/debian/changelog 2024-02-02 13:53:05.000000000 +0000
+++ runc-1.1.5+ds1/debian/changelog 2025-12-15 10:02:07.000000000 +0000
@@ -1,3 +1,9 @@
+runc (1.1.5+ds1-1+deb12u2) bookworm; urgency=medium
+
+ * backport upstream commit d7208f59105e079ba037aea05f9d0603ba7f779f
+
+ -- Tobias Preclik <[email protected]> Mon, 15 Dec 2025 10:02:07 +0000
+
runc (1.1.5+ds1-1+deb12u1) bookworm-security; urgency=high
* Team upload.
diff -Nru runc-1.1.5+ds1/debian/patches/0011-sdver-gen-dev-props.patch runc-1.1.5+ds1/debian/patches/0011-sdver-gen-dev-props.patch
--- runc-1.1.5+ds1/debian/patches/0011-sdver-gen-dev-props.patch 1970-01-01 00:00:00.000000000 +0000
+++ runc-1.1.5+ds1/debian/patches/0011-sdver-gen-dev-props.patch 2025-12-15 10:00:38.000000000 +0000
@@ -0,0 +1,153 @@
+Description: libct/cg/sd: use systemd version when generating dev props
+Backport of upstream commit d7208f59105e079ba037aea05f9d0603ba7f779f,
+ adapted for runc 1.1.5+ds1.
+Origin: upstream
+Author: Kir Kolyshkin <[email protected]>
+Backported-by: Tobias Preclik <[email protected]>
+Applied-Upstream: https://github.com/opencontainers/runc/commit/d7208f59105e079ba037aea05f9d0603ba7f779f
+Last-Update: 2025-12-15
+Forwarded: not-needed
+
+From 13e94b4e9312cd46d64f98f2b7a05d04ed58ef6d Mon Sep 17 00:00:00 2001
+From: Tobias Preclik <[email protected]>
+Date: Mon, 15 Dec 2025 09:56:05 +0100
+Subject: [PATCH] libct/cg/sd: use systemd version when generating dev props
+
+Commit 343951a22b58c38feb added a call to os.Stat for the device path
+when generating systemd device properties, to avoid systemd warning for
+non-existing devices. The idea was, since systemd uses stat(2) to look
+up device properties for a given path, it will fail anyway. In addition,
+this allowed to suppress a warning like this from systemd:
+
+> Couldn't stat device /dev/char/10:200
+
+NOTE that this was done because:
+ - systemd could not add the rule anyway;
+ - runs puts its own set of rules on top of what systemd does.
+
+Apparently, the above change broke some setups, resulting in inability
+to use e.g. /dev/null inside a container. My guess is this is because
+in cgroup v2 we add a second eBPF program, which is not used if the
+first one (added by systemd) returns "access denied".
+
+Next, commit 3b9582895b8685 fixed that by adding a call to os.Stat for
+"/sys/"+path (meaning, if "/dev/char/10:200" does not exist, we retry
+with "/sys/dev/char/10:200", and if it exists, proceed with adding a
+device rule with the original (non-"/sys") path).
+
+How that second fix ever worked was a mystery, because the path we gave
+to systemd still doesn't exist.
+
+Well, I think now I know.
+
+Since systemd v240 (commit 74c48bf5a8005f20) device access rules
+specified as /dev/{block|char}/MM:mm are no longer looked up on the
+filesystem, instead, if possible, those are parsed from the string.
+
+So, we need to do different things, depending on systemd version:
+
+ - for systemd >= v240, use the /dev/{char,block}/MM:mm as is, without
+ doing stat() -- since systemd doesn't do stat() either;
+ - for older version, check if the path exists, and skip passing it on
+ to systemd otherwise.
+ - the check for /sys/dev/{block,char}/MM:mm is not needed in either
+ case.
+
+Pass the systemd version to the function that generates the rules, and
+fix it accordingly.
+
+Backport notes:
+Handle sdVer directly in generateDeviceProperties.
+
+Signed-off-by: Tobias Preclik <[email protected]>
+---
+ libcontainer/cgroups/systemd/common.go | 34 ++++++++++++--------------
+ libcontainer/cgroups/systemd/v1.go | 2 +-
+ libcontainer/cgroups/systemd/v2.go | 2 +-
+ 3 files changed, 17 insertions(+), 21 deletions(-)
+
+diff --git a/libcontainer/cgroups/systemd/common.go b/libcontainer/cgroups/systemd/common.go
+index b6bfb080..d51f2119 100644
+--- a/libcontainer/cgroups/systemd/common.go
++++ b/libcontainer/cgroups/systemd/common.go
+@@ -177,7 +177,9 @@ func allowAllDevices() []systemdDbus.Property {
+
+ // generateDeviceProperties takes the configured device rules and generates a
+ // corresponding set of systemd properties to configure the devices correctly.
+-func generateDeviceProperties(r *configs.Resources) ([]systemdDbus.Property, error) {
++func generateDeviceProperties(r *configs.Resources, cm *dbusConnManager) ([]systemdDbus.Property, error) {
++ sdVer := systemdVersion(cm)
++
+ if r.SkipDevices {
+ return nil, nil
+ }
+@@ -238,9 +240,10 @@ func generateDeviceProperties(r *configs.Resources) ([]systemdDbus.Property, err
+ // trickery to convert things:
+ //
+ // * Concrete rules with non-wildcard major/minor numbers have to use
+- // /dev/{block,char} paths. This is slightly odd because it means
+- // that we cannot add whitelist rules for devices that don't exist,
+- // but there's not too much we can do about that.
++ // /dev/{block,char}/MAJOR:minor paths. Before v240, systemd uses
++ // stat(2) on such paths to look up device properties, meaning we
++ // cannot add whitelist rules for devices that don't exist. Since v240,
++ // device properties are parsed from the path string.
+ //
+ // However, path globbing is not support for path-based rules so we
+ // need to handle wildcards in some other manner.
+@@ -288,21 +291,14 @@ func generateDeviceProperties(r *configs.Resources) ([]systemdDbus.Property, err
+ case devices.CharDevice:
+ entry.Path = fmt.Sprintf("/dev/char/%d:%d", rule.Major, rule.Minor)
+ }
+- // systemd will issue a warning if the path we give here doesn't exist.
+- // Since all of this logic is best-effort anyway (we manually set these
+- // rules separately to systemd) we can safely skip entries that don't
+- // have a corresponding path.
+- if _, err := os.Stat(entry.Path); err != nil {
+- // Also check /sys/dev so that we don't depend on /dev/{block,char}
+- // being populated. (/dev/{block,char} is populated by udev, which
+- // isn't strictly required for systemd). Ironically, this happens most
+- // easily when starting containerd within a runc created container
+- // itself.
+-
+- // We don't bother with securejoin here because we create entry.Path
+- // right above here, so we know it's safe.
+- if _, err := os.Stat("/sys" + entry.Path); err != nil {
+- logrus.Warnf("skipping device %s for systemd: %s", entry.Path, err)
++ if sdVer < 240 {
++ // Old systemd versions use stat(2) on path to find out device major:minor
++ // numbers and type. If the path doesn't exist, it will not add the rule,
++ // emitting a warning instead.
++ // Since all of this logic is best-effort anyway (we manually set these
++ // rules separately to systemd) we can safely skip entries that don't
++ // have a corresponding path.
++ if _, err := os.Stat(entry.Path); err != nil {
+ continue
+ }
+ }
+diff --git a/libcontainer/cgroups/systemd/v1.go b/libcontainer/cgroups/systemd/v1.go
+index a74a05a5..596e0dd3 100644
+--- a/libcontainer/cgroups/systemd/v1.go
++++ b/libcontainer/cgroups/systemd/v1.go
+@@ -76,7 +76,7 @@ var legacySubsystems = []subsystem{
+ func genV1ResourcesProperties(r *configs.Resources, cm *dbusConnManager) ([]systemdDbus.Property, error) {
+ var properties []systemdDbus.Property
+
+- deviceProperties, err := generateDeviceProperties(r)
++ deviceProperties, err := generateDeviceProperties(r, cm)
+ if err != nil {
+ return nil, err
+ }
+diff --git a/libcontainer/cgroups/systemd/v2.go b/libcontainer/cgroups/systemd/v2.go
+index de0cb974..dc39a658 100644
+--- a/libcontainer/cgroups/systemd/v2.go
++++ b/libcontainer/cgroups/systemd/v2.go
+@@ -182,7 +182,7 @@ func genV2ResourcesProperties(r *configs.Resources, cm *dbusConnManager) ([]syst
+ // aren't the end of the world, but it is a bit concerning. However
+ // it's unclear if systemd removes all eBPF programs attached when
+ // doing SetUnitProperties...
+- deviceProperties, err := generateDeviceProperties(r)
++ deviceProperties, err := generateDeviceProperties(r, cm)
+ if err != nil {
+ return nil, err
+ }
diff -Nru runc-1.1.5+ds1/debian/patches/series runc-1.1.5+ds1/debian/patches/series
--- runc-1.1.5+ds1/debian/patches/series 2024-02-02 13:53:05.000000000 +0000
+++ runc-1.1.5+ds1/debian/patches/series 2025-12-15 09:59:00.000000000 +0000
@@ -16,3 +16,4 @@
CVE-2024-21626/0016-libcontainer-mark-all-non-stdio-fds-O_CLOEXEC-before.patch
CVE-2024-21626/0017-init-don-t-special-case-logrus-fds.patch
CVE-2024-21626/0018-Adapt-eaccess-check-for-runc-1.1.6.patch
+0011-sdver-gen-dev-props.patch -p1
Description: libct/cg/sd: use systemd version when generating dev props
Backport of upstream commit d7208f59105e079ba037aea05f9d0603ba7f779f,
adapted for runc 1.1.5+ds1.
Origin: upstream
Author: Kir Kolyshkin <[email protected]>
Backported-by: Tobias Preclik <[email protected]>
Applied-Upstream: https://github.com/opencontainers/runc/commit/d7208f59105e079ba037aea05f9d0603ba7f779f
Last-Update: 2025-12-15
Forwarded: not-needed
From 13e94b4e9312cd46d64f98f2b7a05d04ed58ef6d Mon Sep 17 00:00:00 2001
From: Tobias Preclik <[email protected]>
Date: Mon, 15 Dec 2025 09:56:05 +0100
Subject: [PATCH] libct/cg/sd: use systemd version when generating dev props
Commit 343951a22b58c38feb added a call to os.Stat for the device path
when generating systemd device properties, to avoid systemd warning for
non-existing devices. The idea was, since systemd uses stat(2) to look
up device properties for a given path, it will fail anyway. In addition,
this allowed to suppress a warning like this from systemd:
> Couldn't stat device /dev/char/10:200
NOTE that this was done because:
- systemd could not add the rule anyway;
- runs puts its own set of rules on top of what systemd does.
Apparently, the above change broke some setups, resulting in inability
to use e.g. /dev/null inside a container. My guess is this is because
in cgroup v2 we add a second eBPF program, which is not used if the
first one (added by systemd) returns "access denied".
Next, commit 3b9582895b8685 fixed that by adding a call to os.Stat for
"/sys/"+path (meaning, if "/dev/char/10:200" does not exist, we retry
with "/sys/dev/char/10:200", and if it exists, proceed with adding a
device rule with the original (non-"/sys") path).
How that second fix ever worked was a mystery, because the path we gave
to systemd still doesn't exist.
Well, I think now I know.
Since systemd v240 (commit 74c48bf5a8005f20) device access rules
specified as /dev/{block|char}/MM:mm are no longer looked up on the
filesystem, instead, if possible, those are parsed from the string.
So, we need to do different things, depending on systemd version:
- for systemd >= v240, use the /dev/{char,block}/MM:mm as is, without
doing stat() -- since systemd doesn't do stat() either;
- for older version, check if the path exists, and skip passing it on
to systemd otherwise.
- the check for /sys/dev/{block,char}/MM:mm is not needed in either
case.
Pass the systemd version to the function that generates the rules, and
fix it accordingly.
Backport notes:
Handle sdVer directly in generateDeviceProperties.
Signed-off-by: Tobias Preclik <[email protected]>
---
libcontainer/cgroups/systemd/common.go | 34 ++++++++++++--------------
libcontainer/cgroups/systemd/v1.go | 2 +-
libcontainer/cgroups/systemd/v2.go | 2 +-
3 files changed, 17 insertions(+), 21 deletions(-)
diff --git a/libcontainer/cgroups/systemd/common.go b/libcontainer/cgroups/systemd/common.go
index b6bfb080..d51f2119 100644
--- a/libcontainer/cgroups/systemd/common.go
+++ b/libcontainer/cgroups/systemd/common.go
@@ -177,7 +177,9 @@ func allowAllDevices() []systemdDbus.Property {
// generateDeviceProperties takes the configured device rules and generates a
// corresponding set of systemd properties to configure the devices correctly.
-func generateDeviceProperties(r *configs.Resources) ([]systemdDbus.Property, error) {
+func generateDeviceProperties(r *configs.Resources, cm *dbusConnManager) ([]systemdDbus.Property, error) {
+ sdVer := systemdVersion(cm)
+
if r.SkipDevices {
return nil, nil
}
@@ -238,9 +240,10 @@ func generateDeviceProperties(r *configs.Resources) ([]systemdDbus.Property, err
// trickery to convert things:
//
// * Concrete rules with non-wildcard major/minor numbers have to use
- // /dev/{block,char} paths. This is slightly odd because it means
- // that we cannot add whitelist rules for devices that don't exist,
- // but there's not too much we can do about that.
+ // /dev/{block,char}/MAJOR:minor paths. Before v240, systemd uses
+ // stat(2) on such paths to look up device properties, meaning we
+ // cannot add whitelist rules for devices that don't exist. Since v240,
+ // device properties are parsed from the path string.
//
// However, path globbing is not support for path-based rules so we
// need to handle wildcards in some other manner.
@@ -288,21 +291,14 @@ func generateDeviceProperties(r *configs.Resources) ([]systemdDbus.Property, err
case devices.CharDevice:
entry.Path = fmt.Sprintf("/dev/char/%d:%d", rule.Major, rule.Minor)
}
- // systemd will issue a warning if the path we give here doesn't exist.
- // Since all of this logic is best-effort anyway (we manually set these
- // rules separately to systemd) we can safely skip entries that don't
- // have a corresponding path.
- if _, err := os.Stat(entry.Path); err != nil {
- // Also check /sys/dev so that we don't depend on /dev/{block,char}
- // being populated. (/dev/{block,char} is populated by udev, which
- // isn't strictly required for systemd). Ironically, this happens most
- // easily when starting containerd within a runc created container
- // itself.
-
- // We don't bother with securejoin here because we create entry.Path
- // right above here, so we know it's safe.
- if _, err := os.Stat("/sys" + entry.Path); err != nil {
- logrus.Warnf("skipping device %s for systemd: %s", entry.Path, err)
+ if sdVer < 240 {
+ // Old systemd versions use stat(2) on path to find out device major:minor
+ // numbers and type. If the path doesn't exist, it will not add the rule,
+ // emitting a warning instead.
+ // Since all of this logic is best-effort anyway (we manually set these
+ // rules separately to systemd) we can safely skip entries that don't
+ // have a corresponding path.
+ if _, err := os.Stat(entry.Path); err != nil {
continue
}
}
diff --git a/libcontainer/cgroups/systemd/v1.go b/libcontainer/cgroups/systemd/v1.go
index a74a05a5..596e0dd3 100644
--- a/libcontainer/cgroups/systemd/v1.go
+++ b/libcontainer/cgroups/systemd/v1.go
@@ -76,7 +76,7 @@ var legacySubsystems = []subsystem{
func genV1ResourcesProperties(r *configs.Resources, cm *dbusConnManager) ([]systemdDbus.Property, error) {
var properties []systemdDbus.Property
- deviceProperties, err := generateDeviceProperties(r)
+ deviceProperties, err := generateDeviceProperties(r, cm)
if err != nil {
return nil, err
}
diff --git a/libcontainer/cgroups/systemd/v2.go b/libcontainer/cgroups/systemd/v2.go
index de0cb974..dc39a658 100644
--- a/libcontainer/cgroups/systemd/v2.go
+++ b/libcontainer/cgroups/systemd/v2.go
@@ -182,7 +182,7 @@ func genV2ResourcesProperties(r *configs.Resources, cm *dbusConnManager) ([]syst
// aren't the end of the world, but it is a bit concerning. However
// it's unclear if systemd removes all eBPF programs attached when
// doing SetUnitProperties...
- deviceProperties, err := generateDeviceProperties(r)
+ deviceProperties, err := generateDeviceProperties(r, cm)
if err != nil {
return nil, err
}