authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-19 17:29:26-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-23 22:15:11-08:00
loge41342af83022202c6e413cd84c12b41f19c681a
tree622264e2ef6a094923e7adb290ec717af90d644c
parent64de4a7371e3851dddf39c608d08d480395bb0ab

std.Io.Threaded: reinstate fchmodat fallback

Stephen Gregoratto (original author of the fallback) says: I read through both libcs again to compare: Musl: - `stat` path. Check error. - If path is a symlink, return `OPNOTSUPP`. - `openat` path as `O_PATH|O_NOFOLLOW|O_CLOEXEC`. Return `OPNOTSUPP` if we got `ELOOP`. - Build procfs filename. - `stat` procfs file. - If procfs file is a symlink, return `OPNOTSUPP`. - close path fd. Glibc: - open path as `O_PATH|O_NOFOLLOW|O_CLOEXEC`. - fstatat path fd. - If path is a symlink, return `OPNOTSUPP`. - Build procfs filename. - chmod procfs filename. Return `OPNOTSUPP` if we got `ENOENT`. - close path fd. I prefer glibc since you open the path first, which avoids a possible TOCTOU race.

1 files changed, 111 insertions(+), 14 deletions(-)

lib/std/Io/Threaded.zig+111-14
......@@ -2091,7 +2091,7 @@ fn fileStatLinux(userdata: ?*anyopaque, file: File) File.StatError!File.Stat {
20912091 try current_thread.beginSyscall();
20922092 while (true) {
20932093 var statx = std.mem.zeroes(linux.Statx);
2094 const rc = sys.statx(
2094 switch (sys.errno(sys.statx(
20952095 file.handle,
20962096 "",
20972097 linux.AT.EMPTY_PATH,
......@@ -2106,8 +2106,7 @@ fn fileStatLinux(userdata: ?*anyopaque, file: File) File.StatError!File.Stat {
21062106 .NLINK = true,
21072107 },
21082108 &statx,
2109 );
2110 switch (sys.errno(rc)) {
2109 ))) {
21112110 .SUCCESS => {
21122111 current_thread.endSyscall();
21132112 return statFromLinux(&statx);
......@@ -5120,7 +5119,7 @@ fn posixFchmodat(
51205119 }
51215120
51225121 if (@atomicLoad(UseFchmodat2, &t.use_fchmodat2, .monotonic) == .disabled)
5123 return fchmodatFallback(current_thread, dir_fd, path, mode, flags);
5122 return fchmodatFallback(current_thread, dir_fd, path, mode);
51245123
51255124 comptime assert(native_os == .linux);
51265125
......@@ -5149,7 +5148,7 @@ fn posixFchmodat(
51495148 .ROFS => return error.ReadOnlyFileSystem,
51505149 .NOSYS => {
51515150 @atomicStore(UseFchmodat2, &t.use_fchmodat2, .disabled, .monotonic);
5152 return fchmodatFallback(current_thread, dir_fd, path, mode, flags);
5151 return fchmodatFallback(current_thread, dir_fd, path, mode);
51535152 },
51545153 else => |err| return posix.unexpectedErrno(err),
51555154 }
......@@ -5163,16 +5162,114 @@ fn fchmodatFallback(
51635162 dir_fd: posix.fd_t,
51645163 path: [*:0]const u8,
51655164 mode: posix.mode_t,
5166 flags: u32,
51675165) Dir.SetFilePermissionsError!void {
5168 _ = current_thread;
5169 _ = dir_fd;
5170 _ = path;
5171 _ = mode;
5172 _ = flags;
5173 // I deleted the previous fallback implementation because it looked wrong to me. Please cross-reference
5174 // fhmodat.c in musl libc before blindly restoring the implementation.
5175 @panic("TODO");
5166 comptime assert(native_os == .linux);
5167 const use_c = std.c.versionCheck(if (builtin.abi.isAndroid())
5168 .{ .major = 30, .minor = 0, .patch = 0 }
5169 else
5170 .{ .major = 2, .minor = 28, .patch = 0 });
5171 const sys = if (use_c) std.c else std.os.linux;
5172
5173 // Fallback to changing permissions using procfs:
5174 //
5175 // 1. Open `path` as a `PATH` descriptor.
5176 // 2. Stat the fd and check if it isn't a symbolic link.
5177 // 3. Generate the procfs reference to the fd via `/proc/self/fd/{fd}`.
5178 // 4. Pass the procfs path to `chmod` with the `mode`.
5179 try current_thread.beginSyscall();
5180 const path_fd: posix.fd_t = while (true) {
5181 const rc = posix.system.openat(dir_fd, path, .{
5182 .PATH = true,
5183 .NOFOLLOW = true,
5184 .CLOEXEC = true,
5185 }, @as(posix.mode_t, 0));
5186 switch (posix.errno(rc)) {
5187 .SUCCESS => {
5188 current_thread.endSyscall();
5189 break @intCast(rc);
5190 },
5191 .INTR => {
5192 try current_thread.checkCancel();
5193 continue;
5194 },
5195 else => |e| {
5196 current_thread.endSyscall();
5197 switch (e) {
5198 .FAULT => |err| return errnoBug(err),
5199 .INVAL => |err| return errnoBug(err),
5200 .ACCES => return error.AccessDenied,
5201 .PERM => return error.PermissionDenied,
5202 .LOOP => return error.SymLinkLoop,
5203 .MFILE => return error.ProcessFdQuotaExceeded,
5204 .NAMETOOLONG => return error.NameTooLong,
5205 .NFILE => return error.SystemFdQuotaExceeded,
5206 .NOENT => return error.FileNotFound,
5207 .NOMEM => return error.SystemResources,
5208 else => |err| return posix.unexpectedErrno(err),
5209 }
5210 },
5211 }
5212 };
5213 defer posix.close(path_fd);
5214
5215 try current_thread.beginSyscall();
5216 const path_mode = while (true) {
5217 var statx = std.mem.zeroes(std.os.linux.Statx);
5218 switch (sys.errno(sys.statx(path_fd, "", posix.AT.EMPTY_PATH, .{ .TYPE = true }, &statx))) {
5219 .SUCCESS => {
5220 current_thread.endSyscall();
5221 assert(statx.mask.TYPE);
5222 break statx.mode;
5223 },
5224 .INTR => {
5225 try current_thread.checkCancel();
5226 continue;
5227 },
5228 else => |e| {
5229 current_thread.endSyscall();
5230 switch (e) {
5231 .ACCES => return error.AccessDenied,
5232 .LOOP => return error.SymLinkLoop,
5233 .NOMEM => return error.SystemResources,
5234 else => |err| return posix.unexpectedErrno(err),
5235 }
5236 },
5237 }
5238 };
5239
5240 // Even though we only wanted TYPE, the kernel can still fill in the additional bits.
5241 if ((path_mode & posix.S.IFMT) == posix.S.IFLNK)
5242 return error.OperationUnsupported;
5243
5244 var procfs_buf: ["/proc/self/fd/-2147483648\x00".len]u8 = undefined;
5245 const proc_path = std.fmt.bufPrintSentinel(&procfs_buf, "/proc/self/fd/{d}", .{path_fd}, 0) catch unreachable;
5246 try current_thread.beginSyscall();
5247 while (true) {
5248 switch (posix.errno(posix.system.chmod(proc_path, mode))) {
5249 .SUCCESS => return current_thread.endSyscall(),
5250 .INTR => {
5251 try current_thread.checkCancel();
5252 continue;
5253 },
5254 else => |e| {
5255 current_thread.endSyscall();
5256 switch (e) {
5257 .NOENT => return error.OperationUnsupported, // procfs not mounted.
5258 .BADF => |err| return errnoBug(err),
5259 .FAULT => |err| return errnoBug(err),
5260 .INVAL => |err| return errnoBug(err),
5261 .ACCES => return error.AccessDenied,
5262 .IO => return error.InputOutput,
5263 .LOOP => return error.SymLinkLoop,
5264 .NOMEM => return error.SystemResources,
5265 .NOTDIR => return error.FileNotFound,
5266 .PERM => return error.PermissionDenied,
5267 .ROFS => return error.ReadOnlyFileSystem,
5268 else => |err| return posix.unexpectedErrno(err),
5269 }
5270 },
5271 }
5272 }
51765273}
51775274
51785275const dirSetOwner = switch (native_os) {