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 {...@@ -2091,7 +2091,7 @@ fn fileStatLinux(userdata: ?*anyopaque, file: File) File.StatError!File.Stat {
2091 try current_thread.beginSyscall();2091 try current_thread.beginSyscall();
2092 while (true) {2092 while (true) {
2093 var statx = std.mem.zeroes(linux.Statx);2093 var statx = std.mem.zeroes(linux.Statx);
2094 const rc = sys.statx(2094 switch (sys.errno(sys.statx(
2095 file.handle,2095 file.handle,
2096 "",2096 "",
2097 linux.AT.EMPTY_PATH,2097 linux.AT.EMPTY_PATH,
...@@ -2106,8 +2106,7 @@ fn fileStatLinux(userdata: ?*anyopaque, file: File) File.StatError!File.Stat {...@@ -2106,8 +2106,7 @@ fn fileStatLinux(userdata: ?*anyopaque, file: File) File.StatError!File.Stat {
2106 .NLINK = true,2106 .NLINK = true,
2107 },2107 },
2108 &statx,2108 &statx,
2109 );2109 ))) {
2110 switch (sys.errno(rc)) {
2111 .SUCCESS => {2110 .SUCCESS => {
2112 current_thread.endSyscall();2111 current_thread.endSyscall();
2113 return statFromLinux(&statx);2112 return statFromLinux(&statx);
...@@ -5120,7 +5119,7 @@ fn posixFchmodat(...@@ -5120,7 +5119,7 @@ fn posixFchmodat(
5120 }5119 }
51215120
5122 if (@atomicLoad(UseFchmodat2, &t.use_fchmodat2, .monotonic) == .disabled)5121 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
5125 comptime assert(native_os == .linux);5124 comptime assert(native_os == .linux);
51265125
...@@ -5149,7 +5148,7 @@ fn posixFchmodat(...@@ -5149,7 +5148,7 @@ fn posixFchmodat(
5149 .ROFS => return error.ReadOnlyFileSystem,5148 .ROFS => return error.ReadOnlyFileSystem,
5150 .NOSYS => {5149 .NOSYS => {
5151 @atomicStore(UseFchmodat2, &t.use_fchmodat2, .disabled, .monotonic);5150 @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);
5153 },5152 },
5154 else => |err| return posix.unexpectedErrno(err),5153 else => |err| return posix.unexpectedErrno(err),
5155 }5154 }
...@@ -5163,16 +5162,114 @@ fn fchmodatFallback(...@@ -5163,16 +5162,114 @@ fn fchmodatFallback(
5163 dir_fd: posix.fd_t,5162 dir_fd: posix.fd_t,
5164 path: [*:0]const u8,5163 path: [*:0]const u8,
5165 mode: posix.mode_t,5164 mode: posix.mode_t,
5166 flags: u32,
5167) Dir.SetFilePermissionsError!void {5165) Dir.SetFilePermissionsError!void {
5168 _ = current_thread;5166 comptime assert(native_os == .linux);
5169 _ = dir_fd;5167 const use_c = std.c.versionCheck(if (builtin.abi.isAndroid())
5170 _ = path;5168 .{ .major = 30, .minor = 0, .patch = 0 }
5171 _ = mode;5169 else
5172 _ = flags;5170 .{ .major = 2, .minor = 28, .patch = 0 });
5173 // I deleted the previous fallback implementation because it looked wrong to me. Please cross-reference5171 const sys = if (use_c) std.c else std.os.linux;
5174 // fhmodat.c in musl libc before blindly restoring the implementation.5172
5175 @panic("TODO");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 }
5176}5273}
51775274
5178const dirSetOwner = switch (native_os) {5275const dirSetOwner = switch (native_os) {