| ... | ... | @@ -348,17 +348,58 @@ 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 | |
| 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 | }; |
| 353 | 370 | |
| 354 | | pub fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void { |
| 371 | var has_fchmodat2_syscall = std.atomic.Value(bool).init(true); |
| 372 | |
| 373 | inline 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. |
| 394 | pub inline fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtError!void { |
| 355 | 395 | if (!std.fs.has_executable_bit) @compileError("fchmodat unsupported by target OS"); |
| 356 | 396 | |
| 357 | 397 | const path_c = try toPosixPath(path); |
| 358 | 398 | |
| 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 | 402 | const res = system.fchmodat(dirfd, &path_c, mode, flags); |
| 361 | | |
| 362 | 403 | switch (system.getErrno(res)) { |
| 363 | 404 | .SUCCESS => return, |
| 364 | 405 | .INTR => continue, |
| ... | ... | @@ -368,8 +409,104 @@ pub fn fchmodat(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodA |
| 368 | 409 | .ACCES => return error.AccessDenied, |
| 369 | 410 | .IO => return error.InputOutput, |
| 370 | 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 | 478 | .NOENT => return error.FileNotFound, |
| 372 | 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 | 510 | .NOTDIR => return error.FileNotFound, |
| 374 | 511 | .PERM => return error.AccessDenied, |
| 375 | 512 | .ROFS => return error.ReadOnlyFileSystem, |