From 2e4482cd14d6af958c2e16d7db4d6cf73bd043b8 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 26 Mar 2026 21:45:52 -0700 Subject: [PATCH] std.Io.Dir: add resolve_beneath flag and implement for freebsd,macos --- lib/std/Io/Dir.zig | 16 ++++++-- lib/std/Io/Threaded.zig | 82 +++++++++++++++++++++-------------------- lib/std/c.zig | 3 +- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 595020c561..68b432c38e 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -549,6 +549,10 @@ pub const OpenFileOptions = struct { /// controlling TTY for the current process. allow_ctty: bool = false, follow_symlinks: bool = true, + /// If supported by the operating system, attempted path resolution that + /// would escape the directory instead returns `error.AccessDenied`. If + /// unsupported, this option is ignored. + resolve_beneath: bool = false, pub const Mode = enum { read_only, write_only, read_write }; @@ -570,13 +574,13 @@ pub const OpenFileOptions = struct { /// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On WASI, `sub_path` should be encoded as valid UTF-8. /// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: OpenFileOptions) File.OpenError!File { - return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags); +pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, options: OpenFileOptions) File.OpenError!File { + return io.vtable.dirOpenFile(io.userdata, dir, sub_path, options); } -pub fn openFileAbsolute(io: Io, absolute_path: []const u8, flags: OpenFileOptions) File.OpenError!File { +pub fn openFileAbsolute(io: Io, absolute_path: []const u8, options: OpenFileOptions) File.OpenError!File { assert(path.isAbsolute(absolute_path)); - return openFile(.cwd(), io, absolute_path, flags); + return openFile(.cwd(), io, absolute_path, options); } pub const CreateFileOptions = struct { @@ -618,6 +622,10 @@ pub const CreateFileOptions = struct { /// is available to proceed. lock_nonblocking: bool = false, permissions: Permissions = .default_file, + /// If supported by the operating system, attempted path resolution that + /// would escape the directory instead returns `error.AccessDenied`. If + /// unsupported, this option is ignored. + resolve_beneath: bool = false, }; /// Creates, opens, or overwrites a file with write access. diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 9f3313e501..24572241f3 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -4217,7 +4217,7 @@ fn dirCreateFilePosix( userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, - flags: Dir.CreateFileOptions, + options: Dir.CreateFileOptions, ) File.OpenError!File { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; @@ -4225,34 +4225,35 @@ fn dirCreateFilePosix( var path_buffer: [posix.PATH_MAX]u8 = undefined; const sub_path_posix = try pathToPosix(sub_path, &path_buffer); - var os_flags: posix.O = .{ - .ACCMODE = if (flags.read) .RDWR else .WRONLY, + var flags: posix.O = .{ + .ACCMODE = if (options.read) .RDWR else .WRONLY, .CREAT = true, - .TRUNC = flags.truncate, - .EXCL = flags.exclusive, + .TRUNC = options.truncate, + .EXCL = options.exclusive, }; - if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true; - if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true; + if (@hasField(posix.O, "LARGEFILE")) flags.LARGEFILE = true; + if (@hasField(posix.O, "CLOEXEC")) flags.CLOEXEC = true; + if (@hasField(posix.O, "RESOLVE_BENEATH")) flags.RESOLVE_BENEATH = options.resolve_beneath; // Use the O locking flags if the os supports them to acquire the lock // atomically. Note that the NONBLOCK flag is removed after the openat() // call is successful. - if (have_flock_open_flags) switch (flags.lock) { + if (have_flock_open_flags) switch (options.lock) { .none => {}, .shared => { - os_flags.SHLOCK = true; - os_flags.NONBLOCK = flags.lock_nonblocking; + flags.SHLOCK = true; + flags.NONBLOCK = options.lock_nonblocking; }, .exclusive => { - os_flags.EXLOCK = true; - os_flags.NONBLOCK = flags.lock_nonblocking; + flags.EXLOCK = true; + flags.NONBLOCK = options.lock_nonblocking; }, }; const fd: posix.fd_t = fd: { const syscall: Syscall = try .start(); while (true) { - const rc = openat_sym(dir.handle, sub_path_posix, os_flags, flags.permissions.toMode()); + const rc = openat_sym(dir.handle, sub_path_posix, flags, options.permissions.toMode()); switch (posix.errno(rc)) { .SUCCESS => { syscall.finish(); @@ -4298,9 +4299,9 @@ fn dirCreateFilePosix( }; errdefer closeFd(fd); - if (have_flock and !have_flock_open_flags and flags.lock != .none) { - const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; - const lock_flags = switch (flags.lock) { + if (have_flock and !have_flock_open_flags and options.lock != .none) { + const lock_nonblocking: i32 = if (options.lock_nonblocking) posix.LOCK.NB else 0; + const lock_flags = switch (options.lock) { .none => unreachable, .shared => posix.LOCK.SH | lock_nonblocking, .exclusive => posix.LOCK.EX | lock_nonblocking, @@ -4332,7 +4333,7 @@ fn dirCreateFilePosix( } } - if (have_flock_open_flags and flags.lock_nonblocking) { + if (have_flock_open_flags and options.lock_nonblocking) { var fl_flags: usize = fl: { const syscall: Syscall = try .start(); while (true) { @@ -4785,45 +4786,46 @@ fn dirOpenFilePosix( userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, - flags: Dir.OpenFileOptions, + options: Dir.OpenFileOptions, ) File.OpenError!File { const t: *Threaded = @ptrCast(@alignCast(userdata)); var path_buffer: [posix.PATH_MAX]u8 = undefined; const sub_path_posix = try pathToPosix(sub_path, &path_buffer); - var os_flags: posix.O = switch (native_os) { + var flags: posix.O = switch (native_os) { .wasi => .{ - .read = flags.mode != .write_only, - .write = flags.mode != .read_only, - .NOFOLLOW = !flags.follow_symlinks, + .read = options.mode != .write_only, + .write = options.mode != .read_only, + .NOFOLLOW = !options.follow_symlinks, }, else => .{ - .ACCMODE = switch (flags.mode) { + .ACCMODE = switch (options.mode) { .read_only => .RDONLY, .write_only => .WRONLY, .read_write => .RDWR, }, - .NOFOLLOW = !flags.follow_symlinks, + .NOFOLLOW = !options.follow_symlinks, }, }; - if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true; - if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true; - if (@hasField(posix.O, "NOCTTY")) os_flags.NOCTTY = !flags.allow_ctty; - if (@hasField(posix.O, "PATH") and flags.path_only) os_flags.PATH = true; + if (@hasField(posix.O, "CLOEXEC")) flags.CLOEXEC = true; + if (@hasField(posix.O, "LARGEFILE")) flags.LARGEFILE = true; + if (@hasField(posix.O, "NOCTTY")) flags.NOCTTY = !options.allow_ctty; + if (@hasField(posix.O, "PATH")) flags.PATH = options.path_only; + if (@hasField(posix.O, "RESOLVE_BENEATH")) flags.RESOLVE_BENEATH = options.resolve_beneath; - // Use the O locking flags if the os supports them to acquire the lock + // Use the O locking options if the os supports them to acquire the lock // atomically. Note that the NONBLOCK flag is removed after the openat() // call is successful. - if (have_flock_open_flags) switch (flags.lock) { + if (have_flock_open_flags) switch (options.lock) { .none => {}, .shared => { - os_flags.SHLOCK = true; - os_flags.NONBLOCK = flags.lock_nonblocking; + flags.SHLOCK = true; + flags.NONBLOCK = options.lock_nonblocking; }, .exclusive => { - os_flags.EXLOCK = true; - os_flags.NONBLOCK = flags.lock_nonblocking; + flags.EXLOCK = true; + flags.NONBLOCK = options.lock_nonblocking; }, }; @@ -4832,7 +4834,7 @@ fn dirOpenFilePosix( const fd: posix.fd_t = fd: { const syscall: Syscall = try .start(); while (true) { - const rc = openat_sym(dir.handle, sub_path_posix, os_flags, mode); + const rc = openat_sym(dir.handle, sub_path_posix, flags, mode); switch (posix.errno(rc)) { .SUCCESS => { syscall.finish(); @@ -4878,7 +4880,7 @@ fn dirOpenFilePosix( }; errdefer closeFd(fd); - if (!flags.allow_directory) { + if (!options.allow_directory) { const is_dir = is_dir: { const stat = fileStat(t, .{ .handle = fd, @@ -4893,9 +4895,9 @@ fn dirOpenFilePosix( if (is_dir) return error.IsDir; } - if (have_flock and !have_flock_open_flags and flags.lock != .none) { - const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; - const lock_flags = switch (flags.lock) { + if (have_flock and !have_flock_open_flags and options.lock != .none) { + const lock_nonblocking: i32 = if (options.lock_nonblocking) posix.LOCK.NB else 0; + const lock_flags = switch (options.lock) { .none => unreachable, .shared => posix.LOCK.SH | lock_nonblocking, .exclusive => posix.LOCK.EX | lock_nonblocking, @@ -4926,7 +4928,7 @@ fn dirOpenFilePosix( } } - if (have_flock_open_flags and flags.lock_nonblocking) { + if (have_flock_open_flags and options.lock_nonblocking) { var fl_flags: usize = fl: { const syscall: Syscall = try .start(); while (true) { diff --git a/lib/std/c.zig b/lib/std/c.zig index 73c9ef0b13..90efa67884 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -8535,7 +8535,8 @@ pub const O = switch (native_os) { CREAT: bool = false, TRUNC: bool = false, EXCL: bool = false, - _12: u3 = 0, + RESOLVE_BENEATH: bool = false, + _13: u2 = 0, EVTONLY: bool = false, _16: u1 = 0, NOCTTY: bool = false,