| author | |
| committer | |
| log | 4debd4338ce2c27a0dd6127ce5736ca56538214c |
| tree | a01612aee6f9f480d029db143f530d58a259377c |
| parent | 78549d1e109a922e63b52b3d4a445217ca997c9c |
| parent | 09074d7cd7ab76ebf87cb303825ce53834bcc532 |
| signature |
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 | 5 | const iovec = std.os.iovec; |
| 6 | 6 | const iovec_const = std.os.iovec_const; |
| 7 | 7 | |
| 8 | /// If not linking libc, returns struct{pub const ok = false;} | |
| 9 | /// If linking musl libc, returns struct{pub const ok = true;} | |
| 10 | /// If linking gnu libc (glibc), the `ok` value will be true if the target | |
| 11 | /// version is greater than or equal to `glibc_version`. | |
| 8 | /// If not linking libc, returns false. | |
| 9 | /// If linking musl libc, returns true. | |
| 10 | /// If linking gnu libc (glibc), returns true if the target version is greater | |
| 11 | /// than or equal to `glibc_version`. | |
| 12 | 12 | /// If linking a libc other than these, returns `false`. |
| 13 | 13 | pub inline fn versionCheck(comptime glibc_version: std.SemanticVersion) bool { |
| 14 | 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 | 348 | } |
| 349 | 349 | |
| 350 | 350 | const FChmodAtError = FChmodError || error{ |
| 351 | /// A component of `path` exceeded `NAME_MAX`, or the entire path exceeded | |
| 352 | /// `PATH_MAX`. | |
| 351 | 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 | }; |
| 353 | 367 | |
| 354 | pub fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void { | |
| 368 | var 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. | |
| 385 | pub inline fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void { | |
| 355 | 386 | if (!std.fs.has_executable_bit) @compileError("fchmodat unsupported by target OS"); |
| 356 | 387 | |
| 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; | |
| 358 | 393 | |
| 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 | ||
| 402 | fn fchmodat1(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void { | |
| 403 | const path_c = try toPosixPath(path); | |
| 359 | 404 | while (true) { |
| 360 | 405 | const res = system.fchmodat(dirfd, &path_c, mode, flags); |
| 361 | ||
| 362 | 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 | ||
| 429 | fn 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 | 438 | .SUCCESS => return, |
| 364 | 439 | .INTR => continue, |
| 365 | 440 | .BADF => unreachable, |
| ... | ... | @@ -371,6 +446,74 @@ pub fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodA |
| 371 | 446 | .NOENT => return error.FileNotFound, |
| 372 | 447 | .NOMEM => return error.SystemResources, |
| 373 | 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 | 517 | .PERM => return error.AccessDenied, |
| 375 | 518 | .ROFS => return error.ReadOnlyFileSystem, |
| 376 | 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 | 796 | if (@hasField(SYS, "chmod")) { |
| 797 | 797 | return syscall2(.chmod, @intFromPtr(path), mode); |
| 798 | 798 | } else { |
| 799 | return syscall4( | |
| 800 | .fchmodat, | |
| 801 | @as(usize, @bitCast(@as(isize, AT.FDCWD))), | |
| 802 | @intFromPtr(path), | |
| 803 | mode, | |
| 804 | 0, | |
| 805 | ); | |
| 799 | return fchmodat(AT.FDCWD, path, mode, 0); | |
| 806 | 800 | } |
| 807 | 801 | } |
| 808 | 802 | |
| ... | ... | @@ -814,8 +808,12 @@ pub fn fchown(fd: i32, owner: uid_t, group: gid_t) usize { |
| 814 | 808 | } |
| 815 | 809 | } |
| 816 | 810 | |
| 817 | pub fn fchmodat(fd: i32, path: [*:0]const u8, mode: mode_t, flags: u32) usize { | |
| 818 | return syscall4(.fchmodat, @as(usize, @bitCast(@as(isize, fd))), @intFromPtr(path), mode, flags); | |
| 811 | pub fn fchmodat(fd: i32, path: [*:0]const u8, mode: mode_t, _: u32) usize { | |
| 812 | return syscall3(.fchmodat, @bitCast(@as(isize, fd)), @intFromPtr(path), mode); | |
| 813 | } | |
| 814 | ||
| 815 | pub 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 | } |
| 820 | 818 | |
| 821 | 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 | 443 | futex_waitv = 449, |
| 444 | 444 | set_mempolicy_home_node = 450, |
| 445 | 445 | cachestat = 451, |
| 446 | fchmodat2 = 452, | |
| 446 | 447 | }; |
| 447 | 448 | |
| 448 | 449 | pub const X64 = enum(usize) { |
| ... | ... | @@ -809,6 +810,8 @@ pub const X64 = enum(usize) { |
| 809 | 810 | futex_waitv = 449, |
| 810 | 811 | set_mempolicy_home_node = 450, |
| 811 | 812 | cachestat = 451, |
| 813 | fchmodat2 = 452, | |
| 814 | map_shadow_stack = 453, | |
| 812 | 815 | }; |
| 813 | 816 | |
| 814 | 817 | pub const Arm = enum(usize) { |
| ... | ... | @@ -1218,6 +1221,7 @@ pub const Arm = enum(usize) { |
| 1218 | 1221 | futex_waitv = 449, |
| 1219 | 1222 | set_mempolicy_home_node = 450, |
| 1220 | 1223 | cachestat = 451, |
| 1224 | fchmodat2 = 452, | |
| 1221 | 1225 | |
| 1222 | 1226 | breakpoint = arm_base + 1, |
| 1223 | 1227 | cacheflush = arm_base + 2, |
| ... | ... | @@ -1611,6 +1615,7 @@ pub const Sparc64 = enum(usize) { |
| 1611 | 1615 | futex_waitv = 449, |
| 1612 | 1616 | set_mempolicy_home_node = 450, |
| 1613 | 1617 | cachestat = 451, |
| 1618 | fchmodat2 = 452, | |
| 1614 | 1619 | }; |
| 1615 | 1620 | |
| 1616 | 1621 | pub const Mips = enum(usize) { |
| ... | ... | @@ -2035,6 +2040,7 @@ pub const Mips = enum(usize) { |
| 2035 | 2040 | futex_waitv = Linux + 449, |
| 2036 | 2041 | set_mempolicy_home_node = Linux + 450, |
| 2037 | 2042 | cachestat = Linux + 451, |
| 2043 | fchmodat2 = Linux + 452, | |
| 2038 | 2044 | }; |
| 2039 | 2045 | |
| 2040 | 2046 | pub const Mips64 = enum(usize) { |
| ... | ... | @@ -2395,6 +2401,7 @@ pub const Mips64 = enum(usize) { |
| 2395 | 2401 | futex_waitv = Linux + 449, |
| 2396 | 2402 | set_mempolicy_home_node = Linux + 450, |
| 2397 | 2403 | cachestat = Linux + 451, |
| 2404 | fchmodat2 = Linux + 452, | |
| 2398 | 2405 | }; |
| 2399 | 2406 | |
| 2400 | 2407 | pub const PowerPC = enum(usize) { |
| ... | ... | @@ -2830,6 +2837,7 @@ pub const PowerPC = enum(usize) { |
| 2830 | 2837 | futex_waitv = 449, |
| 2831 | 2838 | set_mempolicy_home_node = 450, |
| 2832 | 2839 | cachestat = 451, |
| 2840 | fchmodat2 = 452, | |
| 2833 | 2841 | }; |
| 2834 | 2842 | |
| 2835 | 2843 | pub const PowerPC64 = enum(usize) { |
| ... | ... | @@ -3237,6 +3245,7 @@ pub const PowerPC64 = enum(usize) { |
| 3237 | 3245 | futex_waitv = 449, |
| 3238 | 3246 | set_mempolicy_home_node = 450, |
| 3239 | 3247 | cachestat = 451, |
| 3248 | fchmodat2 = 452, | |
| 3240 | 3249 | }; |
| 3241 | 3250 | |
| 3242 | 3251 | pub const Arm64 = enum(usize) { |
| ... | ... | @@ -3547,6 +3556,7 @@ pub const Arm64 = enum(usize) { |
| 3547 | 3556 | futex_waitv = 449, |
| 3548 | 3557 | set_mempolicy_home_node = 450, |
| 3549 | 3558 | cachestat = 451, |
| 3559 | fchmodat2 = 452, | |
| 3550 | 3560 | }; |
| 3551 | 3561 | |
| 3552 | 3562 | pub const RiscV64 = enum(usize) { |
| ... | ... | @@ -3858,6 +3868,7 @@ pub const RiscV64 = enum(usize) { |
| 3858 | 3868 | futex_waitv = 449, |
| 3859 | 3869 | set_mempolicy_home_node = 450, |
| 3860 | 3870 | cachestat = 451, |
| 3871 | fchmodat2 = 452, | |
| 3861 | 3872 | |
| 3862 | 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 | 1217 | _ = try os.pwrite(file.handle, bytes, 0); |
| 1218 | 1218 | } |
| 1219 | 1219 | |
| 1220 | fn 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 | ||
| 1220 | 1225 | test "fchmodat smoke test" { |
| 1221 | 1226 | if (!std.fs.has_executable_bit) return error.SkipZigTest; |
| 1222 | 1227 | |
| 1223 | 1228 | var tmp = tmpDir(.{}); |
| 1224 | 1229 | defer tmp.cleanup(); |
| 1225 | 1230 | |
| 1226 | try expectError(error.FileNotFound, os.fchmodat(tmp.dir.fd, "foo.txt", 0o666, 0)); | |
| 1227 | const fd = try os.openat(tmp.dir.fd, "foo.txt", os.O.RDWR | os.O.CREAT | os.O.EXCL, 0o666); | |
| 1231 | try expectError(error.FileNotFound, os.fchmodat(tmp.dir.fd, "regfile", 0o666, 0)); | |
| 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 | 1238 | os.close(fd); |
| 1229 | try os.fchmodat(tmp.dir.fd, "foo.txt", 0o755, 0); | |
| 1230 | const st = try os.fstatat(tmp.dir.fd, "foo.txt", 0); | |
| 1231 | try expectEqual(@as(os.mode_t, 0o755), st.mode & 0b111_111_111); | |
| 1239 | try os.symlinkat("regfile", tmp.dir.fd, "symlink"); | |
| 1240 | const sym_mode = blk: { | |
| 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 | 4917 | // report a nice error here with the file path if it fails instead of |
| 4918 | 4918 | // just returning the error code. |
| 4919 | 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 | } |
| 4923 | 4926 |