Linux: Emulate fchmodat with AT_SYMLINK_NOFOLLOW using O_PATH [BZ #14578]
authorFlorian Weimer <fweimer@redhat.com>
Wed, 22 Jan 2020 18:01:20 +0000 (19:01 +0100)
committerFlorian Weimer <fweimer@redhat.com>
Wed, 12 Feb 2020 07:43:59 +0000 (08:43 +0100)
/proc/self/fd files are special and chmod on O_PATH descriptors
in that directory operates on the symbolic link itself (like lchmod).

sysdeps/unix/sysv/linux/fchmodat.c

index c41ebb290dab0075e54a04e98b916da811811350..719053b333fbad85d42bc5489071f9f32d1d4d8c 100644 (file)
 
 #include <errno.h>
 #include <fcntl.h>
-#include <stddef.h>
+#include <not-cancel.h>
 #include <stdio.h>
-#include <string.h>
-#include <unistd.h>
+#include <sys/stat.h>
 #include <sys/types.h>
-#include <alloca.h>
 #include <sysdep.h>
+#include <unistd.h>
 
 int
 fchmodat (int fd, const char *file, mode_t mode, int flag)
 {
-  if (flag & ~AT_SYMLINK_NOFOLLOW)
+  if (flag == 0)
+    return INLINE_SYSCALL (fchmodat, 3, fd, file, mode);
+  else if (flag != AT_SYMLINK_NOFOLLOW)
     return INLINE_SYSCALL_ERROR_RETURN_VALUE (EINVAL);
-#ifndef __NR_lchmod            /* Linux so far has no lchmod syscall.  */
-  if (flag & AT_SYMLINK_NOFOLLOW)
-    return INLINE_SYSCALL_ERROR_RETURN_VALUE (ENOTSUP);
-#endif
+  else
+    {
+      /* The kernel system call does not have a mode argument.
+        However, we can create an O_PATH descriptor and change that
+        via /proc (which does not resolve symbolic links).  */
+
+      int pathfd = __openat_nocancel (fd, file,
+                                     O_PATH | O_NOFOLLOW | O_CLOEXEC);
+      if (pathfd < 0)
+       /* This may report errors such as ENFILE and EMFILE.  The
+          caller can treat them as temporary if necessary.  */
+       return pathfd;
+
+      char buf[32];
+      if (__snprintf (buf, sizeof (buf), "/proc/self/fd/%d", pathfd) < 0)
+       {
+         /* This also may report strange error codes to the caller
+            (although snprintf really should not fail).  */
+         __close_nocancel (pathfd);
+         return -1;
+       }
 
-  return INLINE_SYSCALL (fchmodat, 3, fd, file, mode);
+      /* This operates directly on the symbolic link if it is one.
+        /proc/self/fd files look like symbolic links, but they are
+        not.  (fchmod and fchmodat do not work on O_PATH descriptors,
+        similar to fstat before Linux 3.6.)  */
+      int ret = __chmod (buf, mode);
+      if (ret != 0)
+       {
+         if (errno == ENOENT)
+           /* /proc has not been mounted.  Without /proc, there is no
+              way to upgrade the O_PATH descriptor to a full
+              descriptor.  It is also not possible to re-open the
+              file without O_PATH because the file name may refer to
+              another file, and opening that without O_PATH may have
+              side effects (such as blocking, device rewinding, or
+              releasing POSIX locks).  */
+           __set_errno (EOPNOTSUPP);
+       }
+      __close_nocancel (pathfd);
+      return ret;
+    }
 }
 libc_hidden_def (fchmodat)