commit 4debd4338ce2c27a0dd6127ce5736ca56538214c (tree)
parent 78549d1e109a922e63b52b3d4a445217ca997c9c
Author: Andrew Kelley <andrew@ziglang.org>
Date: Sun, 14 Jan 2024 11:26:25 -0800
Merge pull request #18547 from ziglang/gh-fork-dump-fchmod-fixes
Add `fchmodat` fallback on Linux when `flags` is nonzero.
Diffstat:
6 files changed, 207 insertions(+), 22 deletions(-)
diff --git 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
@@ -348,17 +348,61 @@ 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);
+
+/// 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);
+ // 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.
+ 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,
.INTR => continue,
@@ -368,9 +412,108 @@ 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),
+ }
+ }
+}
+
+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`
+ // 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,
else => |err| return unexpectedErrno(err),
diff --git 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`.
diff --git 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,
};
diff --git 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);
}
diff --git 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,
+ };
}
}