From 26db31f6f6a5e034333ff81f2352901201572cba Mon Sep 17 00:00:00 2001 From: Stephen Gregoratto Date: Wed, 1 Nov 2023 23:30:17 +1100 Subject: [PATCH 1/5] Add `fchmodat2` to the Linux syscall list This syscall was added to simplify the the libc implementations of fchmodat, as the original syscall does not take a `flags` argument. Another syscall, `map_shadow_stack`, was also added for x86_64. --- lib/std/os/linux/syscalls.zig | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/std/os/linux/syscalls.zig b/lib/std/os/linux/syscalls.zig index b919c354c2..b584f2bd17 100644 --- a/lib/std/os/linux/syscalls.zig +++ b/lib/std/os/linux/syscalls.zig @@ -443,6 +443,7 @@ pub const X86 = enum(usize) { futex_waitv = 449, set_mempolicy_home_node = 450, cachestat = 451, + fchmodat2 = 452, }; pub const X64 = enum(usize) { @@ -809,6 +810,8 @@ pub const X64 = enum(usize) { futex_waitv = 449, set_mempolicy_home_node = 450, cachestat = 451, + fchmodat2 = 452, + map_shadow_stack = 453, }; pub const Arm = enum(usize) { @@ -1218,6 +1221,7 @@ pub const Arm = enum(usize) { futex_waitv = 449, set_mempolicy_home_node = 450, cachestat = 451, + fchmodat2 = 452, breakpoint = arm_base + 1, cacheflush = arm_base + 2, @@ -1611,6 +1615,7 @@ pub const Sparc64 = enum(usize) { futex_waitv = 449, set_mempolicy_home_node = 450, cachestat = 451, + fchmodat2 = 452, }; pub const Mips = enum(usize) { @@ -2035,6 +2040,7 @@ pub const Mips = enum(usize) { futex_waitv = Linux + 449, set_mempolicy_home_node = Linux + 450, cachestat = Linux + 451, + fchmodat2 = Linux + 452, }; pub const Mips64 = enum(usize) { @@ -2395,6 +2401,7 @@ pub const Mips64 = enum(usize) { futex_waitv = Linux + 449, set_mempolicy_home_node = Linux + 450, cachestat = Linux + 451, + fchmodat2 = Linux + 452, }; pub const PowerPC = enum(usize) { @@ -2830,6 +2837,7 @@ pub const PowerPC = enum(usize) { futex_waitv = 449, set_mempolicy_home_node = 450, cachestat = 451, + fchmodat2 = 452, }; pub const PowerPC64 = enum(usize) { @@ -3237,6 +3245,7 @@ pub const PowerPC64 = enum(usize) { futex_waitv = 449, set_mempolicy_home_node = 450, cachestat = 451, + fchmodat2 = 452, }; pub const Arm64 = enum(usize) { @@ -3547,6 +3556,7 @@ pub const Arm64 = enum(usize) { futex_waitv = 449, set_mempolicy_home_node = 450, cachestat = 451, + fchmodat2 = 452, }; pub const RiscV64 = enum(usize) { @@ -3858,6 +3868,7 @@ pub const RiscV64 = enum(usize) { futex_waitv = 449, set_mempolicy_home_node = 450, cachestat = 451, + fchmodat2 = 452, riscv_flush_icache = arch_specific_syscall + 15, }; From cf6751ae5510964f0d349e6ea044d8f618ba6e33 Mon Sep 17 00:00:00 2001 From: Stephen Gregoratto Date: Sat, 4 Nov 2023 17:05:57 +1100 Subject: [PATCH 2/5] Add fchmodat2 bits to os/linux.zig --- lib/std/os/linux.zig | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 8928e1fdf2..d6e539e98c 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -796,13 +796,7 @@ pub fn chmod(path: [*:0]const u8, mode: mode_t) usize { if (@hasField(SYS, "chmod")) { return syscall2(.chmod, @intFromPtr(path), mode); } else { - return syscall4( - .fchmodat, - @as(usize, @bitCast(@as(isize, AT.FDCWD))), - @intFromPtr(path), - mode, - 0, - ); + return fchmodat(AT.FDCWD, path, mode, 0); } } @@ -814,8 +808,12 @@ pub fn fchown(fd: i32, owner: uid_t, group: gid_t) usize { } } -pub fn fchmodat(fd: i32, path: [*:0]const u8, mode: mode_t, flags: u32) usize { - return syscall4(.fchmodat, @as(usize, @bitCast(@as(isize, fd))), @intFromPtr(path), mode, flags); +pub fn fchmodat(fd: i32, path: [*:0]const u8, mode: mode_t, _: u32) usize { + return syscall3(.fchmodat, @bitCast(@as(isize, fd)), @intFromPtr(path), mode); +} + +pub fn fchmodat2(fd: i32, path: [*:0]const u8, mode: mode_t, flags: u32) usize { + return syscall4(.fchmodat2, @bitCast(@as(isize, fd)), @intFromPtr(path), mode, flags); } /// Can only be called on 32 bit systems. For 64 bit see `lseek`. From bc69d62669fac591bda4c91d695d32cf2b78f34c Mon Sep 17 00:00:00 2001 From: Stephen Gregoratto Date: Sat, 4 Nov 2023 17:06:16 +1100 Subject: [PATCH 3/5] 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, + }; } } From 01cc904fc7c10bd58a7eaf17e63126f9cc63922c Mon Sep 17 00:00:00 2001 From: Stephen Gregoratto Date: Sun, 5 Nov 2023 05:30:26 +1100 Subject: [PATCH 4/5] Rework fchmodat tests Based on the Linux kernel fchmodat2 test --- lib/std/os/test.zig | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 490e123506..c74c0aefc4 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -1217,16 +1217,46 @@ test "pwrite with empty buffer" { _ = try os.pwrite(file.handle, bytes, 0); } +fn expectMode(dir: os.fd_t, file: []const u8, mode: os.mode_t) !void { + const st = try os.fstatat(dir, file, os.AT.SYMLINK_NOFOLLOW); + try expectEqual(mode, st.mode & 0b111_111_111); +} + test "fchmodat smoke test" { if (!std.fs.has_executable_bit) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); - try expectError(error.FileNotFound, os.fchmodat(tmp.dir.fd, "foo.txt", 0o666, 0)); - const fd = try os.openat(tmp.dir.fd, "foo.txt", os.O.RDWR | os.O.CREAT | os.O.EXCL, 0o666); + try expectError(error.FileNotFound, os.fchmodat(tmp.dir.fd, "regfile", 0o666, 0)); + const fd = try os.openat( + tmp.dir.fd, + "regfile", + os.O.WRONLY | os.O.CREAT | os.O.EXCL | os.O.TRUNC, + 0o644, + ); os.close(fd); - try os.fchmodat(tmp.dir.fd, "foo.txt", 0o755, 0); - const st = try os.fstatat(tmp.dir.fd, "foo.txt", 0); - try expectEqual(@as(os.mode_t, 0o755), st.mode & 0b111_111_111); + try os.symlinkat("regfile", tmp.dir.fd, "symlink"); + const sym_mode = blk: { + const st = try os.fstatat(tmp.dir.fd, "symlink", os.AT.SYMLINK_NOFOLLOW); + break :blk st.mode & 0b111_111_111; + }; + + try os.fchmodat(tmp.dir.fd, "regfile", 0o640, 0); + try expectMode(tmp.dir.fd, "regfile", 0o640); + try os.fchmodat(tmp.dir.fd, "regfile", 0o600, os.AT.SYMLINK_NOFOLLOW); + try expectMode(tmp.dir.fd, "regfile", 0o600); + + try os.fchmodat(tmp.dir.fd, "symlink", 0o640, 0); + try expectMode(tmp.dir.fd, "regfile", 0o640); + try expectMode(tmp.dir.fd, "symlink", sym_mode); + + var test_link = true; + os.fchmodat(tmp.dir.fd, "symlink", 0o600, os.AT.SYMLINK_NOFOLLOW) catch |err| switch (err) { + error.OperationNotSupported => test_link = false, + else => |e| return e, + }; + if (test_link) + try expectMode(tmp.dir.fd, "symlink", 0o600); + try expectMode(tmp.dir.fd, "regfile", 0o640); } From 09074d7cd7ab76ebf87cb303825ce53834bcc532 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 13 Jan 2024 23:56:32 -0700 Subject: [PATCH 5/5] std.os: proper use of inline Uses `inline` only to forward the comptime-ness of the flags parameter to function selection. Also fixes doc comments in std.c.versionCheck. --- lib/std/c.zig | 8 ++++---- lib/std/os.zig | 32 +++++++++++++++++++------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/lib/std/c.zig b/lib/std/c.zig index 983c38108a..e494931114 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -5,10 +5,10 @@ const page_size = std.mem.page_size; const iovec = std.os.iovec; const iovec_const = std.os.iovec_const; -/// If not linking libc, returns struct{pub const ok = false;} -/// If linking musl libc, returns struct{pub const ok = true;} -/// If linking gnu libc (glibc), the `ok` value will be true if the target -/// version is greater than or equal to `glibc_version`. +/// If not linking libc, returns false. +/// If linking musl libc, returns true. +/// If linking gnu libc (glibc), returns true if the target version is greater +/// than or equal to `glibc_version`. /// If linking a libc other than these, returns `false`. pub inline fn versionCheck(comptime glibc_version: std.SemanticVersion) bool { return comptime blk: { diff --git a/lib/std/os.zig b/lib/std/os.zig index 9e0bc8b39b..5a57f80356 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -351,7 +351,6 @@ 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 @@ -359,23 +358,15 @@ const FChmodAtError = FChmodError || error{ /// /// 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, }; 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 @@ -394,11 +385,23 @@ inline fn skipFchmodatFallback(flags: u32) bool { 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); - // 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 skip_fchmodat_fallback = builtin.os.tag != .linux or + std.c.versionCheck(.{ .major = 2, .minor = 32, .patch = 0 }) or + flags == 0; + + // This function is marked inline so that when flags is comptime-known, + // skip_fchmodat_fallback will be comptime-known true. + if (skip_fchmodat_fallback) + return fchmodat1(dirfd, path, mode, flags); + + return fchmodat2(dirfd, path, mode, flags); +} + +fn fchmodat1(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void { + const path_c = try toPosixPath(path); + while (true) { const res = system.fchmodat(dirfd, &path_c, mode, flags); switch (system.getErrno(res)) { .SUCCESS => return, @@ -421,8 +424,11 @@ pub inline fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) else => |err| return unexpectedErrno(err), } } +} - const use_fchmodat2 = (comptime builtin.os.isAtLeast(.linux, .{ .major = 6, .minor = 6, .patch = 0 }) orelse false) and +fn fchmodat2(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void { + const path_c = try toPosixPath(path); + const use_fchmodat2 = (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`