Merge pull request #18547 from ziglang/gh-fork-dump-fchmod-fixes

Add `fchmodat` fallback on Linux when `flags` is nonzero.
This commit is contained in:
Andrew Kelley 2024-01-14 11:26:25 -08:00 committed by GitHub
commit 4debd4338c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 207 additions and 22 deletions

View File

@ -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: {

View File

@ -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,7 +412,106 @@ 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,

View File

@ -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`.

View File

@ -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,
};

View File

@ -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);
}

View File

@ -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,
};
}
}