Commit bc69d62669

Stephen Gregoratto <dev@sgregoratto.me>
2023-11-04 07:06:16
Linux: Add fchmodat fallback when `flags` is nonzero
The check for determining whether to use the fallback code has been moved into an inline function as per Andrew's comments in #17954.
1 parent cf6751a
Changed files (2)
lib
std
src
lib/std/os.zig
@@ -348,17 +348,58 @@ pub fn fchmod(fd: fd_t, mode: mode_t) FChmodError!void {
 }
 
 const FChmodAtError = FChmodError || error{
+    /// A component of `path` exceeded `NAME_MAX`, or the entire path exceeded
+    /// `PATH_MAX`.
     NameTooLong,
+
+    /// `path` resolves to a symbolic link, and `AT.SYMLINK_NOFOLLOW` was set
+    /// in `flags`. This error only occurs on Linux, where changing the mode of
+    /// a symbolic link has no meaning and can cause undefined behaviour on
+    /// certain filesystems.
+    ///
+    /// The procfs fallback was used but procfs was not mounted.
+    OperationNotSupported,
+
+    /// The procfs fallback was used but the process exceeded its open file
+    /// limit.
+    ProcessFdQuotaExceeded,
+
+    /// The procfs fallback was used but the system exceeded it open file limit.
+    SystemFdQuotaExceeded,
 };
 
-pub fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void {
+var has_fchmodat2_syscall = std.atomic.Value(bool).init(true);
+
+inline fn skipFchmodatFallback(flags: u32) bool {
+    return builtin.os.tag != .linux or
+        flags == 0 or
+        std.c.versionCheck(std.SemanticVersion{ .major = 2, .minor = 32, .patch = 0 }).ok;
+}
+
+/// Changes the `mode` of `path` relative to the directory referred to by
+/// `dirfd`. The process must have the correct privileges in order to do this
+/// successfully, or must have the effective user ID matching the owner of the
+/// file.
+///
+/// On Linux the `fchmodat2` syscall will be used if available, otherwise a
+/// workaround using procfs will be employed. Changing the mode of a symbolic
+/// link with `AT.SYMLINK_NOFOLLOW` set will also return
+/// `OperationNotSupported`, as:
+///
+///  1. Permissions on the link are ignored when resolving its target.
+///  2. This operation has been known to invoke undefined behaviour across
+///     different filesystems[1].
+///
+/// [1]: https://sourceware.org/legacy-ml/libc-alpha/2020-02/msg00467.html.
+pub inline fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void {
     if (!std.fs.has_executable_bit) @compileError("fchmodat unsupported by target OS");
 
     const path_c = try toPosixPath(path);
 
-    while (true) {
+    // No special handling for linux is needed if we can use the libc fallback
+    // or `flags` is empty. Glibc only added the fallback in 2.32.
+    while (skipFchmodatFallback(flags)) {
         const res = system.fchmodat(dirfd, &path_c, mode, flags);
-
         switch (system.getErrno(res)) {
             .SUCCESS => return,
             .INTR => continue,
@@ -368,8 +409,104 @@ pub fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodA
             .ACCES => return error.AccessDenied,
             .IO => return error.InputOutput,
             .LOOP => return error.SymLinkLoop,
+            .MFILE => return error.ProcessFdQuotaExceeded,
+            .NAMETOOLONG => return error.NameTooLong,
+            .NFILE => return error.SystemFdQuotaExceeded,
+            .NOENT => return error.FileNotFound,
+            .NOTDIR => return error.FileNotFound,
+            .NOMEM => return error.SystemResources,
+            .OPNOTSUPP => return error.OperationNotSupported,
+            .PERM => return error.AccessDenied,
+            .ROFS => return error.ReadOnlyFileSystem,
+            else => |err| return unexpectedErrno(err),
+        }
+    }
+
+    const use_fchmodat2 = (comptime builtin.os.isAtLeast(.linux, .{ .major = 6, .minor = 6, .patch = 0 }) orelse false) and
+        has_fchmodat2_syscall.load(.Monotonic);
+    while (use_fchmodat2) {
+        // Later on this should be changed to `system.fchmodat2`
+        // when the musl/glibc add a wrapper.
+        const res = linux.fchmodat2(dirfd, &path_c, mode, flags);
+        switch (linux.getErrno(res)) {
+            .SUCCESS => return,
+            .INTR => continue,
+            .BADF => unreachable,
+            .FAULT => unreachable,
+            .INVAL => unreachable,
+            .ACCES => return error.AccessDenied,
+            .IO => return error.InputOutput,
+            .LOOP => return error.SymLinkLoop,
+            .NOENT => return error.FileNotFound,
+            .NOMEM => return error.SystemResources,
+            .NOTDIR => return error.FileNotFound,
+            .OPNOTSUPP => return error.OperationNotSupported,
+            .PERM => return error.AccessDenied,
+            .ROFS => return error.ReadOnlyFileSystem,
+
+            .NOSYS => { // Use fallback.
+                has_fchmodat2_syscall.store(false, .Monotonic);
+                break;
+            },
+            else => |err| return unexpectedErrno(err),
+        }
+    }
+
+    // Fallback to changing permissions using procfs:
+    //
+    // 1. Open `path` as an `O.PATH` descriptor.
+    // 2. Stat the fd and check if it isn't a symbolic link.
+    // 3. Generate the procfs reference to the fd via `/proc/self/fd/{fd}`.
+    // 4. Pass the procfs path to `chmod` with the `mode`.
+    var pathfd: fd_t = undefined;
+    while (true) {
+        const rc = system.openat(dirfd, &path_c, O.PATH | O.NOFOLLOW | O.CLOEXEC, @as(mode_t, 0));
+        switch (system.getErrno(rc)) {
+            .SUCCESS => {
+                pathfd = @as(fd_t, @intCast(rc));
+                break;
+            },
+            .INTR => continue,
+            .FAULT => unreachable,
+            .INVAL => unreachable,
+            .ACCES => return error.AccessDenied,
+            .PERM => return error.AccessDenied,
+            .LOOP => return error.SymLinkLoop,
+            .MFILE => return error.ProcessFdQuotaExceeded,
+            .NAMETOOLONG => return error.NameTooLong,
+            .NFILE => return error.SystemFdQuotaExceeded,
             .NOENT => return error.FileNotFound,
             .NOMEM => return error.SystemResources,
+            else => |err| return unexpectedErrno(err),
+        }
+    }
+    defer close(pathfd);
+
+    const stat = fstatatZ(pathfd, "", AT.EMPTY_PATH) catch |err| switch (err) {
+        error.NameTooLong => unreachable,
+        error.FileNotFound => unreachable,
+        else => |e| return e,
+    };
+    if ((stat.mode & S.IFMT) == S.IFLNK)
+        return error.OperationNotSupported;
+
+    var procfs_buf: ["/proc/self/fd/-2147483648\x00".len]u8 = undefined;
+    const proc_path = std.fmt.bufPrintZ(procfs_buf[0..], "/proc/self/fd/{d}", .{pathfd}) catch unreachable;
+    while (true) {
+        const res = system.chmod(proc_path, mode);
+        switch (system.getErrno(res)) {
+            // Getting NOENT here means that procfs isn't mounted.
+            .NOENT => return error.OperationNotSupported,
+
+            .SUCCESS => return,
+            .INTR => continue,
+            .BADF => unreachable,
+            .FAULT => unreachable,
+            .INVAL => unreachable,
+            .ACCES => return error.AccessDenied,
+            .IO => return error.InputOutput,
+            .LOOP => return error.SymLinkLoop,
+            .NOMEM => return error.SystemResources,
             .NOTDIR => return error.FileNotFound,
             .PERM => return error.AccessDenied,
             .ROFS => return error.ReadOnlyFileSystem,
src/link/Wasm.zig
@@ -4917,7 +4917,10 @@ fn linkWithLLD(wasm: *Wasm, arena: Allocator, prog_node: *std.Progress.Node) !vo
             // report a nice error here with the file path if it fails instead of
             // just returning the error code.
             // chmod does not interact with umask, so we use a conservative -rwxr--r-- here.
-            try std.os.fchmodat(fs.cwd().fd, full_out_path, 0o744, 0);
+            std.os.fchmodat(fs.cwd().fd, full_out_path, 0o744, 0) catch |err| switch (err) {
+                error.OperationNotSupported => unreachable, // Not a symlink.
+                else => |e| return e,
+            };
         }
     }