authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-01-14 11:26:25-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-01-14 11:26:25-08:00
log4debd4338ce2c27a0dd6127ce5736ca56538214c
treea01612aee6f9f480d029db143f530d58a259377c
parent78549d1e109a922e63b52b3d4a445217ca997c9c
parent09074d7cd7ab76ebf87cb303825ce53834bcc532
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

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

Add `fchmodat` fallback on Linux when `flags` is nonzero.

6 files changed, 207 insertions(+), 22 deletions(-)

lib/std/c.zig+4-4
...@@ -5,10 +5,10 @@ const page_size = std.mem.page_size;...@@ -5,10 +5,10 @@ const page_size = std.mem.page_size;
5const iovec = std.os.iovec;5const iovec = std.os.iovec;
6const iovec_const = std.os.iovec_const;6const iovec_const = std.os.iovec_const;
77
8/// If not linking libc, returns struct{pub const ok = false;}8/// If not linking libc, returns false.
9/// If linking musl libc, returns struct{pub const ok = true;}9/// If linking musl libc, returns true.
10/// If linking gnu libc (glibc), the `ok` value will be true if the target10/// If linking gnu libc (glibc), returns true if the target version is greater
11/// version is greater than or equal to `glibc_version`.11/// than or equal to `glibc_version`.
12/// If linking a libc other than these, returns `false`.12/// If linking a libc other than these, returns `false`.
13pub inline fn versionCheck(comptime glibc_version: std.SemanticVersion) bool {13pub inline fn versionCheck(comptime glibc_version: std.SemanticVersion) bool {
14 return comptime blk: {14 return comptime blk: {
lib/std/os.zig+146-3
...@@ -348,18 +348,93 @@ pub fn fchmod(fd: fd_t, mode: mode_t) FChmodError!void {...@@ -348,18 +348,93 @@ pub fn fchmod(fd: fd_t, mode: mode_t) FChmodError!void {
348}348}
349349
350const FChmodAtError = FChmodError || error{350const FChmodAtError = FChmodError || error{
351 /// A component of `path` exceeded `NAME_MAX`, or the entire path exceeded
352 /// `PATH_MAX`.
351 NameTooLong,353 NameTooLong,
354 /// `path` resolves to a symbolic link, and `AT.SYMLINK_NOFOLLOW` was set
355 /// in `flags`. This error only occurs on Linux, where changing the mode of
356 /// a symbolic link has no meaning and can cause undefined behaviour on
357 /// certain filesystems.
358 ///
359 /// The procfs fallback was used but procfs was not mounted.
360 OperationNotSupported,
361 /// The procfs fallback was used but the process exceeded its open file
362 /// limit.
363 ProcessFdQuotaExceeded,
364 /// The procfs fallback was used but the system exceeded it open file limit.
365 SystemFdQuotaExceeded,
352};366};
353367
354pub fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void {368var has_fchmodat2_syscall = std.atomic.Value(bool).init(true);
369
370/// Changes the `mode` of `path` relative to the directory referred to by
371/// `dirfd`. The process must have the correct privileges in order to do this
372/// successfully, or must have the effective user ID matching the owner of the
373/// file.
374///
375/// On Linux the `fchmodat2` syscall will be used if available, otherwise a
376/// workaround using procfs will be employed. Changing the mode of a symbolic
377/// link with `AT.SYMLINK_NOFOLLOW` set will also return
378/// `OperationNotSupported`, as:
379///
380/// 1. Permissions on the link are ignored when resolving its target.
381/// 2. This operation has been known to invoke undefined behaviour across
382/// different filesystems[1].
383///
384/// [1]: https://sourceware.org/legacy-ml/libc-alpha/2020-02/msg00467.html.
385pub inline fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void {
355 if (!std.fs.has_executable_bit) @compileError("fchmodat unsupported by target OS");386 if (!std.fs.has_executable_bit) @compileError("fchmodat unsupported by target OS");
356387
357 const path_c = try toPosixPath(path);388 // No special handling for linux is needed if we can use the libc fallback
389 // or `flags` is empty. Glibc only added the fallback in 2.32.
390 const skip_fchmodat_fallback = builtin.os.tag != .linux or
391 std.c.versionCheck(.{ .major = 2, .minor = 32, .patch = 0 }) or
392 flags == 0;
358393
394 // This function is marked inline so that when flags is comptime-known,
395 // skip_fchmodat_fallback will be comptime-known true.
396 if (skip_fchmodat_fallback)
397 return fchmodat1(dirfd, path, mode, flags);
398
399 return fchmodat2(dirfd, path, mode, flags);
400}
401
402fn fchmodat1(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void {
403 const path_c = try toPosixPath(path);
359 while (true) {404 while (true) {
360 const res = system.fchmodat(dirfd, &path_c, mode, flags);405 const res = system.fchmodat(dirfd, &path_c, mode, flags);
361
362 switch (system.getErrno(res)) {406 switch (system.getErrno(res)) {
407 .SUCCESS => return,
408 .INTR => continue,
409 .BADF => unreachable,
410 .FAULT => unreachable,
411 .INVAL => unreachable,
412 .ACCES => return error.AccessDenied,
413 .IO => return error.InputOutput,
414 .LOOP => return error.SymLinkLoop,
415 .MFILE => return error.ProcessFdQuotaExceeded,
416 .NAMETOOLONG => return error.NameTooLong,
417 .NFILE => return error.SystemFdQuotaExceeded,
418 .NOENT => return error.FileNotFound,
419 .NOTDIR => return error.FileNotFound,
420 .NOMEM => return error.SystemResources,
421 .OPNOTSUPP => return error.OperationNotSupported,
422 .PERM => return error.AccessDenied,
423 .ROFS => return error.ReadOnlyFileSystem,
424 else => |err| return unexpectedErrno(err),
425 }
426 }
427}
428
429fn fchmodat2(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void {
430 const path_c = try toPosixPath(path);
431 const use_fchmodat2 = (builtin.os.isAtLeast(.linux, .{ .major = 6, .minor = 6, .patch = 0 }) orelse false) and
432 has_fchmodat2_syscall.load(.Monotonic);
433 while (use_fchmodat2) {
434 // Later on this should be changed to `system.fchmodat2`
435 // when the musl/glibc add a wrapper.
436 const res = linux.fchmodat2(dirfd, &path_c, mode, flags);
437 switch (linux.getErrno(res)) {
363 .SUCCESS => return,438 .SUCCESS => return,
364 .INTR => continue,439 .INTR => continue,
365 .BADF => unreachable,440 .BADF => unreachable,
...@@ -371,6 +446,74 @@ pub fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodA...@@ -371,6 +446,74 @@ pub fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodA
371 .NOENT => return error.FileNotFound,446 .NOENT => return error.FileNotFound,
372 .NOMEM => return error.SystemResources,447 .NOMEM => return error.SystemResources,
373 .NOTDIR => return error.FileNotFound,448 .NOTDIR => return error.FileNotFound,
449 .OPNOTSUPP => return error.OperationNotSupported,
450 .PERM => return error.AccessDenied,
451 .ROFS => return error.ReadOnlyFileSystem,
452
453 .NOSYS => { // Use fallback.
454 has_fchmodat2_syscall.store(false, .Monotonic);
455 break;
456 },
457 else => |err| return unexpectedErrno(err),
458 }
459 }
460
461 // Fallback to changing permissions using procfs:
462 //
463 // 1. Open `path` as an `O.PATH` descriptor.
464 // 2. Stat the fd and check if it isn't a symbolic link.
465 // 3. Generate the procfs reference to the fd via `/proc/self/fd/{fd}`.
466 // 4. Pass the procfs path to `chmod` with the `mode`.
467 var pathfd: fd_t = undefined;
468 while (true) {
469 const rc = system.openat(dirfd, &path_c, O.PATH | O.NOFOLLOW | O.CLOEXEC, @as(mode_t, 0));
470 switch (system.getErrno(rc)) {
471 .SUCCESS => {
472 pathfd = @as(fd_t, @intCast(rc));
473 break;
474 },
475 .INTR => continue,
476 .FAULT => unreachable,
477 .INVAL => unreachable,
478 .ACCES => return error.AccessDenied,
479 .PERM => return error.AccessDenied,
480 .LOOP => return error.SymLinkLoop,
481 .MFILE => return error.ProcessFdQuotaExceeded,
482 .NAMETOOLONG => return error.NameTooLong,
483 .NFILE => return error.SystemFdQuotaExceeded,
484 .NOENT => return error.FileNotFound,
485 .NOMEM => return error.SystemResources,
486 else => |err| return unexpectedErrno(err),
487 }
488 }
489 defer close(pathfd);
490
491 const stat = fstatatZ(pathfd, "", AT.EMPTY_PATH) catch |err| switch (err) {
492 error.NameTooLong => unreachable,
493 error.FileNotFound => unreachable,
494 else => |e| return e,
495 };
496 if ((stat.mode & S.IFMT) == S.IFLNK)
497 return error.OperationNotSupported;
498
499 var procfs_buf: ["/proc/self/fd/-2147483648\x00".len]u8 = undefined;
500 const proc_path = std.fmt.bufPrintZ(procfs_buf[0..], "/proc/self/fd/{d}", .{pathfd}) catch unreachable;
501 while (true) {
502 const res = system.chmod(proc_path, mode);
503 switch (system.getErrno(res)) {
504 // Getting NOENT here means that procfs isn't mounted.
505 .NOENT => return error.OperationNotSupported,
506
507 .SUCCESS => return,
508 .INTR => continue,
509 .BADF => unreachable,
510 .FAULT => unreachable,
511 .INVAL => unreachable,
512 .ACCES => return error.AccessDenied,
513 .IO => return error.InputOutput,
514 .LOOP => return error.SymLinkLoop,
515 .NOMEM => return error.SystemResources,
516 .NOTDIR => return error.FileNotFound,
374 .PERM => return error.AccessDenied,517 .PERM => return error.AccessDenied,
375 .ROFS => return error.ReadOnlyFileSystem,518 .ROFS => return error.ReadOnlyFileSystem,
376 else => |err| return unexpectedErrno(err),519 else => |err| return unexpectedErrno(err),
lib/std/os/linux.zig+7-9
...@@ -796,13 +796,7 @@ pub fn chmod(path: [*:0]const u8, mode: mode_t) usize {...@@ -796,13 +796,7 @@ pub fn chmod(path: [*:0]const u8, mode: mode_t) usize {
796 if (@hasField(SYS, "chmod")) {796 if (@hasField(SYS, "chmod")) {
797 return syscall2(.chmod, @intFromPtr(path), mode);797 return syscall2(.chmod, @intFromPtr(path), mode);
798 } else {798 } else {
799 return syscall4(799 return fchmodat(AT.FDCWD, path, mode, 0);
800 .fchmodat,
801 @as(usize, @bitCast(@as(isize, AT.FDCWD))),
802 @intFromPtr(path),
803 mode,
804 0,
805 );
806 }800 }
807}801}
808802
...@@ -814,8 +808,12 @@ pub fn fchown(fd: i32, owner: uid_t, group: gid_t) usize {...@@ -814,8 +808,12 @@ pub fn fchown(fd: i32, owner: uid_t, group: gid_t) usize {
814 }808 }
815}809}
816810
817pub fn fchmodat(fd: i32, path: [*:0]const u8, mode: mode_t, flags: u32) usize {811pub fn fchmodat(fd: i32, path: [*:0]const u8, mode: mode_t, _: u32) usize {
818 return syscall4(.fchmodat, @as(usize, @bitCast(@as(isize, fd))), @intFromPtr(path), mode, flags);812 return syscall3(.fchmodat, @bitCast(@as(isize, fd)), @intFromPtr(path), mode);
813}
814
815pub fn fchmodat2(fd: i32, path: [*:0]const u8, mode: mode_t, flags: u32) usize {
816 return syscall4(.fchmodat2, @bitCast(@as(isize, fd)), @intFromPtr(path), mode, flags);
819}817}
820818
821/// Can only be called on 32 bit systems. For 64 bit see `lseek`.819/// Can only be called on 32 bit systems. For 64 bit see `lseek`.
lib/std/os/linux/syscalls.zig+11
...@@ -443,6 +443,7 @@ pub const X86 = enum(usize) {...@@ -443,6 +443,7 @@ pub const X86 = enum(usize) {
443 futex_waitv = 449,443 futex_waitv = 449,
444 set_mempolicy_home_node = 450,444 set_mempolicy_home_node = 450,
445 cachestat = 451,445 cachestat = 451,
446 fchmodat2 = 452,
446};447};
447448
448pub const X64 = enum(usize) {449pub const X64 = enum(usize) {
...@@ -809,6 +810,8 @@ pub const X64 = enum(usize) {...@@ -809,6 +810,8 @@ pub const X64 = enum(usize) {
809 futex_waitv = 449,810 futex_waitv = 449,
810 set_mempolicy_home_node = 450,811 set_mempolicy_home_node = 450,
811 cachestat = 451,812 cachestat = 451,
813 fchmodat2 = 452,
814 map_shadow_stack = 453,
812};815};
813816
814pub const Arm = enum(usize) {817pub const Arm = enum(usize) {
...@@ -1218,6 +1221,7 @@ pub const Arm = enum(usize) {...@@ -1218,6 +1221,7 @@ pub const Arm = enum(usize) {
1218 futex_waitv = 449,1221 futex_waitv = 449,
1219 set_mempolicy_home_node = 450,1222 set_mempolicy_home_node = 450,
1220 cachestat = 451,1223 cachestat = 451,
1224 fchmodat2 = 452,
12211225
1222 breakpoint = arm_base + 1,1226 breakpoint = arm_base + 1,
1223 cacheflush = arm_base + 2,1227 cacheflush = arm_base + 2,
...@@ -1611,6 +1615,7 @@ pub const Sparc64 = enum(usize) {...@@ -1611,6 +1615,7 @@ pub const Sparc64 = enum(usize) {
1611 futex_waitv = 449,1615 futex_waitv = 449,
1612 set_mempolicy_home_node = 450,1616 set_mempolicy_home_node = 450,
1613 cachestat = 451,1617 cachestat = 451,
1618 fchmodat2 = 452,
1614};1619};
16151620
1616pub const Mips = enum(usize) {1621pub const Mips = enum(usize) {
...@@ -2035,6 +2040,7 @@ pub const Mips = enum(usize) {...@@ -2035,6 +2040,7 @@ pub const Mips = enum(usize) {
2035 futex_waitv = Linux + 449,2040 futex_waitv = Linux + 449,
2036 set_mempolicy_home_node = Linux + 450,2041 set_mempolicy_home_node = Linux + 450,
2037 cachestat = Linux + 451,2042 cachestat = Linux + 451,
2043 fchmodat2 = Linux + 452,
2038};2044};
20392045
2040pub const Mips64 = enum(usize) {2046pub const Mips64 = enum(usize) {
...@@ -2395,6 +2401,7 @@ pub const Mips64 = enum(usize) {...@@ -2395,6 +2401,7 @@ pub const Mips64 = enum(usize) {
2395 futex_waitv = Linux + 449,2401 futex_waitv = Linux + 449,
2396 set_mempolicy_home_node = Linux + 450,2402 set_mempolicy_home_node = Linux + 450,
2397 cachestat = Linux + 451,2403 cachestat = Linux + 451,
2404 fchmodat2 = Linux + 452,
2398};2405};
23992406
2400pub const PowerPC = enum(usize) {2407pub const PowerPC = enum(usize) {
...@@ -2830,6 +2837,7 @@ pub const PowerPC = enum(usize) {...@@ -2830,6 +2837,7 @@ pub const PowerPC = enum(usize) {
2830 futex_waitv = 449,2837 futex_waitv = 449,
2831 set_mempolicy_home_node = 450,2838 set_mempolicy_home_node = 450,
2832 cachestat = 451,2839 cachestat = 451,
2840 fchmodat2 = 452,
2833};2841};
28342842
2835pub const PowerPC64 = enum(usize) {2843pub const PowerPC64 = enum(usize) {
...@@ -3237,6 +3245,7 @@ pub const PowerPC64 = enum(usize) {...@@ -3237,6 +3245,7 @@ pub const PowerPC64 = enum(usize) {
3237 futex_waitv = 449,3245 futex_waitv = 449,
3238 set_mempolicy_home_node = 450,3246 set_mempolicy_home_node = 450,
3239 cachestat = 451,3247 cachestat = 451,
3248 fchmodat2 = 452,
3240};3249};
32413250
3242pub const Arm64 = enum(usize) {3251pub const Arm64 = enum(usize) {
...@@ -3547,6 +3556,7 @@ pub const Arm64 = enum(usize) {...@@ -3547,6 +3556,7 @@ pub const Arm64 = enum(usize) {
3547 futex_waitv = 449,3556 futex_waitv = 449,
3548 set_mempolicy_home_node = 450,3557 set_mempolicy_home_node = 450,
3549 cachestat = 451,3558 cachestat = 451,
3559 fchmodat2 = 452,
3550};3560};
35513561
3552pub const RiscV64 = enum(usize) {3562pub const RiscV64 = enum(usize) {
...@@ -3858,6 +3868,7 @@ pub const RiscV64 = enum(usize) {...@@ -3858,6 +3868,7 @@ pub const RiscV64 = enum(usize) {
3858 futex_waitv = 449,3868 futex_waitv = 449,
3859 set_mempolicy_home_node = 450,3869 set_mempolicy_home_node = 450,
3860 cachestat = 451,3870 cachestat = 451,
3871 fchmodat2 = 452,
38613872
3862 riscv_flush_icache = arch_specific_syscall + 15,3873 riscv_flush_icache = arch_specific_syscall + 15,
3863};3874};
lib/std/os/test.zig+35-5
...@@ -1217,16 +1217,46 @@ test "pwrite with empty buffer" {...@@ -1217,16 +1217,46 @@ test "pwrite with empty buffer" {
1217 _ = try os.pwrite(file.handle, bytes, 0);1217 _ = try os.pwrite(file.handle, bytes, 0);
1218}1218}
12191219
1220fn expectMode(dir: os.fd_t, file: []const u8, mode: os.mode_t) !void {
1221 const st = try os.fstatat(dir, file, os.AT.SYMLINK_NOFOLLOW);
1222 try expectEqual(mode, st.mode & 0b111_111_111);
1223}
1224
1220test "fchmodat smoke test" {1225test "fchmodat smoke test" {
1221 if (!std.fs.has_executable_bit) return error.SkipZigTest;1226 if (!std.fs.has_executable_bit) return error.SkipZigTest;
12221227
1223 var tmp = tmpDir(.{});1228 var tmp = tmpDir(.{});
1224 defer tmp.cleanup();1229 defer tmp.cleanup();
12251230
1226 try expectError(error.FileNotFound, os.fchmodat(tmp.dir.fd, "foo.txt", 0o666, 0));1231 try expectError(error.FileNotFound, os.fchmodat(tmp.dir.fd, "regfile", 0o666, 0));
1227 const fd = try os.openat(tmp.dir.fd, "foo.txt", os.O.RDWR | os.O.CREAT | os.O.EXCL, 0o666);1232 const fd = try os.openat(
1233 tmp.dir.fd,
1234 "regfile",
1235 os.O.WRONLY | os.O.CREAT | os.O.EXCL | os.O.TRUNC,
1236 0o644,
1237 );
1228 os.close(fd);1238 os.close(fd);
1229 try os.fchmodat(tmp.dir.fd, "foo.txt", 0o755, 0);1239 try os.symlinkat("regfile", tmp.dir.fd, "symlink");
1230 const st = try os.fstatat(tmp.dir.fd, "foo.txt", 0);1240 const sym_mode = blk: {
1231 try expectEqual(@as(os.mode_t, 0o755), st.mode & 0b111_111_111);1241 const st = try os.fstatat(tmp.dir.fd, "symlink", os.AT.SYMLINK_NOFOLLOW);
1242 break :blk st.mode & 0b111_111_111;
1243 };
1244
1245 try os.fchmodat(tmp.dir.fd, "regfile", 0o640, 0);
1246 try expectMode(tmp.dir.fd, "regfile", 0o640);
1247 try os.fchmodat(tmp.dir.fd, "regfile", 0o600, os.AT.SYMLINK_NOFOLLOW);
1248 try expectMode(tmp.dir.fd, "regfile", 0o600);
1249
1250 try os.fchmodat(tmp.dir.fd, "symlink", 0o640, 0);
1251 try expectMode(tmp.dir.fd, "regfile", 0o640);
1252 try expectMode(tmp.dir.fd, "symlink", sym_mode);
1253
1254 var test_link = true;
1255 os.fchmodat(tmp.dir.fd, "symlink", 0o600, os.AT.SYMLINK_NOFOLLOW) catch |err| switch (err) {
1256 error.OperationNotSupported => test_link = false,
1257 else => |e| return e,
1258 };
1259 if (test_link)
1260 try expectMode(tmp.dir.fd, "symlink", 0o600);
1261 try expectMode(tmp.dir.fd, "regfile", 0o640);
1232}1262}
src/link/Wasm.zig+4-1
...@@ -4917,7 +4917,10 @@ fn linkWithLLD(wasm: *Wasm, arena: Allocator, prog_node: *std.Progress.Node) !vo...@@ -4917,7 +4917,10 @@ fn linkWithLLD(wasm: *Wasm, arena: Allocator, prog_node: *std.Progress.Node) !vo
4917 // report a nice error here with the file path if it fails instead of4917 // report a nice error here with the file path if it fails instead of
4918 // just returning the error code.4918 // just returning the error code.
4919 // chmod does not interact with umask, so we use a conservative -rwxr--r-- here.4919 // chmod does not interact with umask, so we use a conservative -rwxr--r-- here.
4920 try std.os.fchmodat(fs.cwd().fd, full_out_path, 0o744, 0);4920 std.os.fchmodat(fs.cwd().fd, full_out_path, 0o744, 0) catch |err| switch (err) {
4921 error.OperationNotSupported => unreachable, // Not a symlink.
4922 else => |e| return e,
4923 };
4921 }4924 }
4922 }4925 }
49234926