Support AT_EMPTY_PATH in fchownat/fchmodat2#194
Conversation
fd10b66 to
63b7930
Compare
jserv
left a comment
There was a problem hiding this comment.
Append Close #128 at the end of git commit messages.
ca1fea1 to
153a72d
Compare
jserv
left a comment
There was a problem hiding this comment.
Automated review of the AT_EMPTY_PATH handling. The common thread: the new empty-path branch returns before the normal path's reject_unsupported_fuse_path_op() / interception layer, so it treats some fd classes differently from every other *at op. errno preservation, translate_at_flags dropping AT_EMPTY_PATH, the O_PATH handling, sysroot parity, leaks, and the test-matrix count math all check out.
| /* AT_EMPTY_PATH with an empty path chmods dirfd itself; see the identical | ||
| * branch in sys_fchownat above for the O_PATH/AT_FDCWD rationale. | ||
| */ | ||
| if ((flags & LINUX_AT_EMPTY_PATH) && path[0] == '\0') { |
There was a problem hiding this comment.
Empty-path branch bypasses FUSE interception (mirror at :2519 in fchownat). It returns before path_translate_at + reject_unsupported_fuse_path_op, so it diverges from the named path in two ways:
- A
dirfdinside a FUSE mount hashost_fd == -1(fuse.c:1893), sohost_dirfd_ref_openreturns EBADF here, whereas the named path returns ENOSYS. - If cwd is inside a FUSE mount, the AT_FDCWD sub-case calls host
fchmodat(".", ...)directly, mutating the host backing path instead of returning ENOSYS.
Suggested fix: snapshot dirfd first and return -LINUX_ENOSYS for FD_FUSE_* to match reject_unsupported_fuse_path_op.
There was a problem hiding this comment.
Fixed. dirfd is now snapshotted (fd_snapshot) before host_dirfd_ref_open: FD_FUSE_DEV/FD_FUSE_FILE/FD_FUSE_DIR return -ENOSYS, matching reject_unsupported_fuse_path_op's behavior for the named-path case. For the AT_FDCWD sub-case, fuse_resolve_at_path(LINUX_AT_FDCWD, ".", ...) now checks whether cwd is inside a FUSE mount before touching the host path (same helper dirfd_guest_base_path in path.c already uses for this). Applied identically to the mirrored fchownat branch.
| } | ||
|
|
||
| host_fd_ref_t ref; | ||
| if (host_dirfd_ref_open(dirfd, &ref) < 0) |
There was a problem hiding this comment.
Proc-intercept divergence from the mirrored stat_at_path (mirror at :2533 in fchownat). stat_at_path checks snap.type == FD_PATH && snap.proc_path[0] and routes to proc_intercept_stat; this branch skips that and fchmod/fchowns the backing host temp fd directly.
A guest that opens a synthesized /proc path O_PATH then calls fchownat(fd, "", ..., AT_EMPTY_PATH) silently mutates elfuse's internal temp file and reports success (Linux would return EPERM). chown_result's EPERM-to-fakeroot-success mapping hides it further.
Suggested fix: snapshot the fd before host_dirfd_ref_open; for FD_PATH with proc_path set, return EPERM instead of touching the backing file.
There was a problem hiding this comment.
Fixed. Same fd_snapshot up front: FD_PATH with proc_path set now returns -EPERM instead of touching the backing file, mirroring stat_at_path's check.
Verified the divergence and the fix with a standalone probe: pre-fix, fchmodat2(procfd_of_/proc/self/fd, "", 0700, AT_EMPTY_PATH) returned 0 and silently mutated elfuse's synthesized backing directory; post-fix it returns -EPERM.
I did try adding this as a permanent regression test, but pulled it back out — under the qemu lane (real Alpine kernel), root fchownat on a real procfs dentry via the same call actually succeeds (simple_setattr-style, no fs-specific restriction), so an EPERM assertion there would be asserting elfuse-only behavior against a real kernel and breaks qemu-aarch64 parity testing. Since this specific check protects an elfuse-internal invariant (the temp-file backing) rather than Linux-compatible behavior, I decided the code fix + the manual probe was better than a flaky/misleading cross-platform assertion. Happy to add it gated some other way if you'd prefer.
| if ((flags & LINUX_AT_EMPTY_PATH) && path[0] == '\0') { | ||
| int mac_flags = translate_at_flags(flags); | ||
| if (dirfd == LINUX_AT_FDCWD) { | ||
| int host_rc = fchownat(AT_FDCWD, ".", owner, group, mac_flags); |
There was a problem hiding this comment.
TOCTOU on the AT_FDCWD overlay update. fchownat(AT_FDCWD, ".") here and the follow-up fstatat(AT_FDCWD, ".") at :2526 resolve "." twice. A concurrent chdir() in another guest thread between them records the requested uid/gid overlay against a different directory's dev/ino. The dirfd sub-case is immune because it fchown+fstat the same fd.
Suggested fix: open "." once and fchown(fd)+fstat(fd) on that single descriptor, like the dirfd branch.
There was a problem hiding this comment.
Fixed. The AT_FDCWD branch now opens "." once (open(".", O_RDONLY)) and does fchown(fd) + fstat(fd) on that single descriptor, same as the dirfd branch, instead of resolving "." twice via fchownat then fstatat.
|
|
||
| static int fchmodat2(int dirfd, const char *path, mode_t mode, int flags) | ||
| { | ||
| return (int) syscall(SYS_fchmodat2, dirfd, path, (long) mode, (long) flags); |
There was a problem hiding this comment.
No ENOSYS skip guard for fchmodat2. This issues raw fchmodat2(2) (SYS 452, Linux 6.6+) with no fallback. On a qemu fixture running a <6.6 kernel it fails hard rather than skipping. Worth confirming the fixture kernel is >= 6.6 (the count bump to 217 assumes it).
There was a problem hiding this comment.
Fixed. Added an fchmodat2_supported() probe at the start of main() (via the same raw syscall, against the already-created fixture file with a no-op mode); if it returns ENOSYS the test now prints a skip message and returns 0 instead of running the rest and hard-failing.
For the record, the qemu lane's fixture is Alpine 3.21's linux-virt kernel, which is currently 6.12.x (>= 6.6, so fchmodat2 is present and the 217 count holds) — but it's fetched live and unpinned in-repo, so this guards against a future/older kernel there without needing to touch the count.
fchownat() and fchmodat2() only accepted AT_SYMLINK_NOFOLLOW, so *at(fd, "", ..., AT_EMPTY_PATH) returned EINVAL instead of operating on fd itself. That is the only way to chown or chmod an O_PATH descriptor (plain fchown()/fchmod() reject FD_PATH with EBADF, matching Linux), so unlike the plain EBADF guard this flag combination needs real handling, not just acceptance. An empty path with AT_EMPTY_PATH now operates on dirfd directly, or the current directory when dirfd is AT_FDCWD, mirroring the identical AT_EMPTY_PATH branch already in fs-stat.c's stat_at_path. Neither AT_EMPTY_PATH branch went through path_translate_at, so both missed the FUSE and /proc interception every other *at() path gets: a FUSE-backed dirfd returned EBADF instead of ENOSYS, cwd inside a FUSE mount was chmod/chowned on the host directly, and an O_PATH fd on a synthesized /proc directory silently mutated elfuse's internal backing file instead of failing. fchownat's AT_FDCWD case also resolved "." twice (once to chown, once to stat), racing a concurrent chdir() on another thread of the same guest. All four are fixed by checking the dirfd's type up front and reusing a single open fd for the AT_FDCWD chown+stat pair. Close sysprog21#128
153a72d to
74513b9
Compare
fchownat() and fchmodat2() only accepted AT_SYMLINK_NOFOLLOW, so
*at(fd, "", ..., AT_EMPTY_PATH) returned EINVAL instead of operating on
fd itself. That is the only way to chown or chmod an O_PATH descriptor
(plain fchown()/fchmod() reject FD_PATH with EBADF, matching Linux), so
unlike the plain EBADF guard this flag combination needs real handling,
not just acceptance.
An empty path with AT_EMPTY_PATH now operates on dirfd directly, or the
current directory when dirfd is AT_FDCWD, mirroring the identical
AT_EMPTY_PATH branch already in fs-stat.c's stat_at_path.
Close #128
Summary by cubic
Adds full
AT_EMPTY_PATHsupport to fchownat and fchmodat2 so an empty path targets the dirfd (or cwd forAT_FDCWD), enabling chown/chmod viaO_PATHand matching Linux/stat_at_path. Also fixes missing FUSE/proc interception and a cwd race in fchownat.AT_EMPTY_PATHnow chowns dirfd or "." (AT_FDCWD); returnsENOSYSfor FUSE dirfds andEPERMfor synthesized/proc; avoids a race by opening "." once for chown+stat; keepfchown(O_PATH)returningEBADF.AT_EMPTY_PATHnow chmods dirfd or "." (AT_FDCWD); returnsENOSYSfor FUSE dirfds andEPERMfor synthesized/proc; keepfchmod(O_PATH)returningEBADF.test-fchownat-empty-pathandtest-fchmodat-empty-path(uses rawfchmodat2syscall; skips ifENOSYS); wire intotests/test-matrix.shand update baselines.Written for commit 74513b9. Summary will update on new commits.