From bb5006d7287607e49b71d25d263ebcc3d5845c60 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 2 Mar 2023 18:32:15 -0700 Subject: [PATCH] std: add fchmodat Also add `std.fs.has_executable_bit` for doing conditional compilation. This adds the linux syscalls for chmod and fchmodat, as well as the extern libc function declarations. Only `fchmodat` is added to `std.os`, and it is not yet added to std.fs. --- lib/std/c.zig | 2 ++ lib/std/fs.zig | 5 +++ lib/std/os.zig | 35 +++++++++++++++++++-- lib/std/os/linux.zig | 18 +++++++++++ lib/std/os/test.zig | 72 ++++++++++++++++++++++++++------------------ 5 files changed, 100 insertions(+), 32 deletions(-) diff --git a/lib/std/c.zig b/lib/std/c.zig index 1334b2f2c1..9fc3b1d57e 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -171,7 +171,9 @@ pub extern "c" fn dup(fd: c.fd_t) c_int; pub extern "c" fn dup2(old_fd: c.fd_t, new_fd: c.fd_t) c_int; pub extern "c" fn readlink(noalias path: [*:0]const u8, noalias buf: [*]u8, bufsize: usize) isize; pub extern "c" fn readlinkat(dirfd: c.fd_t, noalias path: [*:0]const u8, noalias buf: [*]u8, bufsize: usize) isize; +pub extern "c" fn chmod(path: [*:0]const u8, mode: c.mode_t) c_int; pub extern "c" fn fchmod(fd: c.fd_t, mode: c.mode_t) c_int; +pub extern "c" fn fchmodat(fd: c.fd_t, path: [*:0]const u8, mode: c.mode_t, flags: c_uint) c_int; pub extern "c" fn fchown(fd: c.fd_t, owner: c.uid_t, group: c.gid_t) c_int; pub extern "c" fn umask(mode: c.mode_t) c.mode_t; diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 52a93a498f..def4332700 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -11,6 +11,11 @@ const math = std.math; const is_darwin = builtin.os.tag.isDarwin(); +pub const has_executable_bit = switch (builtin.os.tag) { + .windows, .wasi => false, + else => true, +}; + pub const path = @import("fs/path.zig"); pub const File = @import("fs/file.zig").File; pub const wasi = @import("fs/wasi.zig"); diff --git a/lib/std/os.zig b/lib/std/os.zig index bd6719ec8f..fe664302a7 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -302,8 +302,7 @@ pub const FChmodError = error{ /// successfully, or must have the effective user ID matching the owner /// of the file. pub fn fchmod(fd: fd_t, mode: mode_t) FChmodError!void { - if (builtin.os.tag == .windows or builtin.os.tag == .wasi) - @compileError("Unsupported OS"); + if (!std.fs.has_executable_bit) @compileError("fchmod unsupported by target OS"); while (true) { const res = system.fchmod(fd, mode); @@ -311,8 +310,38 @@ pub fn fchmod(fd: fd_t, mode: mode_t) FChmodError!void { switch (system.getErrno(res)) { .SUCCESS => return, .INTR => continue, - .BADF => unreachable, // Can be reached if the fd refers to a non-iterable directory. + .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, + .PERM => return error.AccessDenied, + .ROFS => return error.ReadOnlyFileSystem, + else => |err| return unexpectedErrno(err), + } + } +} +const FChmodAtError = FChmodError || error{ + NameTooLong, +}; + +pub 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) { + const res = system.fchmodat(dirfd, &path_c, mode, flags); + + switch (system.getErrno(res)) { + .SUCCESS => return, + .INTR => continue, + .BADF => unreachable, .FAULT => unreachable, .INVAL => unreachable, .ACCES => return error.AccessDenied, diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index fe2f8404e2..b151e5f235 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -769,6 +769,20 @@ pub fn fchmod(fd: i32, mode: mode_t) usize { return syscall2(.fchmod, @bitCast(usize, @as(isize, fd)), mode); } +pub fn chmod(path: [*:0]const u8, mode: mode_t) usize { + if (@hasField(SYS, "chmod")) { + return syscall2(.chmod, @ptrToInt(path), mode); + } else { + return syscall4( + .fchmodat, + @bitCast(usize, @as(isize, AT.FDCWD)), + @ptrToInt(path), + mode, + 0, + ); + } +} + pub fn fchown(fd: i32, owner: uid_t, group: gid_t) usize { if (@hasField(SYS, "fchown32")) { return syscall3(.fchown32, @bitCast(usize, @as(isize, fd)), owner, group); @@ -777,6 +791,10 @@ 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, @bitCast(usize, @as(isize, fd)), @ptrToInt(path), mode, flags); +} + /// Can only be called on 32 bit systems. For 64 bit see `lseek`. pub fn llseek(fd: i32, offset: u64, result: ?*u64, whence: usize) usize { // NOTE: The offset parameter splitting is independent from the target diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index 177e32f772..7a5fc0de33 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -531,17 +531,17 @@ test "memfd_create" { else => return error.SkipZigTest, } - const fd = std.os.memfd_create("test", 0) catch |err| switch (err) { + const fd = os.memfd_create("test", 0) catch |err| switch (err) { // Related: https://github.com/ziglang/zig/issues/4019 error.SystemOutdated => return error.SkipZigTest, else => |e| return e, }; - defer std.os.close(fd); - try expect((try std.os.write(fd, "test")) == 4); - try std.os.lseek_SET(fd, 0); + defer os.close(fd); + try expect((try os.write(fd, "test")) == 4); + try os.lseek_SET(fd, 0); var buf: [10]u8 = undefined; - const bytes_read = try std.os.read(fd, &buf); + const bytes_read = try os.read(fd, &buf); try expect(bytes_read == 4); try expect(mem.eql(u8, buf[0..4], "test")); } @@ -688,7 +688,7 @@ test "signalfd" { .linux, .solaris => {}, else => return error.SkipZigTest, } - _ = std.os.signalfd; + _ = os.signalfd; } test "sync" { @@ -757,11 +757,11 @@ test "shutdown socket" { if (native_os == .wasi) return error.SkipZigTest; if (native_os == .windows) { - _ = try std.os.windows.WSAStartup(2, 2); + _ = try os.windows.WSAStartup(2, 2); } defer { if (native_os == .windows) { - std.os.windows.WSACleanup() catch unreachable; + os.windows.WSACleanup() catch unreachable; } } const sock = try os.socket(os.AF.INET, os.SOCK.STREAM, 0); @@ -855,13 +855,13 @@ test "dup & dup2" { var file = try tmp.dir.createFile("os_dup_test", .{}); defer file.close(); - var duped = std.fs.File{ .handle = try std.os.dup(file.handle) }; + var duped = std.fs.File{ .handle = try os.dup(file.handle) }; defer duped.close(); try duped.writeAll("dup"); // Tests aren't run in parallel so using the next fd shouldn't be an issue. const new_fd = duped.handle + 1; - try std.os.dup2(file.handle, new_fd); + try os.dup2(file.handle, new_fd); var dup2ed = std.fs.File{ .handle = new_fd }; defer dup2ed.close(); try dup2ed.writeAll("dup2"); @@ -909,46 +909,46 @@ test "POSIX file locking with fcntl" { const fd = file.handle; // Place an exclusive lock on the first byte, and a shared lock on the second byte: - var struct_flock = std.mem.zeroInit(std.os.Flock, .{ .type = std.os.F.WRLCK }); - _ = try std.os.fcntl(fd, std.os.F.SETLK, @ptrToInt(&struct_flock)); + var struct_flock = std.mem.zeroInit(os.Flock, .{ .type = os.F.WRLCK }); + _ = try os.fcntl(fd, os.F.SETLK, @ptrToInt(&struct_flock)); struct_flock.start = 1; - struct_flock.type = std.os.F.RDLCK; - _ = try std.os.fcntl(fd, std.os.F.SETLK, @ptrToInt(&struct_flock)); + struct_flock.type = os.F.RDLCK; + _ = try os.fcntl(fd, os.F.SETLK, @ptrToInt(&struct_flock)); // Check the locks in a child process: - const pid = try std.os.fork(); + const pid = try os.fork(); if (pid == 0) { // child expects be denied the exclusive lock: struct_flock.start = 0; - struct_flock.type = std.os.F.WRLCK; - try expectError(error.Locked, std.os.fcntl(fd, std.os.F.SETLK, @ptrToInt(&struct_flock))); + struct_flock.type = os.F.WRLCK; + try expectError(error.Locked, os.fcntl(fd, os.F.SETLK, @ptrToInt(&struct_flock))); // child expects to get the shared lock: struct_flock.start = 1; - struct_flock.type = std.os.F.RDLCK; - _ = try std.os.fcntl(fd, std.os.F.SETLK, @ptrToInt(&struct_flock)); + struct_flock.type = os.F.RDLCK; + _ = try os.fcntl(fd, os.F.SETLK, @ptrToInt(&struct_flock)); // child waits for the exclusive lock in order to test deadlock: struct_flock.start = 0; - struct_flock.type = std.os.F.WRLCK; - _ = try std.os.fcntl(fd, std.os.F.SETLKW, @ptrToInt(&struct_flock)); + struct_flock.type = os.F.WRLCK; + _ = try os.fcntl(fd, os.F.SETLKW, @ptrToInt(&struct_flock)); // child exits without continuing: - std.os.exit(0); + os.exit(0); } else { // parent waits for child to get shared lock: std.time.sleep(1 * std.time.ns_per_ms); // parent expects deadlock when attempting to upgrade the shared lock to exclusive: struct_flock.start = 1; - struct_flock.type = std.os.F.WRLCK; - try expectError(error.DeadLock, std.os.fcntl(fd, std.os.F.SETLKW, @ptrToInt(&struct_flock))); + struct_flock.type = os.F.WRLCK; + try expectError(error.DeadLock, os.fcntl(fd, os.F.SETLKW, @ptrToInt(&struct_flock))); // parent releases exclusive lock: struct_flock.start = 0; - struct_flock.type = std.os.F.UNLCK; - _ = try std.os.fcntl(fd, std.os.F.SETLK, @ptrToInt(&struct_flock)); + struct_flock.type = os.F.UNLCK; + _ = try os.fcntl(fd, os.F.SETLK, @ptrToInt(&struct_flock)); // parent releases shared lock: struct_flock.start = 1; - struct_flock.type = std.os.F.UNLCK; - _ = try std.os.fcntl(fd, std.os.F.SETLK, @ptrToInt(&struct_flock)); + struct_flock.type = os.F.UNLCK; + _ = try os.fcntl(fd, os.F.SETLK, @ptrToInt(&struct_flock)); // parent waits for child: - const result = std.os.waitpid(pid, 0); + const result = os.waitpid(pid, 0); try expect(result.status == 0 * 256); } } @@ -1182,3 +1182,17 @@ test "pwrite with empty buffer" { _ = try os.pwrite(file.handle, bytes, 0); } + +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); + 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); +}