Repository navigation
Root changes directories and opens files whatever their mode (CAP_DAC_OVERRIDE) - #34
Merged
Merged
Conversation
…_OVERRIDE) tar, running as root in a container, could not restore a Go module cache: Go makes module directories 0555, tar as root creates each directory with its final mode and then its contents, and the nested mkdir answered EACCES. The container's files are the Mac user's, so macOS enforces a mode's owner bits against the guest's root as against anyone; Linux lets uid 0 through. As root on 0.6.10, in a directory after chmod 555: mkdir, symlink, mkfifo, rmdir, and rename or unlink of a file on disk all failed; so did appending to a 0444 file, reading a 000 one, and listing or entering a 000 directory. New regular files seemed to work, because they are made in the in-memory upper layer (upm.h). They did not: the write-back into the 0555 directory got EACCES every time, the file stayed in memory and was lost when the container stopped (mkdir a; echo x > a/f; chmod 555 a; then docker cp showed an empty a/). A file call the host refused with EACCES is now done once more when the caller is root holding CAP_DAC_OVERRIDE (FS_DAC_REDO around fs_call and chdir, so every path call passes through it and the first attempt costs nothing more). That pass takes no fast path: the walk opens each directory on the way, and each one of the container's that lacks owner rwx is given it for the rest of the call, by descriptor, or, where the mode refuses the open itself, by its name in its already open parent with no symlink followed, so nothing outside the root changes. open, truncate, chdir and rename do the same for the leaf. The modes are put back by descriptor when the call ends (the entry may have been renamed or removed), unless something else changed them meanwhile; the call's own chmod is what stays. The image layer is never changed (no container writes it), nor a read-only bind mount. The write-back uses the same pass, whoever made the file. Another user still gets EACCES, and so does root after a capset that drops the capability. The in-memory layer let both make and rename new files in a 0555 directory (its mirror directories are 0777); it now asks the host directory's mode, and treats root without the capability as bound by it. Limits: while a redone call runs (microseconds) the container's other processes can see the wider mode, and an engine killed just then leaves it. dockivad rejects --cap-drop, so the capability can only be dropped from inside. Image files and directories without owner read or search permission stay shut to root. exec, chown, inotify and unix-socket paths are not redone. tests/native/rootdac_guest.c (scripts/verify_overlay.py, every runtime build, with and without the memory layer) does each operation as root in a directory made in the container, a bind mount and a 0555 directory of the image, at 0555 and 000, on files on disk and still in memory; then as another user and as root without the capability, expecting EACCES; the verifier checks the files and modes left on the host, that the image and a read-only mount are unchanged, and busybox tar extracting and rm -rf removing a tree of 0555 directories as root. It fails on main at the first mkdir.
The Linux build (GCC, glibc) failed: fs_dac_leaf discarded fchmodat's result where it puts a mode back after the open it widened it for failed, and glibc marks fchmodat warn_unused_result, which a cast does not silence. That restore went by name, and it could also hit the wrong file: when the name had been renamed over or removed between the chmod and the open, it set the old file's mode on whatever was there now. The descriptor opened is now compared with the file that was widened (device and inode) before it is kept, and the mode goes back by name only where the name still is that file and its mode is still the one set here. Where it cannot go back there, and where fs_dac_end's fchmod fails, the engine says so on stderr (the container's log) with both modes and the error: the guest's call is already done or refused by then, so there is no result to carry it. Builds with GCC 13.3 on Ubuntu 24.04 with the CI's flags, plain and sanitized.
This was referenced Nov 14, 2026
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.
The bug
tar, running as root in a Dockiva container, could not restore a Go module cache (the app repo's CI, 2026-11-14): Go makes module directories 0555, tar as root creates each directory with its final mode and then its contents, and the nestedmkdiransweredEACCES. Linux lets uid 0 through (CAP_DAC_OVERRIDE). Here the container's files are the Mac user's, so macOS enforces a mode's owner bits against the guest's root too.As root on 0.6.10 (engine
af13b2b), in a directoryaafterchmod 555 a:mkdir a/d,ln -s x a/l,mkfifo a/p,rmdir a/dmv/rmof a file already on disk inachmod 000 a:ls a,touch a/z,cd aa(touch a/f)The last row hid a second bug. New regular files are made in the in-memory upper layer (
native/upm.h), and the write-back into a 0555 directory gotEACCESevery time, so the file stayed in memory and was lost when the container stopped:mkdir a; echo x > a/f; chmod 555 a, thendocker cpshowed an emptya/. That is the order Go uses for a module directory (fill it, then make it read-only).The fix
A file call the host refused with
EACCESis done once more when the caller is root holding CAP_DAC_OVERRIDE (FS_DAC_REDOaroundfs_callandchdirinnative/fs.h). Every path call passes through that one place, and a call that succeeds pays nothing new.open,truncate,chdirandrenamedo the same for the leaf (a 000 file, a 000 directory to list, a 0555 directory moved to another parent).chmodis what stays.Another user still gets
EACCES, and so does root after acapsetthat drops the capability. The in-memory layer let both of them create and rename new files inside a 0555 directory (its mirror directories are 0777); it now asks the host directory's mode.Limits
--cap-dropis not supported: dockivad answersUnsupported HostConfig: CapDrop. The capability can only be dropped from inside the container./etc/shadow) stay shut to root.exec,chown, inotify and unix-socket paths through an unsearchable directory are not redone.statof its directory.Checks
tests/native/rootdac_guest.c, run byscripts/verify_overlay.pyon all four runtime builds (interpreter, HVF, both sanitizer builds), with and without the memory layer. It does each operation above as root in a directory made in the container, a bind mount, and a 0555 directory of the image, at 0555 and at 000, on files on disk and files still in memory; then as uid 1000 and as root without the capability, expectingEACCES. The verifier then checks the files and modes left on the host, that the image and a read-only mount are unchanged, and busyboxtarextracting plusrm -rfremoving a tree of 0555 directories as root. Onmainit fails at the firstmkdir(rootdac FAIL line 102 /w); on this branch all eight runs pass.scripts/verify_overlay.py: all 54 lines PASS.verify_fs_escape,verify_memfs,verify_lookup_cache,verify_procfs,verify_fork_sockets,verify_fexecve,verify_accept,verify_listen,verify_epoll,verify_fadvise,verify_madvise,verify_map_stack,verify_sysinfo,verify_compat_limits,verify_address_space,verify_cpu_state,verify_memory_backing,verify_hvf.verify_dockivad.py(31 checks, with and without--new-home) andverify_tty.pypass.go vetandgo test ./...indockivadpass.fchmodatiswarn_unused_resultin glibc).verify_cow(sanitizer assertion) andverify_decode_cache(its test no longer compiles) fail, the same way onmain.ubuntu:24.04: a tree with 0555 directories, 0444 files and symlinks, archived withtar --posix -cf x.tar -P, extracts as root with exit 0, empty stderr and 33 of 33 entries, identical content (diff -r) and the modes kept;rm -rfthen removes it. The same operations work in a named volume, a bind mount and/tmp; uid 1000 gets Permission denied for each;docker cpafter the lost-file sequence above now shows the files; a two-stepdocker buildthat adds to and removes from a 0555 directory of the previous layer works.Found on the way, not changed here
ENOENTon 0.6.10 (ln /v/a /v/b), whatever the modes:linkatreadsov_leaf.layer, which means nothing for a mount's leaf. The new test makes hard links in the container layer only.docker commitof a container with nested 0555 directories fails,docker cpof a tree with a 000 directory is truncated, anddocker rmiof an image with a non-empty 0555 directory removesimage.jsonbut not the rootfs, after whichdocker imagesanswers "Host operation failed". Several paths useos.RemoveAlland plainos.MkdirwhereremoveTreeexists for this.The
GOFLAGS=-modcacherwworkaround in the app repo's CI stays until a release carries this.