]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
fs-util: prefer glibc's fchmodat() if possible
authorLuca Boccassi <luca.boccassi@gmail.com>
Tue, 23 Sep 2025 22:13:23 +0000 (23:13 +0100)
committerLennart Poettering <lennart@poettering.net>
Wed, 24 Sep 2025 06:48:42 +0000 (08:48 +0200)
Since v2.39 glibc's fchmodat() will call into the kernel's fchmodat2()
if flags are passed:

https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=65341f7bbea824d2ff9d37db15d8be162df42bd3;hp=c52c2c32db15aba8bbe1a0b4d3235f97d9c1a525

On older versions, if the flag is anything other than AT_SYMLINK_NOFOLLOW,
it returns EINVAL, so we can detect it and call the kernel syscall directly
ourselves.

Using the glibc wrappers when possible is prefereable so that programs
like fakeroot can intercept its calls and redirect them.

Follow-up for adecfb3bc0be0def49433277fcad5333893756cc

src/basic/fs-util.c

index a12cfd33c4f822b5bacbfe99f8340935ac6d1659..8cbe29fc0f0876d8a914a05e735f7e1a0b8f5b58 100644 (file)
@@ -288,15 +288,20 @@ int fchmod_opath(int fd, mode_t m) {
          * - fchmod(2) only operates on open files (i. e., fds with an open file description);
          * - fchmodat(2) does not have a flag arg like fchownat(2) does, so no way to pass AT_EMPTY_PATH;
          *   + it should not be confused with the libc fchmodat(3) interface, which adds 4th flag argument,
-         *     but does not support AT_EMPTY_PATH (only supports AT_SYMLINK_NOFOLLOW);
+         *     and supports AT_EMPTY_PATH since v2.39 (previously only supported AT_SYMLINK_NOFOLLOW). So if
+         *     the kernel has fchmodat2(2), since v2.39 glibc will call into it directly. If the kernel
+         *     doesn't, or glibc is older than v2.39, glibc's internal fallback will return EINVAL if
+         *     AT_EMPTY_PATH is passed.
          * - fchmodat2(2) supports all the AT_* flags, but is still very recent.
          *
-         * We try to use fchmodat2(), and, if it is not supported, resort
-         * to the /proc/self/fd dance. */
+         * We try to use fchmodat(3) first, and on EINVAL fall back to fchmodat2(), and, if that is also not
+         * supported, resort to the /proc/self/fd dance. */
 
         assert(fd >= 0);
 
-        if (fchmodat2(fd, "", m, AT_EMPTY_PATH) >= 0)
+        if (fchmodat(fd, "", m, AT_EMPTY_PATH) >= 0)
+                return 0;
+        if (errno == EINVAL && fchmodat2(fd, "", m, AT_EMPTY_PATH) >= 0) /* glibc too old? */
                 return 0;
         if (!IN_SET(errno, ENOSYS, EPERM)) /* Some container managers block unknown syscalls with EPERM */
                 return -errno;