authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-11 21:00:14-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-23 22:15:09-08:00
log0e230993d51d0ecded40d5235ada4f2f64036b26
tree4caa6e7f003c791cd96a54b63226b78eaa649282
parentc4cefd68358470d464bf66bc41985cadd874db73

std.Io.Dir: add setFilePermissions and setFileOwner


11 files changed, 239 insertions(+), 211 deletions(-)

lib/std/Io.zig+2
...@@ -684,7 +684,9 @@ pub const VTable = struct {...@@ -684,7 +684,9 @@ pub const VTable = struct {
684 dirSymLink: *const fn (?*anyopaque, Dir, target_path: []const u8, sym_link_path: []const u8, Dir.SymLinkFlags) Dir.SymLinkError!void,684 dirSymLink: *const fn (?*anyopaque, Dir, target_path: []const u8, sym_link_path: []const u8, Dir.SymLinkFlags) Dir.SymLinkError!void,
685 dirReadLink: *const fn (?*anyopaque, Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize,685 dirReadLink: *const fn (?*anyopaque, Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize,
686 dirSetOwner: *const fn (?*anyopaque, Dir, ?File.Uid, ?File.Gid) Dir.SetOwnerError!void,686 dirSetOwner: *const fn (?*anyopaque, Dir, ?File.Uid, ?File.Gid) Dir.SetOwnerError!void,
687 dirSetFileOwner: *const fn (?*anyopaque, Dir, []const u8, ?File.Uid, ?File.Gid, Dir.SetFileOwnerOptions) Dir.SetFileOwnerError!void,
687 dirSetPermissions: *const fn (?*anyopaque, Dir, Dir.Permissions) Dir.SetPermissionsError!void,688 dirSetPermissions: *const fn (?*anyopaque, Dir, Dir.Permissions) Dir.SetPermissionsError!void,
689 dirSetFilePermissions: *const fn (?*anyopaque, Dir, []const u8, File.Permissions, Dir.SetFilePermissionsOptions) Dir.SetFilePermissionsError!void,
688 dirSetTimestamps: *const fn (?*anyopaque, Dir, []const u8, last_accessed: Timestamp, last_modified: Timestamp, Dir.SetTimestampsOptions) Dir.SetTimestampsError!void,690 dirSetTimestamps: *const fn (?*anyopaque, Dir, []const u8, last_accessed: Timestamp, last_modified: Timestamp, Dir.SetTimestampsOptions) Dir.SetTimestampsError!void,
689 dirSetTimestampsNow: *const fn (?*anyopaque, Dir, []const u8, Dir.SetTimestampsOptions) Dir.SetTimestampsError!void,691 dirSetTimestampsNow: *const fn (?*anyopaque, Dir, []const u8, Dir.SetTimestampsOptions) Dir.SetTimestampsError!void,
690692
lib/std/Io/Dir.zig+41
...@@ -1698,6 +1698,29 @@ pub fn setPermissions(dir: Dir, io: Io, new_permissions: File.Permissions) SetPe...@@ -1698,6 +1698,29 @@ pub fn setPermissions(dir: Dir, io: Io, new_permissions: File.Permissions) SetPe
1698 return io.vtable.dirSetPermissions(io.userdata, dir, new_permissions);1698 return io.vtable.dirSetPermissions(io.userdata, dir, new_permissions);
1699}1699}
17001700
1701pub const SetFilePermissionsError = PathNameError || SetPermissionsError || error{
1702 ProcessFdQuotaExceeded,
1703 SystemFdQuotaExceeded,
1704 /// `SetFilePermissionsOptions.follow_symlinks` was set to false, which is
1705 /// not allowed by the file system or operating system.
1706 OperationNotSupported,
1707};
1708
1709pub const SetFilePermissionsOptions = struct {
1710 follow_symlinks: bool = true,
1711};
1712
1713/// Also known as "chmodat".
1714pub fn setFilePermissions(
1715 dir: Dir,
1716 io: Io,
1717 sub_path: []const u8,
1718 new_permissions: File.Permissions,
1719 options: SetFilePermissionsOptions,
1720) SetFilePermissionsError!void {
1721 return io.vtable.dirSetFilePermissions(io.userdata, sub_path, dir, new_permissions, options);
1722}
1723
1701pub const SetOwnerError = File.SetOwnerError;1724pub const SetOwnerError = File.SetOwnerError;
17021725
1703/// Also known as "chown".1726/// Also known as "chown".
...@@ -1711,6 +1734,24 @@ pub fn setOwner(dir: Dir, io: Io, owner: ?File.Uid, group: ?File.Gid) SetOwnerEr...@@ -1711,6 +1734,24 @@ pub fn setOwner(dir: Dir, io: Io, owner: ?File.Uid, group: ?File.Gid) SetOwnerEr
1711 return io.vtable.dirSetOwner(io.userdata, dir, owner, group);1734 return io.vtable.dirSetOwner(io.userdata, dir, owner, group);
1712}1735}
17131736
1737pub const SetFileOwnerError = PathNameError || SetOwnerError;
1738
1739pub const SetFileOwnerOptions = struct {
1740 follow_symlinks: bool = true,
1741};
1742
1743/// Also known as "fchownat".
1744pub fn setFileOwner(
1745 dir: Dir,
1746 io: Io,
1747 sub_path: []const u8,
1748 owner: ?File.Uid,
1749 group: ?File.Gid,
1750 options: SetFileOwnerOptions,
1751) SetOwnerError!void {
1752 return io.vtable.dirSetFileOwner(io.userdata, dir, sub_path, owner, group, options);
1753}
1754
1714pub const SetTimestampsError = File.SetTimestampsError || PathNameError;1755pub const SetTimestampsError = File.SetTimestampsError || PathNameError;
17151756
1716pub const SetTimestampsOptions = struct {1757pub const SetTimestampsOptions = struct {
lib/std/Io/Threaded.zig+176-3
...@@ -76,6 +76,7 @@ old_sig_pipe: if (have_sig_pipe) posix.Sigaction else void,...@@ -76,6 +76,7 @@ old_sig_pipe: if (have_sig_pipe) posix.Sigaction else void,
76use_sendfile: UseSendfile = .default,76use_sendfile: UseSendfile = .default,
77use_copy_file_range: UseCopyFileRange = .default,77use_copy_file_range: UseCopyFileRange = .default,
78use_fcopyfile: UseFcopyfile = .default,78use_fcopyfile: UseFcopyfile = .default,
79use_fchmodat2: UseFchmodat2 = .default,
7980
80stderr_writer: File.Writer = .{81stderr_writer: File.Writer = .{
81 .io = undefined,82 .io = undefined,
...@@ -124,6 +125,15 @@ pub const UseFcopyfile = if (have_fcopyfile) enum {...@@ -124,6 +125,15 @@ pub const UseFcopyfile = if (have_fcopyfile) enum {
124 pub const default: UseFcopyfile = .disabled;125 pub const default: UseFcopyfile = .disabled;
125};126};
126127
128pub const UseFchmodat2 = if (have_fchmodat2 and !have_fchmodat_flags) enum {
129 enabled,
130 disabled,
131 pub const default: UseFchmodat2 = .enabled;
132} else enum {
133 disabled,
134 pub const default: UseFchmodat2 = .disabled;
135};
136
127const Thread = struct {137const Thread = struct {
128 /// The value that needs to be passed to pthread_kill or tgkill in order to138 /// The value that needs to be passed to pthread_kill or tgkill in order to
129 /// send a signal.139 /// send a signal.
...@@ -712,7 +722,9 @@ pub fn io(t: *Threaded) Io {...@@ -712,7 +722,9 @@ pub fn io(t: *Threaded) Io {
712 .dirSymLink = dirSymLink,722 .dirSymLink = dirSymLink,
713 .dirReadLink = dirReadLink,723 .dirReadLink = dirReadLink,
714 .dirSetOwner = dirSetOwner,724 .dirSetOwner = dirSetOwner,
725 .dirSetFileOwner = dirSetFileOwner,
715 .dirSetPermissions = dirSetPermissions,726 .dirSetPermissions = dirSetPermissions,
727 .dirSetFilePermissions = dirSetFilePermissions,
716 .dirSetTimestamps = dirSetTimestamps,728 .dirSetTimestamps = dirSetTimestamps,
717 .dirSetTimestampsNow = dirSetTimestampsNow,729 .dirSetTimestampsNow = dirSetTimestampsNow,
718730
...@@ -842,7 +854,9 @@ pub fn ioBasic(t: *Threaded) Io {...@@ -842,7 +854,9 @@ pub fn ioBasic(t: *Threaded) Io {
842 .dirSymLink = dirSymLink,854 .dirSymLink = dirSymLink,
843 .dirReadLink = dirReadLink,855 .dirReadLink = dirReadLink,
844 .dirSetOwner = dirSetOwner,856 .dirSetOwner = dirSetOwner,
857 .dirSetFileOwner = dirSetFileOwner,
845 .dirSetPermissions = dirSetPermissions,858 .dirSetPermissions = dirSetPermissions,
859 .dirSetFilePermissions = dirSetFilePermissions,
846 .dirSetTimestamps = dirSetTimestamps,860 .dirSetTimestamps = dirSetTimestamps,
847 .dirSetTimestampsNow = dirSetTimestampsNow,861 .dirSetTimestampsNow = dirSetTimestampsNow,
848862
...@@ -921,6 +935,11 @@ const have_copy_file_range = switch (native_os) {...@@ -921,6 +935,11 @@ const have_copy_file_range = switch (native_os) {
921 else => false,935 else => false,
922};936};
923const have_fcopyfile = is_darwin;937const have_fcopyfile = is_darwin;
938const have_fchmodat2 = native_os == .linux and
939 (builtin.os.isAtLeast(.linux, .{ .major = 6, .minor = 6, .patch = 0 }) orelse true) and
940 (builtin.abi.isAndroid() or !std.c.versionCheck(.{ .major = 2, .minor = 32, .patch = 0 }));
941const have_fchmodat_flags = native_os != .linux or
942 (!builtin.abi.isAndroid() and std.c.versionCheck(.{ .major = 2, .minor = 32, .patch = 0 }));
924943
925const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat;944const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat;
926const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat;945const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat;
...@@ -4862,11 +4881,11 @@ const dirSetPermissions = switch (native_os) {...@@ -4862,11 +4881,11 @@ const dirSetPermissions = switch (native_os) {
4862};4881};
48634882
4864fn dirSetPermissionsWindows(userdata: ?*anyopaque, dir: Dir, permissions: Dir.Permissions) Dir.SetPermissionsError!void {4883fn dirSetPermissionsWindows(userdata: ?*anyopaque, dir: Dir, permissions: Dir.Permissions) Dir.SetPermissionsError!void {
4865 // TODO I think we can actually set permissions on a dir on windows?4884 const t: *Threaded = @ptrCast(@alignCast(userdata));
4866 _ = userdata;4885 _ = t;
4867 _ = dir;4886 _ = dir;
4868 _ = permissions;4887 _ = permissions;
4869 return error.Unexpected;4888 @panic("TODO");
4870}4889}
48714890
4872fn dirSetPermissionsPosix(userdata: ?*anyopaque, dir: Dir, permissions: Dir.Permissions) Dir.SetPermissionsError!void {4891fn dirSetPermissionsPosix(userdata: ?*anyopaque, dir: Dir, permissions: Dir.Permissions) Dir.SetPermissionsError!void {
...@@ -4875,6 +4894,133 @@ fn dirSetPermissionsPosix(userdata: ?*anyopaque, dir: Dir, permissions: Dir.Perm...@@ -4875,6 +4894,133 @@ fn dirSetPermissionsPosix(userdata: ?*anyopaque, dir: Dir, permissions: Dir.Perm
4875 return setPermissionsPosix(current_thread, dir.handle, permissions.toMode());4894 return setPermissionsPosix(current_thread, dir.handle, permissions.toMode());
4876}4895}
48774896
4897fn dirSetFilePermissions(
4898 userdata: ?*anyopaque,
4899 dir: Dir,
4900 sub_path: []const u8,
4901 permissions: Dir.Permissions,
4902 options: Dir.SetFilePermissionsOptions,
4903) Dir.SetFilePermissionsError!void {
4904 if (!Dir.Permissions.has_executable_bit) return error.Unexpected;
4905 if (is_windows) @panic("TODO");
4906 const t: *Threaded = @ptrCast(@alignCast(userdata));
4907 const current_thread = Thread.getCurrent(t);
4908
4909 var path_buffer: [posix.PATH_MAX]u8 = undefined;
4910 const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
4911
4912 const mode = permissions.toMode();
4913 const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0;
4914
4915 return posixFchmodat(t, current_thread, dir.handle, sub_path_posix, mode, flags);
4916}
4917
4918fn posixFchmodat(
4919 t: *Threaded,
4920 current_thread: *Thread,
4921 dir_fd: posix.fd_t,
4922 path: [*:0]const u8,
4923 mode: posix.mode_t,
4924 flags: u32,
4925) Dir.SetFilePermissionsError!void {
4926 // No special handling for linux is needed if we can use the libc fallback
4927 // or `flags` is empty. Glibc only added the fallback in 2.32.
4928 if (have_fchmodat_flags or flags == 0) {
4929 try current_thread.beginSyscall();
4930 while (true) {
4931 const rc = if (have_fchmodat_flags)
4932 posix.system.fchmodat(dir_fd, path, mode, flags)
4933 else
4934 posix.system.fchmodat(dir_fd, path, mode);
4935 switch (posix.errno(rc)) {
4936 .SUCCESS => return current_thread.endSyscall(),
4937 .CANCELED => return current_thread.endSyscallCanceled(),
4938 .INTR => {
4939 try current_thread.checkCancel();
4940 continue;
4941 },
4942 else => |e| {
4943 current_thread.endSyscall();
4944 switch (e) {
4945 .BADF => |err| return errnoBug(err),
4946 .FAULT => |err| return errnoBug(err),
4947 .INVAL => |err| return errnoBug(err),
4948 .ACCES => return error.AccessDenied,
4949 .IO => return error.InputOutput,
4950 .LOOP => return error.SymLinkLoop,
4951 .MFILE => return error.ProcessFdQuotaExceeded,
4952 .NAMETOOLONG => return error.NameTooLong,
4953 .NFILE => return error.SystemFdQuotaExceeded,
4954 .NOENT => return error.FileNotFound,
4955 .NOTDIR => return error.FileNotFound,
4956 .NOMEM => return error.SystemResources,
4957 .OPNOTSUPP => return error.OperationNotSupported,
4958 .PERM => return error.PermissionDenied,
4959 .ROFS => return error.ReadOnlyFileSystem,
4960 else => |err| return posix.unexpectedErrno(err),
4961 }
4962 },
4963 }
4964 }
4965 }
4966
4967 if (@atomicLoad(UseFchmodat2, &t.use_fchmodat2, .monotonic) == .disabled)
4968 return fchmodatFallback(current_thread, dir_fd, path, mode, flags);
4969
4970 comptime assert(native_os == .linux);
4971
4972 try current_thread.beginSyscall();
4973 while (true) {
4974 switch (std.os.linux.errno(std.os.linux.fchmodat2(dir_fd, path, mode, flags))) {
4975 .SUCCESS => return current_thread.endSyscall(),
4976 .CANCELED => return current_thread.endSyscallCanceled(),
4977 .INTR => {
4978 try current_thread.checkCancel();
4979 continue;
4980 },
4981 else => |e| {
4982 current_thread.endSyscall();
4983 switch (e) {
4984 .BADF => |err| return errnoBug(err),
4985 .FAULT => |err| return errnoBug(err),
4986 .INVAL => |err| return errnoBug(err),
4987 .ACCES => return error.AccessDenied,
4988 .IO => return error.InputOutput,
4989 .LOOP => return error.SymLinkLoop,
4990 .NOENT => return error.FileNotFound,
4991 .NOMEM => return error.SystemResources,
4992 .NOTDIR => return error.FileNotFound,
4993 .OPNOTSUPP => return error.OperationNotSupported,
4994 .PERM => return error.PermissionDenied,
4995 .ROFS => return error.ReadOnlyFileSystem,
4996 .NOSYS => {
4997 @atomicStore(UseFchmodat2, &t.use_fchmodat2, .disabled, .monotonic);
4998 return fchmodatFallback(current_thread, dir_fd, path, mode, flags);
4999 },
5000 else => |err| return posix.unexpectedErrno(err),
5001 }
5002 },
5003 }
5004 }
5005}
5006
5007fn fchmodatFallback(
5008 current_thread: *Thread,
5009 dir_fd: posix.fd_t,
5010 path: [*:0]const u8,
5011 mode: posix.mode_t,
5012 flags: u32,
5013) Dir.SetFilePermissionsError!void {
5014 _ = current_thread;
5015 _ = dir_fd;
5016 _ = path;
5017 _ = mode;
5018 _ = flags;
5019 // I deleted the previous fallback implementation because it looked wrong to me. Please cross-reference
5020 // fhmodat.c in musl libc before blindly restoring the implementation.
5021 @panic("TODO");
5022}
5023
4878const dirSetOwner = switch (native_os) {5024const dirSetOwner = switch (native_os) {
4879 .windows => dirSetOwnerUnsupported,5025 .windows => dirSetOwnerUnsupported,
4880 else => dirSetOwnerPosix,5026 else => dirSetOwnerPosix,
...@@ -4927,6 +5073,33 @@ fn setOwnerPosix(current_thread: *Thread, fd: posix.fd_t, uid: posix.uid_t, gid:...@@ -4927,6 +5073,33 @@ fn setOwnerPosix(current_thread: *Thread, fd: posix.fd_t, uid: posix.uid_t, gid:
4927 }5073 }
4928}5074}
49295075
5076fn dirSetFileOwner(
5077 userdata: ?*anyopaque,
5078 dir: Dir,
5079 sub_path: []const u8,
5080 owner: ?File.Uid,
5081 group: ?File.Gid,
5082 options: Dir.SetFileOwnerOptions,
5083) Dir.SetFileOwnerError!void {
5084 const t: *Threaded = @ptrCast(@alignCast(userdata));
5085 const current_thread = Thread.getCurrent(t);
5086
5087 if (is_windows) {
5088 @panic("TODO");
5089 }
5090
5091 var path_buffer: [posix.PATH_MAX]u8 = undefined;
5092 const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
5093
5094 _ = current_thread;
5095 _ = dir;
5096 _ = sub_path_posix;
5097 _ = owner;
5098 _ = group;
5099 _ = options;
5100 @panic("TODO");
5101}
5102
4930const fileSync = switch (native_os) {5103const fileSync = switch (native_os) {
4931 .windows => fileSyncWindows,5104 .windows => fileSyncWindows,
4932 else => fileSyncPosix,5105 else => fileSyncPosix,
lib/std/os/linux.zig+2-2
...@@ -1420,7 +1420,7 @@ pub fn chmod(path: [*:0]const u8, mode: mode_t) usize {...@@ -1420,7 +1420,7 @@ pub fn chmod(path: [*:0]const u8, mode: mode_t) usize {
1420 if (@hasField(SYS, "chmod")) {1420 if (@hasField(SYS, "chmod")) {
1421 return syscall2(.chmod, @intFromPtr(path), mode);1421 return syscall2(.chmod, @intFromPtr(path), mode);
1422 } else {1422 } else {
1423 return fchmodat(AT.FDCWD, path, mode, 0);1423 return fchmodat(AT.FDCWD, path, mode);
1424 }1424 }
1425}1425}
14261426
...@@ -1432,7 +1432,7 @@ pub fn fchown(fd: i32, owner: uid_t, group: gid_t) usize {...@@ -1432,7 +1432,7 @@ pub fn fchown(fd: i32, owner: uid_t, group: gid_t) usize {
1432 }1432 }
1433}1433}
14341434
1435pub fn fchmodat(fd: i32, path: [*:0]const u8, mode: mode_t, _: u32) usize {1435pub fn fchmodat(fd: i32, path: [*:0]const u8, mode: mode_t) usize {
1436 return syscall3(.fchmodat, @bitCast(@as(isize, fd)), @intFromPtr(path), mode);1436 return syscall3(.fchmodat, @bitCast(@as(isize, fd)), @intFromPtr(path), mode);
1437}1437}
14381438
lib/std/posix.zig-184
...@@ -310,190 +310,6 @@ pub fn close(fd: fd_t) void {...@@ -310,190 +310,6 @@ pub fn close(fd: fd_t) void {
310 }310 }
311}311}
312312
313pub const FChmodAtError = std.Io.File.SetPermissionsError || error{
314 /// A component of `path` exceeded `NAME_MAX`, or the entire path exceeded
315 /// `PATH_MAX`.
316 NameTooLong,
317 /// `path` resolves to a symbolic link, and `AT.SYMLINK_NOFOLLOW` was set
318 /// in `flags`. This error only occurs on Linux, where changing the mode of
319 /// a symbolic link has no meaning and can cause undefined behaviour on
320 /// certain filesystems.
321 ///
322 /// The procfs fallback was used but procfs was not mounted.
323 OperationNotSupported,
324 /// The procfs fallback was used but the process exceeded its open file
325 /// limit.
326 ProcessFdQuotaExceeded,
327 /// The procfs fallback was used but the system exceeded it open file limit.
328 SystemFdQuotaExceeded,
329};
330
331/// Changes the `mode` of `path` relative to the directory referred to by
332/// `dirfd`. The process must have the correct privileges in order to do this
333/// successfully, or must have the effective user ID matching the owner of the
334/// file.
335///
336/// On Linux the `fchmodat2` syscall will be used if available, otherwise a
337/// workaround using procfs will be employed. Changing the mode of a symbolic
338/// link with `AT.SYMLINK_NOFOLLOW` set will also return
339/// `OperationNotSupported`, as:
340///
341/// 1. Permissions on the link are ignored when resolving its target.
342/// 2. This operation has been known to invoke undefined behaviour across
343/// different filesystems[1].
344///
345/// [1]: https://sourceware.org/legacy-ml/libc-alpha/2020-02/msg00467.html.
346pub inline fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void {
347 if (!fs.has_executable_bit) @compileError("fchmodat unsupported by target OS");
348
349 // No special handling for linux is needed if we can use the libc fallback
350 // or `flags` is empty. Glibc only added the fallback in 2.32.
351 const skip_fchmodat_fallback = native_os != .linux or
352 (!builtin.abi.isAndroid() and std.c.versionCheck(.{ .major = 2, .minor = 32, .patch = 0 })) or
353 flags == 0;
354
355 // This function is marked inline so that when flags is comptime-known,
356 // skip_fchmodat_fallback will be comptime-known true.
357 if (skip_fchmodat_fallback)
358 return fchmodat1(dirfd, path, mode, flags);
359
360 return fchmodat2(dirfd, path, mode, flags);
361}
362
363fn fchmodat1(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void {
364 const path_c = try toPosixPath(path);
365 while (true) {
366 const res = system.fchmodat(dirfd, &path_c, mode, flags);
367 switch (errno(res)) {
368 .SUCCESS => return,
369 .INTR => continue,
370 .BADF => unreachable,
371 .FAULT => unreachable,
372 .INVAL => unreachable,
373 .ACCES => return error.AccessDenied,
374 .IO => return error.InputOutput,
375 .LOOP => return error.SymLinkLoop,
376 .MFILE => return error.ProcessFdQuotaExceeded,
377 .NAMETOOLONG => return error.NameTooLong,
378 .NFILE => return error.SystemFdQuotaExceeded,
379 .NOENT => return error.FileNotFound,
380 .NOTDIR => return error.FileNotFound,
381 .NOMEM => return error.SystemResources,
382 .OPNOTSUPP => return error.OperationNotSupported,
383 .PERM => return error.PermissionDenied,
384 .ROFS => return error.ReadOnlyFileSystem,
385 else => |err| return unexpectedErrno(err),
386 }
387 }
388}
389
390fn fchmodat2(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void {
391 const global = struct {
392 var has_fchmodat2: bool = true;
393 };
394 const path_c = try toPosixPath(path);
395 const use_fchmodat2 = (builtin.os.isAtLeast(.linux, .{ .major = 6, .minor = 6, .patch = 0 }) orelse false) and
396 @atomicLoad(bool, &global.has_fchmodat2, .monotonic);
397 while (use_fchmodat2) {
398 // Later on this should be changed to `system.fchmodat2`
399 // when the musl/glibc add a wrapper.
400 const res = linux.fchmodat2(dirfd, &path_c, mode, flags);
401 switch (linux.errno(res)) {
402 .SUCCESS => return,
403 .INTR => continue,
404 .BADF => unreachable,
405 .FAULT => unreachable,
406 .INVAL => unreachable,
407 .ACCES => return error.AccessDenied,
408 .IO => return error.InputOutput,
409 .LOOP => return error.SymLinkLoop,
410 .NOENT => return error.FileNotFound,
411 .NOMEM => return error.SystemResources,
412 .NOTDIR => return error.FileNotFound,
413 .OPNOTSUPP => return error.OperationNotSupported,
414 .PERM => return error.PermissionDenied,
415 .ROFS => return error.ReadOnlyFileSystem,
416
417 .NOSYS => {
418 @atomicStore(bool, &global.has_fchmodat2, false, .monotonic);
419 break;
420 },
421 else => |err| return unexpectedErrno(err),
422 }
423 }
424
425 // Fallback to changing permissions using procfs:
426 //
427 // 1. Open `path` as a `PATH` descriptor.
428 // 2. Stat the fd and check if it isn't a symbolic link.
429 // 3. Generate the procfs reference to the fd via `/proc/self/fd/{fd}`.
430 // 4. Pass the procfs path to `chmod` with the `mode`.
431 var pathfd: fd_t = undefined;
432 while (true) {
433 const rc = system.openat(dirfd, &path_c, .{ .PATH = true, .NOFOLLOW = true, .CLOEXEC = true }, @as(mode_t, 0));
434 switch (errno(rc)) {
435 .SUCCESS => {
436 pathfd = @intCast(rc);
437 break;
438 },
439 .INTR => continue,
440 .FAULT => unreachable,
441 .INVAL => unreachable,
442 .ACCES => return error.AccessDenied,
443 .PERM => return error.PermissionDenied,
444 .LOOP => return error.SymLinkLoop,
445 .MFILE => return error.ProcessFdQuotaExceeded,
446 .NAMETOOLONG => return error.NameTooLong,
447 .NFILE => return error.SystemFdQuotaExceeded,
448 .NOENT => return error.FileNotFound,
449 .NOMEM => return error.SystemResources,
450 else => |err| return unexpectedErrno(err),
451 }
452 }
453 defer close(pathfd);
454
455 const path_mode = if (linux.wrapped.statx(
456 pathfd,
457 "",
458 AT.EMPTY_PATH,
459 .{ .TYPE = true },
460 )) |stx| blk: {
461 assert(stx.mask.TYPE);
462 break :blk stx.mode;
463 } else |err| switch (err) {
464 error.NameTooLong => unreachable,
465 error.FileNotFound => unreachable,
466 else => |e| return e,
467 };
468 // Even though we only wanted TYPE, the kernel can still fill in the additional bits.
469 if ((path_mode & S.IFMT) == S.IFLNK)
470 return error.OperationNotSupported;
471
472 var procfs_buf: ["/proc/self/fd/-2147483648\x00".len]u8 = undefined;
473 const proc_path = std.fmt.bufPrintSentinel(procfs_buf[0..], "/proc/self/fd/{d}", .{pathfd}, 0) catch unreachable;
474 while (true) {
475 const res = system.chmod(proc_path, mode);
476 switch (errno(res)) {
477 // Getting NOENT here means that procfs isn't mounted.
478 .NOENT => return error.OperationNotSupported,
479
480 .SUCCESS => return,
481 .INTR => continue,
482 .BADF => unreachable,
483 .FAULT => unreachable,
484 .INVAL => unreachable,
485 .ACCES => return error.AccessDenied,
486 .IO => return error.InputOutput,
487 .LOOP => return error.SymLinkLoop,
488 .NOMEM => return error.SystemResources,
489 .NOTDIR => return error.FileNotFound,
490 .PERM => return error.PermissionDenied,
491 .ROFS => return error.ReadOnlyFileSystem,
492 else => |err| return unexpectedErrno(err),
493 }
494 }
495}
496
497pub const RebootError = error{313pub const RebootError = error{
498 PermissionDenied,314 PermissionDenied,
499} || UnexpectedError;315} || UnexpectedError;
lib/std/posix/test.zig+1-1
...@@ -895,7 +895,7 @@ fn expectMode(dir: posix.fd_t, file: []const u8, mode: posix.mode_t) !void {...@@ -895,7 +895,7 @@ fn expectMode(dir: posix.fd_t, file: []const u8, mode: posix.mode_t) !void {
895}895}
896896
897test "fchmodat smoke test" {897test "fchmodat smoke test" {
898 if (!std.fs.has_executable_bit) return error.SkipZigTest;898 if (!Io.File.Permissions.has_executable_bit) return error.SkipZigTest;
899899
900 var tmp = tmpDir(.{});900 var tmp = tmpDir(.{});
901 defer tmp.cleanup();901 defer tmp.cleanup();
lib/std/tar.zig+3-3
...@@ -1122,7 +1122,7 @@ fn normalizePath(bytes: []u8) []u8 {...@@ -1122,7 +1122,7 @@ fn normalizePath(bytes: []u8) []u8 {
1122fn filePermissions(mode: u32, options: PipeOptions) Io.File.Permissions {1122fn filePermissions(mode: u32, options: PipeOptions) Io.File.Permissions {
1123 const default_mode = 0o666;1123 const default_mode = 0o666;
11241124
1125 if (!std.fs.has_executable_bit or options.mode_mode == .ignore)1125 if (!Io.File.Permissions.has_executable_bit or options.mode_mode == .ignore)
1126 return .fromMode(default_mode);1126 return .fromMode(default_mode);
11271127
1128 const S = std.posix.S;1128 const S = std.posix.S;
...@@ -1137,7 +1137,7 @@ fn filePermissions(mode: u32, options: PipeOptions) Io.File.Permissions {...@@ -1137,7 +1137,7 @@ fn filePermissions(mode: u32, options: PipeOptions) Io.File.Permissions {
1137}1137}
11381138
1139test filePermissions {1139test filePermissions {
1140 if (!std.fs.has_executable_bit) return error.SkipZigTest;1140 if (!Io.File.Permissions.has_executable_bit) return error.SkipZigTest;
1141 try testing.expectEqual(0o666, filePermissions(0o744, PipeOptions{ .mode_mode = .ignore }));1141 try testing.expectEqual(0o666, filePermissions(0o744, PipeOptions{ .mode_mode = .ignore }));
1142 try testing.expectEqual(0o777, filePermissions(0o744, PipeOptions{}));1142 try testing.expectEqual(0o777, filePermissions(0o744, PipeOptions{}));
1143 try testing.expectEqual(0o666, filePermissions(0o644, PipeOptions{}));1143 try testing.expectEqual(0o666, filePermissions(0o644, PipeOptions{}));
...@@ -1145,7 +1145,7 @@ test filePermissions {...@@ -1145,7 +1145,7 @@ test filePermissions {
1145}1145}
11461146
1147test "executable bit" {1147test "executable bit" {
1148 if (!std.fs.has_executable_bit) return error.SkipZigTest;1148 if (!Io.File.Permissions.has_executable_bit) return error.SkipZigTest;
11491149
1150 const io = testing.io;1150 const io = testing.io;
1151 const S = std.posix.S;1151 const S = std.posix.S;
lib/std/zig/parser_test.zig+7-7
...@@ -6334,7 +6334,7 @@ var fixed_buffer_mem: [100 * 1024]u8 = undefined;...@@ -6334,7 +6334,7 @@ var fixed_buffer_mem: [100 * 1024]u8 = undefined;
63346334
6335fn testParse(source: [:0]const u8, allocator: mem.Allocator, anything_changed: *bool) ![]u8 {6335fn testParse(source: [:0]const u8, allocator: mem.Allocator, anything_changed: *bool) ![]u8 {
6336 var buffer: [64]u8 = undefined;6336 var buffer: [64]u8 = undefined;
6337 const stderr, _ = std.debug.lockStderrWriter(&buffer);6337 const stderr = std.debug.lockStderrWriter(&buffer);
6338 defer std.debug.unlockStderrWriter();6338 defer std.debug.unlockStderrWriter();
63396339
6340 var tree = try std.zig.Ast.parse(allocator, source, .zig);6340 var tree = try std.zig.Ast.parse(allocator, source, .zig);
...@@ -6342,17 +6342,17 @@ fn testParse(source: [:0]const u8, allocator: mem.Allocator, anything_changed: *...@@ -6342,17 +6342,17 @@ fn testParse(source: [:0]const u8, allocator: mem.Allocator, anything_changed: *
63426342
6343 for (tree.errors) |parse_error| {6343 for (tree.errors) |parse_error| {
6344 const loc = tree.tokenLocation(0, parse_error.token);6344 const loc = tree.tokenLocation(0, parse_error.token);
6345 try stderr.print("(memory buffer):{d}:{d}: error: ", .{ loc.line + 1, loc.column + 1 });6345 try stderr.printUnescaped("(memory buffer):{d}:{d}: error: ", .{ loc.line + 1, loc.column + 1 });
6346 try tree.renderError(parse_error, stderr);6346 try tree.renderError(parse_error, &stderr.interface);
6347 try stderr.print("\n{s}\n", .{source[loc.line_start..loc.line_end]});6347 try stderr.interface.print("\n{s}\n", .{source[loc.line_start..loc.line_end]});
6348 {6348 {
6349 var i: usize = 0;6349 var i: usize = 0;
6350 while (i < loc.column) : (i += 1) {6350 while (i < loc.column) : (i += 1) {
6351 try stderr.writeAll(" ");6351 try stderr.writeAllUnescaped(" ");
6352 }6352 }
6353 try stderr.writeAll("^");6353 try stderr.writeAllUnescaped("^");
6354 }6354 }
6355 try stderr.writeAll("\n");6355 try stderr.writeAllUnescaped("\n");
6356 }6356 }
6357 if (tree.errors.len != 0) {6357 if (tree.errors.len != 0) {
6358 return error.ParseError;6358 return error.ParseError;
src/Package/Fetch.zig+2-2
...@@ -1717,7 +1717,7 @@ fn deleteFileFallible(io: Io, dir: Io.Dir, deleted_file: *DeletedFile) DeletedFi...@@ -1717,7 +1717,7 @@ fn deleteFileFallible(io: Io, dir: Io.Dir, deleted_file: *DeletedFile) DeletedFi
1717}1717}
17181718
1719fn setExecutable(file: Io.File) !void {1719fn setExecutable(file: Io.File) !void {
1720 if (!std.fs.has_executable_bit) return;1720 if (!Io.File.Permissions.has_executable_bit) return;
17211721
1722 const S = std.posix.S;1722 const S = std.posix.S;
1723 const mode = Io.File.default_mode | S.IXUSR | S.IXGRP | S.IXOTH;1723 const mode = Io.File.default_mode | S.IXUSR | S.IXGRP | S.IXOTH;
...@@ -2183,7 +2183,7 @@ test "tarball without root folder" {...@@ -2183,7 +2183,7 @@ test "tarball without root folder" {
2183}2183}
21842184
2185test "set executable bit based on file content" {2185test "set executable bit based on file content" {
2186 if (!std.fs.has_executable_bit) return error.SkipZigTest;2186 if (!Io.File.Permissions.has_executable_bit) return error.SkipZigTest;
2187 const gpa = std.testing.allocator;2187 const gpa = std.testing.allocator;
2188 const io = std.testing.io;2188 const io = std.testing.io;
21892189
src/link/Lld.zig+4-8
...@@ -1327,6 +1327,7 @@ fn getLDMOption(target: *const std.Target) ?[]const u8 {...@@ -1327,6 +1327,7 @@ fn getLDMOption(target: *const std.Target) ?[]const u8 {
1327}1327}
1328fn wasmLink(lld: *Lld, arena: Allocator) !void {1328fn wasmLink(lld: *Lld, arena: Allocator) !void {
1329 const comp = lld.base.comp;1329 const comp = lld.base.comp;
1330 const diags = &comp.link_diags;
1330 const shared_memory = comp.config.shared_memory;1331 const shared_memory = comp.config.shared_memory;
1331 const export_memory = comp.config.export_memory;1332 const export_memory = comp.config.export_memory;
1332 const import_memory = comp.config.import_memory;1333 const import_memory = comp.config.import_memory;
...@@ -1566,17 +1567,12 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void {...@@ -1566,17 +1567,12 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void {
1566 // is not the case, it means we will get "exec format error" when trying to run1567 // is not the case, it means we will get "exec format error" when trying to run
1567 // it, and then can react to that in the same way as trying to run an ELF file1568 // it, and then can react to that in the same way as trying to run an ELF file
1568 // from a foreign CPU architecture.1569 // from a foreign CPU architecture.
1569 if (fs.has_executable_bit and target.os.tag == .wasi and1570 if (Io.File.Permissions.has_executable_bit and target.os.tag == .wasi and
1570 comp.config.output_mode == .Exe)1571 comp.config.output_mode == .Exe)
1571 {1572 {
1572 // TODO: what's our strategy for reporting linker errors from this function?
1573 // report a nice error here with the file path if it fails instead of
1574 // just returning the error code.
1575 // chmod does not interact with umask, so we use a conservative -rwxr--r-- here.1573 // chmod does not interact with umask, so we use a conservative -rwxr--r-- here.
1576 std.posix.fchmodat(Io.Dir.cwd().handle, full_out_path, 0o744, 0) catch |err| switch (err) {1574 Io.Dir.cwd().setFilePermissions(full_out_path, .fromMode(0o744), .{}) catch |err|
1577 error.OperationNotSupported => unreachable, // Not a symlink.1575 return diags.fail("{s}: failed to enable executable permissions: {t}", .{ full_out_path, err });
1578 else => |e| return e,
1579 };
1580 }1576 }
1581 }1577 }
1582}1578}
src/link/Wasm.zig+1-1
...@@ -3002,7 +3002,7 @@ pub fn createEmpty(...@@ -3002,7 +3002,7 @@ pub fn createEmpty(
3002 wasm.base.file = try emit.root_dir.handle.createFile(io, emit.sub_path, .{3002 wasm.base.file = try emit.root_dir.handle.createFile(io, emit.sub_path, .{
3003 .truncate = true,3003 .truncate = true,
3004 .read = true,3004 .read = true,
3005 .mode = if (fs.has_executable_bit)3005 .mode = if (Io.File.Permissions.has_executable_bit)
3006 if (target.os.tag == .wasi and output_mode == .Exe)3006 if (target.os.tag == .wasi and output_mode == .Exe)
3007 Io.File.default_mode | 0b001_000_0003007 Io.File.default_mode | 0b001_000_000
3008 else3008 else