overlay: support relative pathnames in mount options - #14732
Open
kvanzuijlen wants to merge 1 commit into
Open
kvanzuijlen wants to merge 1 commit into
kvanzuijlen wants to merge 1 commit into
Conversation
Linux resolves relative pathnames in overlayfs mount options against the working directory of the mounting process. gVisor rejected every non-absolute pathname in lowerdir, upperdir, and workdir with EINVAL. containerd depends on the Linux behaviour. Once the absolute option string would exceed one page, containerd chdir()s to the snapshot directory and passes relative pathnames instead. Docker 29 uses the containerd image store by default. Under gVisor, Docker can then neither run nor build an image of about 38 layers or more: failed to mount /tmp/containerd-mount3312412956: fstype: overlay, data: "workdir=/var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1. overlayfs/snapshots/60/work,upperdir=.../60/fs,lowerdir=59/fs:58/fs:...: 13/fs", err: invalid argument paketobuildpacks/builder-jammy-java-tiny has 39 layers. It is the default Cloud Native Buildpacks builder, so pack build and Spring Boot's bootBuildImage do not work. Take the working directory from the context, alongside the existing VFS root, and resolve relative pathnames against it. This covers both mount(2) and fsconfig(2) without either call site changing. A context without a task has no working directory. Such a context still returns EINVAL for a relative pathname rather than falling back to the root. No mount resolves to a directory the caller did not name. Also reject empty pathnames. gVisor previously accepted an empty workdir=, because "" + "/work" parses as the absolute path /work. Fixes google#14699 Assisted-by: Claude Code
kvanzuijlen
force-pushed
the
overlay-relative-mount-pathnames
branch
from
September 13, 2026 15:43
bde91b6 to
37b0aad
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #14699
Problem
Linux resolves relative pathnames in overlayfs mount options against the working directory of the mounting process. gVisor rejects them:
containerd switches to relative pathnames after a chdir, once the absolute option string would exceed one page. A shallow image therefore works and a deep one fails. With the default
--data-rootthe cutoff is near 38 layers. Docker can neither run nor build an image past it.The default Cloud Native Buildpacks builder has 39 layers.
pack buildand Spring Boot'sbootBuildImagefail under gVisor, which is how I hit this.Docker is not needed to reproduce:
This is not the same as #12475. That one goes away when a tmpfs is mounted at
/var/lib/docker, which this setup already does. With the tmpfs in place, absolute pathnames still mount at 80 layers, and keep working until the option string exceeds one page. Only the relative form returns EINVAL.Fix
CtxWorkingDirectoryandWorkingDirectoryFromContextsit next to the existingCtxRootandRootFromContext.Task.contextValueanswers them fromt.FSContext().WorkingDirectory().overlay.GetFilesystemreads the working directory once, holds a reference for the call, and resolves each pathname throughresolveStart.Only a task has a working directory. Every other context yields a zero
VirtualDentry, andresolveStartreturns EINVAL rather than falling back tovfsroot.RootstaysvfsrootandStartbecomes the working directory, which is the pairinggetTaskPathOperationalready uses for any relative pathname atAT_FDCWD.Reading the working directory from the context also covers fsconfig(2).
Fd.DoCmdCreatepasses the task toNewFilesystem, which passes it toGetFilesystem.Empty pathnames now return EINVAL. gVisor previously accepted an empty
workdir=, because"" + "/work"parses as the absolute path/work. Linux rejects all three.upperdirandworkdirusefsparam_file_or_stringwithoutfs_param_can_be_empty, so the genericfs_parserrejects them before overlay sees them, andovl_fill_superrejects an emptylowerdironctx->nr == 0. I confirmed thelowerdircase returns EINVAL on 6.12.95.Why the check existed
Not as a security boundary. The check appears verbatim in 77c206e ("Add //pkg/sentry/fsimpl/overlay.", 2020-06-12), the commit that created the package, when
Startwas unconditionallyvfsrootandGetFilesystemhad no access to a working directory. That commit lists eight deliberate divergences from Linux. This is not among them. 1c8e91d ("fs/overlay: create the "work" directory inworkdir", 2023-09-20) copied the check for workdir without stating a reason. No CVE, escape, or traversal fix in the package's history refers to the check.CVE-2021-30465
gVisor's CVE record marks that symlink-exchange race
exploitable_under_gvisor. The stated gap is that runsc handles mount paths similarly to runc. This change does not increase that exposure.GetFilesystemreads the working directory once and holds a reference for the whole call. Nothing can exchange the start dentry between the workdir, upperdir, and lowerdir resolutions, not even a concurrent chdir() in the calling process.GetDentryAtreturns each layer dentry with a reference held, so a later swap cannot retarget the mount. The symlink walk beneath the start dentry is unchanged from the existing absolute resolution againstvfsroot.A task that performs this mount already holds CAP_SYS_ADMIN in its mount namespace and can bind-mount arbitrary paths. Resolving against its working directory grants no new reach.
getTaskPathOperationalready pairsRoot: RootDirectory()withStart: WorkingDirectory()for every relative pathname atAT_FDCWD. A task whose working directory sits outside its root, after a chroot without a chdir, reaches those paths today withopenat(AT_FDCWD, "../.."). Overlay now resolves relative pathnames the same way the rest of the syscall surface does.Testing
MountTest.OverlayfsRelativePathschdirs into a directory that is not the mount target's parent, then mounts withlowerdir=../l0:../l1,upperdir=../u,workdir=../w. It checks that both lower layers are visible through the merged directory and that a new file lands in the upper layer. The test fails if any of the three pathnames resolves incorrectly.mount_test_nativepasses, so the test matches Linux.mount_test_runsc_systrap_directfs,mount_test_runsc_systrap_shared, andmount_test_runsc_systrap_overlaypass. Reverting the fix and keeping the test fails all three runsc variants:Host kernel 6.12.95, platform systrap.
overlay_testcovers the rejections. mount(2) cannot reach them, because a task always has a working directory.contexttest.Contextsupplies credentials but no working directory.AI assistance
I wrote this patch with Claude Code. The commit message carries an
Assisted-bytrailer. I reviewed every line and ran the tests above myself, using the provided makefile.Credit
@jdymitarai took the same approach in #14728, including the context key, then withdrew it before review. This version differs in what happens when there is no working directory: #14728 falls back to
vfsroot, so a relative pathname resolves against the root instead of failing. This one returns EINVAL. #14728 also answersCtxWorkingDirwith the root directory increateProcessContextandsupervisorContext; this one leavespkg/sentry/kernel/kernel.goalone, so only a task supplies a working directory.