Skip to content

[1.4] libct: reuse tmpfs for directory masks#5281

Merged
kolyshkin merged 10 commits into
opencontainers:release-1.4from
lifubang:backport-5275-1.4
May 21, 2026
Merged

[1.4] libct: reuse tmpfs for directory masks#5281
kolyshkin merged 10 commits into
opencontainers:release-1.4from
lifubang:backport-5275-1.4

Conversation

@lifubang

@lifubang lifubang commented May 13, 2026

Copy link
Copy Markdown
Member

Backport #5190, #5275, and #5285

#5190

A lot of filesystem-related stuff happens inside the container root
directory, and we have used its name before. It makes sense to pre-open
it and use a *os.File handle instead.

Function names in internal/pathrs are kept as is for simplicity (and it
is an internal package), but they now accept root as *os.File.

This is a somewhat naive attempt at it, but it is surprisingly simple.

#5275

This PR carries forward #5262.

Wondering if a problem that showed up recently in k8s when we added more masked paths can be handled better in runc itself? please see details below:

Kubernetes may add one sysfs thermal_throttle entry per CPU to maskedPaths. On large Intel systems this can produce many directory masks for a single container. runc currently handles each directory mask with a separate read-only tmpfs mount, and therefore a separate tmpfs superblock.

On Linux 4.18/RHEL 8 kernels, creating and tearing down many tmpfs superblocks can contend on the global shrinker_rwsem when containers start or stop concurrently.

Use one read-only tmpfs for directory masks and bind-mount it over the remaining directory targets. The first non-procfs-fd directory mount is reopened through the container root fd before it is reused. File masks still bind /dev/null, and procfs fd targets keep the existing one-tmpfs-per-target behavior because they are fd aliases rather than stable rootfs paths.

The bind mounts do not create additional tmpfs superblocks. They also retain the read-only mount flag inherited from the source vfsmount, so the masking semantics remain unchanged.

xref: kubernetes/kubernetes#138512
xref: kubernetes/kubernetes#138388
xref: kubernetes/kubernetes#131018

(With some assistance from claude/codex)

This PR is a follow-up to #5275.

In that change, we started reusing a single tmpfs mount to mask multiple directories. While this optimization works well when masking more than one path, it introduces unnecessary overhead when only a single directory needs to be masked.

This change restores the original behavior for single-path masking while preserving the shared tmpfs logic for multiple paths.

@lifubang lifubang added the backport/1.4-pr A backport PR to release-1.4 label May 13, 2026
@kolyshkin

Copy link
Copy Markdown
Contributor
Error: libcontainer/rootfs_linux.go:414:21: cannot use c.root (variable of type string) as *os.File value in argument to maskPaths
Error: libcontainer/rootfs_linux.go:1395:40: cannot use rootFd (variable of type *os.File) as string value in argument to reopenAfterMount

Either don't backport to 1.4, or this needs to be reworked

@lifubang

Copy link
Copy Markdown
Member Author

this needs to be reworked

We also need to backport #5190.

@lifubang lifubang marked this pull request as draft May 15, 2026 03:42
@kolyshkin kolyshkin added this to the 1.4.3 milestone May 18, 2026
kolyshkin and others added 10 commits May 20, 2026 10:21
Indeed, it does not make sense to prepend c.root once we started using
MkdirAllInRoot in commit 63c2908.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
(cherry picked from commit 6035252)
Signed-off-by: lifubang <lifubang@acmcoder.com>
No change in functionality, just a preparation for the next patch.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
(cherry picked from commit 78b8067)
Signed-off-by: lifubang <lifubang@acmcoder.com>
A lot of filesystem-related stuff happens inside the container root
directory, and we have used its name before. It makes sense to pre-open
it and use a *os.File handle instead.

Function names in internal/pathrs are kept as is for simplicity (and it
is an internal package), but they now accept root as *os.File.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
(cherry picked from commit 28cb321)
Signed-off-by: lifubang <lifubang@acmcoder.com>
This uses preopened rootfs in Chdir and pivotRoot.

While at it, add O_PATH when opening oldroot in pivotRoot.

Co-authored-by: Kir Kolyshkin <kolyshkin@gmail.com>
Signed-off-by: lfbzhm <lifubang@acmcoder.com>
Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
(cherry picked from commit 5b094ed)
Signed-off-by: lifubang <lifubang@acmcoder.com>
Co-authored-by: Davanum Srinivas <davanum@gmail.com>
Refactored-by: lifubang <lifubang@acmcoder.com>
Signed-off-by: lifubang <lifubang@acmcoder.com>
(cherry picked from commit abf70ba)
Signed-off-by: lifubang <lifubang@acmcoder.com>
Previously, masked directories (e.g., /proc/acpi, /proc/scsi) were
mounted as read-only tmpfs without explicit size or inode limits.
Although these mounts are meant to be empty and unwritable, the lack
of resource constraints means that—should an attacker bypass the
read-only protection (e.g., via container escape, mount namespace
manipulation, or a kernel vulnerability)—the tmpfs could consume up
to 50% of system memory by default (the kernel's default tmpfs limit).

To mitigate this risk in high-density container environments and
adhere to the principle of least privilege, we now explicitly set:
  - nr_blocks=1 (sufficient for at most one block size)
  - nr_inodes=1 (sufficient for at most one inode)
Ref: https://man7.org/linux/man-pages/man5/tmpfs.5.html

These limits ensure that even if compromised, kernel memory usage
remains strictly bounded and negligible.

This change aligns with best practices used by other container
runtimes and strengthens defense-in-depth for sensitive masked paths.

Co-authored-by: Davanum Srinivas <davanum@gmail.com>
Refactored-by: lifubang <lifubang@acmcoder.com>
Signed-off-by: lifubang <lifubang@acmcoder.com>
(cherry picked from commit e57a7a4)
Signed-off-by: lifubang <lifubang@acmcoder.com>
Kubernetes may add one sysfs thermal_throttle entry per CPU to
maskedPaths. On large Intel systems this can produce many directory
masks for a single container. runc currently handles each directory
mask with a separate read-only tmpfs mount, and therefore a separate
tmpfs superblock.

On Linux 4.18/RHEL 8 kernels, creating and tearing down many tmpfs
superblocks can contend on the global shrinker_rwsem when containers
start or stop concurrently.

Use one read-only tmpfs for directory masks and bind-mount it over the
remaining directory targets. The first non-procfs-fd directory mount is
reopened through the container root fd before it is reused. File masks
still bind /dev/null, and procfs fd targets keep the existing
one-tmpfs-per-target behaviour because they are fd aliases rather than
stable rootfs paths.

If the bind-mount of the shared source fails (e.g. due to kernel
restrictions), fall back to individual tmpfs mounts for all remaining
directories. Tmpfs mounts use nr_blocks=1,nr_inodes=1 to minimise
kernel resource usage.

The bind mounts do not create additional tmpfs superblocks. They also
retain the read-only mount flag inherited from the source vfsmount, so
the masking semantics remain unchanged.

xref: kubernetes/kubernetes#138512
xref: kubernetes/kubernetes#138388
xref: kubernetes/kubernetes#131018

Co-authored-by: Davanum Srinivas <davanum@gmail.com>
Refactored-by: lifubang <lifubang@acmcoder.com>
Signed-off-by: lifubang <lifubang@acmcoder.com>
(cherry picked from commit c046c9b)
Signed-off-by: lifubang <lifubang@acmcoder.com>
Co-authored-by: Davanum Srinivas <davanum@gmail.com>
Signed-off-by: lifubang <lifubang@acmcoder.com>
(cherry picked from commit 124772f)
Signed-off-by: lifubang <lifubang@acmcoder.com>
This is a follow-up to opencontainers#5275. That change reused a single tmpfs mount
to mask multiple directories, which is efficient when masking more than
one path. However, it introduced unnecessary overhead when only one
directory is masked. This commit restores the original behavior for the
single-path case while preserving shared tmpfs logic for multiple paths.

Signed-off-by: lifubang <lifubang@acmcoder.com>
(cherry picked from commit e7e2f00)
Signed-off-by: lifubang <lifubang@acmcoder.com>
Close the root file descriptor immediately after use in maskPaths to
reduce the window during which an attacker could potentially exploit
an open fd to access or manipulate the root filesystem. This follows
the principle of least privilege and mitigates risks in compromised
or malicious container scenarios.

Co-authored-by: Kir Kolyshkin <kolyshkin@gmail.com>
Signed-off-by: lifubang <lifubang@acmcoder.com>
(cherry picked from commit b88635e)
Signed-off-by: lifubang <lifubang@acmcoder.com>
@lifubang lifubang force-pushed the backport-5275-1.4 branch from a613bb8 to 0d9be80 Compare May 20, 2026 10:21
@lifubang lifubang marked this pull request as ready for review May 20, 2026 10:23
@kolyshkin kolyshkin requested review from cyphar and rata and removed request for cyphar May 20, 2026 20:47

@rata rata left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I found a bug in the original PR (the bindFailed carry-over in iterations). The rest is more cosmetical or me wanting to understand more. Sorry I didn't review the orignal PRs

Comment thread libcontainer/rootfs_linux.go Outdated
Comment thread libcontainer/rootfs_linux.go
Comment on lines +1390 to +1395
err = mountViaFds("", sharedMaskSrc, path, dstFd, "", unix.MS_BIND, "")
if err != nil {
// A bind-mount inherits MNT_READONLY from the source vfsmount,
// but if it fails fall back to individual tmpfs mounts.
bindFailed = true
logrus.WithError(err).Warn("maskPaths: shared tmpfs bind-mount failed, falling back to per-directory tmpfs")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can this fail and we should care about a fallback instead of returning an error?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I have no idea why this bind mount might have failed -- but treating it as a recoverable scenario (with a fallback) seems like a feature, not a flaw. That’s the robust!

@rata rata May 21, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, if this can fail and the fallback works in some real-world scenario, I'm okay with this. Otherwise it is just dead-code and added complexity for no real good.

It seems crun also has the fallback, so maybe it is ok... I'm not convinced, but okay :D

@rata rata left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@kolyshkin kolyshkin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM (already reviewed the original PRs, not really looking at this backport)

@kolyshkin kolyshkin merged commit 3079745 into opencontainers:release-1.4 May 21, 2026
68 of 69 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport/1.4-pr A backport PR to release-1.4

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants