authorgravatar for dev@sgregoratto.meStephen Gregoratto <dev@sgregoratto.me> 2023-11-04 17:06:16+11:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-01-13 23:52:01-07:00
logbc69d62669fac591bda4c91d695d32cf2b78f34c
treea726f05b28feb98e49666bc6fdb82e5bece6efed
parentcf6751ae5510964f0d349e6ea044d8f618ba6e33

Linux: Add fchmodat fallback when `flags` is nonzero

The check for determining whether to use the fallback code has been moved into an inline function as per Andrew's comments in #17954.

2 files changed, 144 insertions(+), 4 deletions(-)

lib/std/os.zig+140-3
...@@ -348,17 +348,58 @@ pub fn fchmod(fd: fd_t, mode: mode_t) FChmodError!void {...@@ -348,17 +348,58 @@ 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
355 /// `path` resolves to a symbolic link, and `AT.SYMLINK_NOFOLLOW` was set
356 /// in `flags`. This error only occurs on Linux, where changing the mode of
357 /// a symbolic link has no meaning and can cause undefined behaviour on
358 /// certain filesystems.
359 ///
360 /// The procfs fallback was used but procfs was not mounted.
361 OperationNotSupported,
362
363 /// The procfs fallback was used but the process exceeded its open file
364 /// limit.
365 ProcessFdQuotaExceeded,
366
367 /// The procfs fallback was used but the system exceeded it open file limit.
368 SystemFdQuotaExceeded,
352};369};
353370
354pub fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void {371var has_fchmodat2_syscall = std.atomic.Value(bool).init(true);
372
373inline fn skipFchmodatFallback(flags: u32) bool {
374 return builtin.os.tag != .linux or
375 flags == 0 or
376 std.c.versionCheck(std.SemanticVersion{ .major = 2, .minor = 32, .patch = 0 }).ok;
377}
378
379/// Changes the `mode` of `path` relative to the directory referred to by
380/// `dirfd`. The process must have the correct privileges in order to do this
381/// successfully, or must have the effective user ID matching the owner of the
382/// file.
383///
384/// On Linux the `fchmodat2` syscall will be used if available, otherwise a
385/// workaround using procfs will be employed. Changing the mode of a symbolic
386/// link with `AT.SYMLINK_NOFOLLOW` set will also return
387/// `OperationNotSupported`, as:
388///
389/// 1. Permissions on the link are ignored when resolving its target.
390/// 2. This operation has been known to invoke undefined behaviour across
391/// different filesystems[1].
392///
393/// [1]: https://sourceware.org/legacy-ml/libc-alpha/2020-02/msg00467.html.
394pub 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");395 if (!std.fs.has_executable_bit) @compileError("fchmodat unsupported by target OS");
356396
357 const path_c = try toPosixPath(path);397 const path_c = try toPosixPath(path);
358398
359 while (true) {399 // No special handling for linux is needed if we can use the libc fallback
400 // or `flags` is empty. Glibc only added the fallback in 2.32.
401 while (skipFchmodatFallback(flags)) {
360 const res = system.fchmodat(dirfd, &path_c, mode, flags);402 const res = system.fchmodat(dirfd, &path_c, mode, flags);
361
362 switch (system.getErrno(res)) {403 switch (system.getErrno(res)) {
363 .SUCCESS => return,404 .SUCCESS => return,
364 .INTR => continue,405 .INTR => continue,
...@@ -368,8 +409,104 @@ pub fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodA...@@ -368,8 +409,104 @@ pub fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodA
368 .ACCES => return error.AccessDenied,409 .ACCES => return error.AccessDenied,
369 .IO => return error.InputOutput,410 .IO => return error.InputOutput,
370 .LOOP => return error.SymLinkLoop,411 .LOOP => return error.SymLinkLoop,
412 .MFILE => return error.ProcessFdQuotaExceeded,
413 .NAMETOOLONG => return error.NameTooLong,
414 .NFILE => return error.SystemFdQuotaExceeded,
415 .NOENT => return error.FileNotFound,
416 .NOTDIR => return error.FileNotFound,
417 .NOMEM => return error.SystemResources,
418 .OPNOTSUPP => return error.OperationNotSupported,
419 .PERM => return error.AccessDenied,
420 .ROFS => return error.ReadOnlyFileSystem,
421 else => |err| return unexpectedErrno(err),
422 }
423 }
424
425 const use_fchmodat2 = (comptime builtin.os.isAtLeast(.linux, .{ .major = 6, .minor = 6, .patch = 0 }) orelse false) and
426 has_fchmodat2_syscall.load(.Monotonic);
427 while (use_fchmodat2) {
428 // Later on this should be changed to `system.fchmodat2`
429 // when the musl/glibc add a wrapper.
430 const res = linux.fchmodat2(dirfd, &path_c, mode, flags);
431 switch (linux.getErrno(res)) {
432 .SUCCESS => return,
433 .INTR => continue,
434 .BADF => unreachable,
435 .FAULT => unreachable,
436 .INVAL => unreachable,
437 .ACCES => return error.AccessDenied,
438 .IO => return error.InputOutput,
439 .LOOP => return error.SymLinkLoop,
440 .NOENT => return error.FileNotFound,
441 .NOMEM => return error.SystemResources,
442 .NOTDIR => return error.FileNotFound,
443 .OPNOTSUPP => return error.OperationNotSupported,
444 .PERM => return error.AccessDenied,
445 .ROFS => return error.ReadOnlyFileSystem,
446
447 .NOSYS => { // Use fallback.
448 has_fchmodat2_syscall.store(false, .Monotonic);
449 break;
450 },
451 else => |err| return unexpectedErrno(err),
452 }
453 }
454
455 // Fallback to changing permissions using procfs:
456 //
457 // 1. Open `path` as an `O.PATH` descriptor.
458 // 2. Stat the fd and check if it isn't a symbolic link.
459 // 3. Generate the procfs reference to the fd via `/proc/self/fd/{fd}`.
460 // 4. Pass the procfs path to `chmod` with the `mode`.
461 var pathfd: fd_t = undefined;
462 while (true) {
463 const rc = system.openat(dirfd, &path_c, O.PATH | O.NOFOLLOW | O.CLOEXEC, @as(mode_t, 0));
464 switch (system.getErrno(rc)) {
465 .SUCCESS => {
466 pathfd = @as(fd_t, @intCast(rc));
467 break;
468 },
469 .INTR => continue,
470 .FAULT => unreachable,
471 .INVAL => unreachable,
472 .ACCES => return error.AccessDenied,
473 .PERM => return error.AccessDenied,
474 .LOOP => return error.SymLinkLoop,
475 .MFILE => return error.ProcessFdQuotaExceeded,
476 .NAMETOOLONG => return error.NameTooLong,
477 .NFILE => return error.SystemFdQuotaExceeded,
371 .NOENT => return error.FileNotFound,478 .NOENT => return error.FileNotFound,
372 .NOMEM => return error.SystemResources,479 .NOMEM => return error.SystemResources,
480 else => |err| return unexpectedErrno(err),
481 }
482 }
483 defer close(pathfd);
484
485 const stat = fstatatZ(pathfd, "", AT.EMPTY_PATH) catch |err| switch (err) {
486 error.NameTooLong => unreachable,
487 error.FileNotFound => unreachable,
488 else => |e| return e,
489 };
490 if ((stat.mode & S.IFMT) == S.IFLNK)
491 return error.OperationNotSupported;
492
493 var procfs_buf: ["/proc/self/fd/-2147483648\x00".len]u8 = undefined;
494 const proc_path = std.fmt.bufPrintZ(procfs_buf[0..], "/proc/self/fd/{d}", .{pathfd}) catch unreachable;
495 while (true) {
496 const res = system.chmod(proc_path, mode);
497 switch (system.getErrno(res)) {
498 // Getting NOENT here means that procfs isn't mounted.
499 .NOENT => return error.OperationNotSupported,
500
501 .SUCCESS => return,
502 .INTR => continue,
503 .BADF => unreachable,
504 .FAULT => unreachable,
505 .INVAL => unreachable,
506 .ACCES => return error.AccessDenied,
507 .IO => return error.InputOutput,
508 .LOOP => return error.SymLinkLoop,
509 .NOMEM => return error.SystemResources,
373 .NOTDIR => return error.FileNotFound,510 .NOTDIR => return error.FileNotFound,
374 .PERM => return error.AccessDenied,511 .PERM => return error.AccessDenied,
375 .ROFS => return error.ReadOnlyFileSystem,512 .ROFS => return error.ReadOnlyFileSystem,
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