From bc69d62669fac591bda4c91d695d32cf2b78f34c Mon Sep 17 00:00:00 2001 From: Stephen Gregoratto Date: Sat, 4 Nov 2023 17:06:16 +1100 Subject: [PATCH] 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. --- lib/std/os.zig | 143 +++++++++++++++++++++++++++++++++++++++++++++- src/link/Wasm.zig | 5 +- 2 files changed, 144 insertions(+), 4 deletions(-) diff --git a/lib/std/os.zig b/lib/std/os.zig index f07f30513e..9e0bc8b39b 100644 --- a/lib/std/os.zig +++ b/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,7 +409,103 @@ 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, diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 6f20e86bdc..a0cf23579b 100644 --- a/src/link/Wasm.zig +++ b/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, + }; } }