]> 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)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Fri, 10 Oct 2025 08:36:47 +0000 (10:36 +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

(cherry picked from commit 696b1263dc78858f96345a366933c66d53ae4899)

src/basic/fs-util.c

index 9fb6f5d0b82d8eb884d585c134c4e46c8ef4f662..0c84f42ed6ef5f9d49167df5284fe3fb19196fb9 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;