bubblewrap: Use the new mount API for overlay mounts - #805
Open
filbranden wants to merge 8 commits into
Open
filbranden wants to merge 8 commits into
filbranden wants to merge 8 commits into
Conversation
The loop that collects the lower layers advances op to the last SETUP_OVERLAY_SRC op. Those ops hold only a source, so the error messages printed "Can't make overlay mount on (null)". Save the destination before the loop runs. Signed-off-by: Filipe Brandenburger <filbranden@gmail.com>
Collect the layers first, then mount. The walk over the SETUP_OVERLAY_SRC ops no longer builds the options string as it goes, so a second way to mount an overlay can reuse the same layer list. No change in behaviour. Signed-off-by: Filipe Brandenburger <filbranden@gmail.com>
These syscalls are part of the same "new mount API" as mount_setattr(). Provide the syscall numbers on the same architectures, and return ENOSYS where the number is unknown. glibc guards its whole fsopen() group on FSOPEN_CLOEXEC, and the FSCONFIG_* names are enumerators rather than macros, so guard the constants the same way instead of testing each name. Signed-off-by: Filipe Brandenburger <filbranden@gmail.com>
mount(2) takes every lower layer in one options string, and the kernel
keeps at most one page of it. The rest is cut off, often in the middle of
a path, so overlayfs rejects the mount for an overlap that the caller
never asked for. With /proc/self/fd paths this caps an overlay at roughly
200 layers.
fsconfig() appends one layer per call and builds no string, so the layer
count is bounded only by overlayfs itself. Mount the overlay with
fsopen(), one fsconfig("lowerdir+") per layer, fsmount() and move_mount().
overlayfs supports this from Linux 6.7 on. Older kernels keep the mount(2)
path. Build with -Dassume_kernel=6.7.0 to compile the fallback out, and
run with --debug-opt=force-overlay-fallback to exercise it on a kernel
that does not need it.
Signed-off-by: Filipe Brandenburger <filbranden@gmail.com>
The kernel keeps at most one page of the mount(2) options string and drops the rest. A path cut in half normally still names a real directory, an ancestor of the other layers, so overlayfs reports an overlap that the caller never asked for. Refuse the mount and name the real problem. Take the page size from sysconf(), because it is not 4 KiB everywhere. Signed-off-by: Filipe Brandenburger <filbranden@gmail.com>
250 lower layers do not fit in the mount(2) options string but stay well under the overlayfs maximum. Check that they mount, and that the mount(2) path refuses them with a clear message. Also run the existing overlay tests again with --debug-opt=force-overlay-fallback, so the mount(2) path keeps its coverage on kernels that no longer need it. Signed-off-by: Filipe Brandenburger <filbranden@gmail.com>
Signed-off-by: Filipe Brandenburger <filbranden@gmail.com>
Signed-off-by: Filipe Brandenburger <filbranden@gmail.com>
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 #804.
Investigated, implemented and tested with the help of Claude Code.
What
Overlay mounts (
--overlay,--ro-overlay,--tmp-overlay) are now built withfsopen()+ onefsconfig("lowerdir+")per layer +fsmount()+move_mount(), instead of passing every layer in a singlemount(2)options string.This removes the limit on how many
--overlay-srclayers bubblewrap can stack.Why
mount(2)takes its options as one string, andcopy_mount_options()keeps at most one page of it. Everything past that is dropped, usually in the middle of a path. A truncated layer path normally still names a real directory — commonly an ancestor of the other layers, because container layer stores put every layer under one parent. overlayfs then correctly reports an overlap that the caller never asked for:The error is accurate and completely misleading. The overlap is manufactured by the truncation; the layers the user passed are all distinct.
This was hit in the wild with bubblewrap 0.12.0 mounting a 31-layer container image. At the time each entry was a full absolute path (~130 bytes), so the string ran past 4096 bytes at 31 layers. c77dd38 ("setup: Use O_PATH fds and disallow symlink targets") shortened each entry to a
/proc/self/fd/Nmagic symlink, which moves the ceiling to roughly 200 layers, but the limit is still there and still reports itself as a bogus overlap.fsconfig(FSCONFIG_SET_STRING, "lowerdir+", ...)appends one layer per call and accumulates the list in the kernel, so there is no options string to overflow. The only remaining bound is overlayfs's ownOVL_MAX_STACK.Implementation
The overlay case in
setup_newroot()previously walked theSETUP_OVERLAY_SRCops and appended to aStringBuilderas it went. The first commit of the series splits that apart: the walk now just collects the layer fds, and a helper does the mounting. That makes room for a second helper without threading#ifguards through the walk.overlay_mount_fsconfig()does:fsopen("overlay", FSOPEN_CLOEXEC)fsconfig(FSCONFIG_SET_STRING, "upperdir"/"workdir", ...)when there is an upper layerfsconfig(FSCONFIG_SET_STRING, "lowerdir+", ...)once per layer, in the same order the options string usedfsconfig(FSCONFIG_SET_FLAG, "userxattr")fsconfig(FSCONFIG_CMD_CREATE)fsmount(..., MOUNT_ATTR_NOSUID | MOUNT_ATTR_NODEV), the same restrictionsMS_NOSUID | MS_NODEVapplied beforemove_mount(mount_fd, "", dest_fd, "", MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH)overlay_mount_legacy()is the previousmount(2)code, unchanged apart from taking its layers as an array.Only the
fsopen/fsconfig/fsmount/move_mountwrappers are new. They followmount_setattr_wrapper()exactly:#ifndef __NR_*fallback numbers on the same three architectures,ENOSYSwhere the number is unknown. The constants are guarded onFSOPEN_CLOEXEC, the same way glibc's<sys/mount.h>guards them, becauseFSCONFIG_*are enumerators rather than macros and cannot be tested individually.Fallback
overlayfs only gained
lowerdir+, and new mount API support generally, in Linux 6.7.fsopen()itself has existed since 5.2, so on 5.2–6.6 the call succeeds and the failure only appears atFSCONFIG_CMD_CREATE, whereEINVALis indistinguishable from a genuinely bad configuration. Rather than guess,overlay_mount_fsconfig()returns false on any failure and the caller falls back tooverlay_mount_legacy(). That costs one extramount(2)in the error case and keeps the diagnostics the existing path already produces — the ELOOP message in particular is unchanged.-Dassume_kernel=6.7.0compiles the fallback out, matching what-Dassume_kernel=5.12.0already does formount_setattr.--debug-opt=force-overlay-fallbackforces themount(2)path so it can still be tested on a kernel that does not need it, mirroringforce-mount-setattr-fallback.Two fixes on the way
Can't make overlay mount on (null). The layer walk advancesopto the lastSETUP_OVERLAY_SRC, and those ops carry only a source, no destination.mount(2)path now refuses the mount and says so, instead of letting the kernel truncate and overlayfs blame an overlap. The threshold comes fromsysconf(_SC_PAGESIZE), since the page is not 4 KiB everywhere.Testing
On Linux 6.12, x86_64, unprivileged:
meson testpasses, in a default build and with-Dassume_kernel=6.7.0.test-run.shoverlay assertions pass unchanged. They pin layer precedence (--overlay-src lower1 --overlay-src lower2must readbfrom lower2), which is what confirms repeatedlowerdir+appends keep the same order the colon-separated list had.test-sandbox.pytests mount 250 lower layers, and check that themount(2)path refuses the same 250 layers with the new message. The existing--tmp-overlayand--ro-overlaytests are also run again under--debug-opt=force-overlay-fallback; all the fallback tests skip when-Dassume_kernelhas compiled that path out.unshare(CLONE_NEWUSER | CLONE_NEWNS): 300, 400 and 499 layers all mount and every layer is visible; layer 500 fails withEINVAL, which is overlayfs'sOVL_MAX_STACK. The same program shows the equivalentmount(2)options string at 300 layers is 8118 bytes and fails. Happy to share it.Relationship to other work
This is a third use of the new mount API in bubblewrap, after #754 and #756 brought in
mount_setattr(). It is related to #755 but does not implement or close it: that issue is aboutopen_tree()+move_mount()for--[ro-]bind-fd, which is a different problem (a path-lookup race, not a length limit) and a different part of the API.fsopen()creates a new mount rather than cloning an existing one, so none of the cross-namespace constraints that apply toopen_tree()apply here.