From 5a7dc4b0fae62c8b7ec798043c93e4d90704a638 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 14:41:31 -0800 Subject: [PATCH 01/19] std.Io: introduce File.MemoryMap by defining the pointer contents to only be synchronized after explicit sync points, makes it legal to have a fallback implementation based on file operations while still supporting a handful of use cases for memory mapping. furthermore, it makes it legal for evented I/O implementations to use evented file I/O for the sync points rather than memory mapping. not yet done: - implement checking the length when options.len is null - some windows impl work - some wasi impl work - unit tests - integration with compiler --- lib/std/Io.zig | 7 + lib/std/Io/File.zig | 49 +++- lib/std/Io/File/MemoryMap.zig | 79 +++++ lib/std/Io/File/Reader.zig | 4 +- lib/std/Io/Threaded.zig | 522 +++++++++++++++++++++++++++++----- lib/std/posix.zig | 2 +- lib/std/process.zig | 15 +- lib/std/zig/system.zig | 1 - src/link/MappedFile.zig | 1 - 9 files changed, 597 insertions(+), 83 deletions(-) create mode 100644 lib/std/Io/File/MemoryMap.zig diff --git a/lib/std/Io.zig b/lib/std/Io.zig index b4d6efc715f163e0b59300d355284423d8b33382..1da10a027fa172a208228600d7558378509d2094 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -9,6 +9,7 @@ //! * concurrent queues //! * wait groups and select //! * mutexes, futexes, events, and conditions +//! * memory mapped files //! This interface allows programmers to write optimal, reusable code while //! participating in these operations. const Io = @This(); @@ -653,6 +654,12 @@ pub const VTable = struct { fileRealPath: *const fn (?*anyopaque, File, out_buffer: []u8) File.RealPathError!usize, fileHardLink: *const fn (?*anyopaque, File, Dir, []const u8, File.HardLinkOptions) File.HardLinkError!void, + fileMemoryMapCreate: *const fn (?*anyopaque, File, File.MemoryMap.CreateOptions) File.MemoryMap.CreateError!File.MemoryMap, + fileMemoryMapDestroy: *const fn (?*anyopaque, *File.MemoryMap) void, + fileMemoryMapSetLength: *const fn (?*anyopaque, *File.MemoryMap, n: usize) File.MemoryMap.SetLengthError!void, + fileMemoryMapRead: *const fn (?*anyopaque, *File.MemoryMap) File.ReadPositionalError!void, + fileMemoryMapWrite: *const fn (?*anyopaque, *File.MemoryMap) File.WritePositionalError!void, + processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File, processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize, lockStderr: *const fn (?*anyopaque, ?Terminal.Mode) Cancelable!LockedStderr, diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 27833d4d8a5bada35d695b50c25abeac2f54c446..e622a6dd3cd457aa8351757f5c8729b4d09416b0 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -14,6 +14,8 @@ handle: Handle, pub const Reader = @import("File/Reader.zig"); pub const Writer = @import("File/Writer.zig"); pub const Atomic = @import("File/Atomic.zig"); +/// Memory intended to remain consistent with file contents. +pub const MemoryMap = @import("File/MemoryMap.zig"); pub const Handle = std.posix.fd_t; pub const INode = std.posix.ino_t; @@ -529,7 +531,25 @@ pub fn readStreaming(file: File, io: Io, buffer: []const []u8) Reader.Error!usiz return io.vtable.fileReadStreaming(io.userdata, file, buffer); } -pub const ReadPositionalError = Reader.Error || error{Unseekable}; +pub const ReadPositionalError = error{ + InputOutput, + SystemResources, + /// Trying to read a directory file descriptor as if it were a file. + IsDir, + BrokenPipe, + /// Non-blocking has been enabled, and reading from the file descriptor + /// would block. + WouldBlock, + /// In WASI, this error occurs when the file descriptor does + /// not hold the required rights to read from it. + AccessDenied, + /// Unable to read file due to lock. Depending on the `Io` implementation, + /// reading from a locked file may return this error, or may ignore the + /// lock. + LockViolation, + /// This file cannot be read positionally. + Unseekable, +} || Io.Cancelable || Io.UnexpectedError; /// Returns 0 on stream end or if `buffer` has no space available for data. /// @@ -539,7 +559,31 @@ pub fn readPositional(file: File, io: Io, buffer: []const []u8, offset: u64) Rea return io.vtable.fileReadPositional(io.userdata, file, buffer, offset); } -pub const WritePositionalError = Writer.Error || error{Unseekable}; +pub const WritePositionalError = error{ + DiskQuota, + FileTooBig, + InputOutput, + NoSpaceLeft, + DeviceBusy, + /// File descriptor does not hold the required rights to write to it. + AccessDenied, + PermissionDenied, + /// File is an unconnected socket, or closed its read end. + BrokenPipe, + /// Insufficient kernel memory to read from in_fd. + SystemResources, + /// The process cannot access the file because another process has locked + /// a portion of the file. Windows-only. + LockViolation, + /// Non-blocking has been enabled and this operation would block. + WouldBlock, + /// This error occurs when a device gets disconnected before or mid-flush + /// while it's being written to - errno(6): No such device or address. + NoDevice, + FileBusy, + /// This file cannot be written positionally. + Unseekable, +} || Io.Cancelable || Io.UnexpectedError; /// See also: /// * `writer` @@ -744,4 +788,5 @@ test { _ = Reader; _ = Writer; _ = Atomic; + _ = MemoryMap; } diff --git a/lib/std/Io/File/MemoryMap.zig b/lib/std/Io/File/MemoryMap.zig new file mode 100644 index 0000000000000000000000000000000000000000..848a8837d12b8553c0189b4cf9e4fbd23fd644c3 --- /dev/null +++ b/lib/std/Io/File/MemoryMap.zig @@ -0,0 +1,79 @@ +const MemoryMap = @This(); + +const builtin = @import("builtin"); +const native_os = builtin.os.tag; +const is_windows = native_os == .windows; + +const std = @import("../../std.zig"); +const Io = std.Io; +const File = Io.File; +const Allocator = std.mem.Allocator; + +file: File, +/// Byte index inside `file` where `memory` starts. +offset: usize, +/// Memory that may or may not remain consistent with file contents. Use `read` +/// and `write` to ensure synchronization points. +memory: []u8, +/// Tells whether it is memory-mapped or file operations. On Windows this also +/// has a section handle. +section: ?Section, + +pub const Section = if (is_windows) std.os.windows.HANDLE else void; + +pub const CreateError = error{ + /// A file descriptor refers to a non-regular file. Or a file mapping was requested, + /// but the file descriptor is not open for reading. Or `MAP.SHARED` was requested + /// and `PROT_WRITE` is set, but the file descriptor is not open in `RDWR` mode. + /// Or `PROT_WRITE` is set, but the file is append-only. + AccessDenied, + /// The `prot` argument asks for `PROT_EXEC` but the mapped area belongs to a file on + /// a filesystem that was mounted no-exec. + PermissionDenied, + LockedMemoryLimitExceeded, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, +} || Allocator.Error || File.ReadPositionalError; + +pub const CreateOptions = struct { + protection: std.process.MemoryProtection = .{ .read = true, .write = true }, + populate: bool = true, + /// Byte index of file to start from. + offset: u64 = 0, + /// `null` indicates to map the entire file. If mapping the entire file is + /// desired and the file size is known, it is more efficient to populate + /// the value here. + len: ?usize = null, +}; + +pub fn create(io: Io, file: File, options: CreateOptions) CreateError!MemoryMap { + return io.vtable.fileMemoryMapCreate(io.userdata, file, options); +} + +/// If `write` is not called before this function, changes to `memory` may or may +/// not be synchronized to `file`. +pub fn destroy(mm: *MemoryMap, io: Io) void { + io.vtable.fileMemoryMapDestroy(io.userdata, mm); +} + +pub const SetLengthError = error{ + LockedMemoryLimitExceeded, +} || Allocator.Error || File.SetLengthError; + +/// Change the size of the mapping. This does not sync the contents. The size +/// of the file after calling this is unspecified until `write` is called. +/// +/// May change the pointer address of `memory`. +pub fn setLength(mm: *MemoryMap, io: Io, n: usize) File.SetLengthError!void { + return io.vtable.fileMemoryMapSetLength(io.userdata, mm, n); +} + +/// Synchronizes the contents of `memory` from `file`. +pub fn read(mm: *MemoryMap, io: Io) File.ReadPositionalError!void { + return io.vtable.fileMemoryMapRead(io.userdata, mm); +} + +/// Synchronizes the contents of `memory` to `file`. +pub fn write(mm: *MemoryMap, io: Io) File.WritePositionalError!void { + return io.vtable.fileMemoryMapWrite(io.userdata, mm); +} diff --git a/lib/std/Io/File/Reader.zig b/lib/std/Io/File/Reader.zig index 0c573c9ae1423ef9ebe8d1fee5e26d477cfec008..c2842ee45ffbbeb55fd898fa34d4a4f58d7cc869 100644 --- a/lib/std/Io/File/Reader.zig +++ b/lib/std/Io/File/Reader.zig @@ -29,13 +29,11 @@ interface: Io.Reader, pub const Error = error{ InputOutput, SystemResources, + /// Trying to read a directory file descriptor as if it were a file. IsDir, BrokenPipe, ConnectionResetByPeer, Timeout, - /// In WASI, EBADF is mapped to this error because it is returned when - /// trying to read a directory file descriptor as if it were a file. - NotOpenForReading, SocketUnconnected, /// Non-blocking has been enabled, and reading from the file descriptor /// would block. diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 735efa9992d279b8cffeb63cc2abe6ee4a2e70d2..09d69682cee9c03c09a7d76638481ca6b2d35ed0 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -22,6 +22,12 @@ const windows = std.os.windows; const ws2_32 = std.os.windows.ws2_32; /// Thread-safe. +/// +/// Used for: +/// * allocating `Io.Future` and `Io.Group` closures. +/// * formatting spawning child processes +/// * scanning environment variables on some targets +/// * memory-mapping when mmap or equivalent is not available allocator: Allocator, mutex: std.Thread.Mutex = .{}, cond: std.Thread.Condition = .{}, @@ -1490,6 +1496,12 @@ pub fn io(t: *Threaded) Io { .fileRealPath = fileRealPath, .fileHardLink = fileHardLink, + .fileMemoryMapCreate = fileMemoryMapCreate, + .fileMemoryMapDestroy = fileMemoryMapDestroy, + .fileMemoryMapSetLength = fileMemoryMapSetLength, + .fileMemoryMapRead = fileMemoryMapRead, + .fileMemoryMapWrite = fileMemoryMapWrite, + .processExecutableOpen = processExecutableOpen, .processExecutablePath = processExecutablePath, .lockStderr = lockStderr, @@ -1642,6 +1654,12 @@ pub fn ioBasic(t: *Threaded) Io { .fileRealPath = fileRealPath, .fileHardLink = fileHardLink, + .fileMemoryMapCreate = fileMemoryMapCreate, + .fileMemoryMapDestroy = fileMemoryMapDestroy, + .fileMemoryMapSetLength = fileMemoryMapSetLength, + .fileMemoryMapRead = fileMemoryMapRead, + .fileMemoryMapWrite = fileMemoryMapWrite, + .processExecutableOpen = processExecutableOpen, .processExecutablePath = processExecutablePath, .lockStderr = lockStderr, @@ -1733,15 +1751,24 @@ const have_wait4 = switch (native_os) { else => false, }; +const have_mmap = switch (native_os) { + .wasi, .windows => false, + else => true, +}; + const open_sym = if (posix.lfs64_abi) posix.system.open64 else posix.system.open; const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat; const lseek_sym = if (posix.lfs64_abi) posix.system.lseek64 else posix.system.lseek; const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv; +const pread_sym = if (posix.lfs64_abi) posix.system.pread64 else posix.system.pread; const ftruncate_sym = if (posix.lfs64_abi) posix.system.ftruncate64 else posix.system.ftruncate; const pwritev_sym = if (posix.lfs64_abi) posix.system.pwritev64 else posix.system.pwritev; +const pwrite_sym = if (posix.lfs64_abi) posix.system.pwrite64 else posix.system.pwrite; const sendfile_sym = if (posix.lfs64_abi) posix.system.sendfile64 else posix.system.sendfile; +const mmap_sym = if (posix.lfs64_abi) posix.system.mmap64 else posix.system.mmap; + const linux_copy_file_range_use_c = std.c.versionCheck(if (builtin.abi.isAndroid()) .{ .major = 34, .minor = 0, @@ -8028,27 +8055,22 @@ fn fileReadPositionalPosix(userdata: ?*anyopaque, file: File, data: []const []u8 try syscall.checkCancel(); continue; }, - else => |e| { - syscall.finish(); - switch (e) { - .INVAL => |err| return errnoBug(err), - .FAULT => |err| return errnoBug(err), - .AGAIN => |err| return errnoBug(err), - .BADF => return error.NotOpenForReading, // File operation on directory. - .IO => return error.InputOutput, - .ISDIR => return error.IsDir, - .NOBUFS => return error.SystemResources, - .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketUnconnected, - .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.Timeout, - .NXIO => return error.Unseekable, - .SPIPE => return error.Unseekable, - .OVERFLOW => return error.Unseekable, - .NOTCAPABLE => return error.AccessDenied, - else => |err| return posix.unexpectedErrno(err), - } - }, + .NOTCONN => |err| return syscall.errnoBug(err), // not a socket + .CONNRESET => |err| return syscall.errnoBug(err), // not a socket + .BADF => |err| return syscall.errnoBug(err), // use after free + .INVAL => |err| return syscall.errnoBug(err), + .FAULT => |err| return syscall.errnoBug(err), // segmentation fault + .AGAIN => |err| return syscall.errnoBug(err), + .IO => return syscall.fail(error.InputOutput), + .ISDIR => return syscall.fail(error.IsDir), + .NOBUFS => return syscall.fail(error.SystemResources), + .NOMEM => return syscall.fail(error.SystemResources), + .TIMEDOUT => return syscall.fail(error.Timeout), + .NXIO => return syscall.fail(error.Unseekable), + .SPIPE => return syscall.fail(error.Unseekable), + .OVERFLOW => return syscall.fail(error.Unseekable), + .NOTCAPABLE => return syscall.fail(error.AccessDenied), + else => |err| return syscall.unexpectedErrno(err), } } } @@ -8061,33 +8083,28 @@ fn fileReadPositionalPosix(userdata: ?*anyopaque, file: File, data: []const []u8 syscall.finish(); return @bitCast(rc); }, - .INTR => { + .INTR, .TIMEDOUT => { try syscall.checkCancel(); continue; }, - else => |e| { + .NXIO => return syscall.fail(error.Unseekable), + .SPIPE => return syscall.fail(error.Unseekable), + .OVERFLOW => return syscall.fail(error.Unseekable), + .NOBUFS => return syscall.fail(error.SystemResources), + .NOMEM => return syscall.fail(error.SystemResources), + .AGAIN => return syscall.fail(error.WouldBlock), + .IO => return syscall.fail(error.InputOutput), + .ISDIR => return syscall.fail(error.IsDir), + .NOTCONN => |err| return syscall.errnoBug(err), // not a socket + .CONNRESET => |err| return syscall.errnoBug(err), // not a socket + .INVAL => |err| return syscall.errnoBug(err), + .FAULT => |err| return syscall.errnoBug(err), + .BADF => |err| { syscall.finish(); - switch (e) { - .INVAL => |err| return errnoBug(err), - .FAULT => |err| return errnoBug(err), - .AGAIN => return error.WouldBlock, - .BADF => |err| { - if (native_os == .wasi) return error.NotOpenForReading; // File operation on directory. - return errnoBug(err); // File descriptor used after closed. - }, - .IO => return error.InputOutput, - .ISDIR => return error.IsDir, - .NOBUFS => return error.SystemResources, - .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketUnconnected, - .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.Timeout, - .NXIO => return error.Unseekable, - .SPIPE => return error.Unseekable, - .OVERFLOW => return error.Unseekable, - else => |err| return posix.unexpectedErrno(err), - } + if (native_os == .wasi) return error.IsDir; // File operation on directory. + return errnoBug(err); // File descriptor used after closed. }, + else => |err| return syscall.unexpectedErrno(err), } } } @@ -8770,29 +8787,24 @@ fn fileWritePositional( try syscall.checkCancel(); continue; }, - else => |e| { - syscall.finish(); - switch (e) { - .INVAL => |err| return errnoBug(err), - .FAULT => |err| return errnoBug(err), - .AGAIN => return error.WouldBlock, - .BADF => return error.NotOpenForWriting, // Usually a race condition. - .DESTADDRREQ => |err| return errnoBug(err), // `connect` was never called. - .DQUOT => return error.DiskQuota, - .FBIG => return error.FileTooBig, - .IO => return error.InputOutput, - .NOSPC => return error.NoSpaceLeft, - .PERM => return error.PermissionDenied, - .PIPE => return error.BrokenPipe, - .CONNRESET => |err| return errnoBug(err), // Not a socket handle. - .BUSY => return error.DeviceBusy, - .TXTBSY => return error.FileBusy, - .NXIO => return error.Unseekable, - .SPIPE => return error.Unseekable, - .OVERFLOW => return error.Unseekable, - else => |err| return posix.unexpectedErrno(err), - } - }, + .INVAL => |err| return syscall.errnoBug(err), + .FAULT => |err| return syscall.errnoBug(err), + .DESTADDRREQ => |err| return syscall.errnoBug(err), // `connect` was never called. + .CONNRESET => |err| return syscall.errnoBug(err), // Not a socket handle. + .BADF => |err| return syscall.errnoBug(err), // use after free + .AGAIN => return syscall.fail(error.WouldBlock), + .DQUOT => return syscall.fail(error.DiskQuota), + .FBIG => return syscall.fail(error.FileTooBig), + .IO => return syscall.fail(error.InputOutput), + .NOSPC => return syscall.fail(error.NoSpaceLeft), + .PERM => return syscall.fail(error.PermissionDenied), + .PIPE => return syscall.fail(error.BrokenPipe), + .BUSY => return syscall.fail(error.DeviceBusy), + .TXTBSY => return syscall.fail(error.FileBusy), + .NXIO => return syscall.fail(error.Unseekable), + .SPIPE => return syscall.fail(error.Unseekable), + .OVERFLOW => return syscall.fail(error.Unseekable), + else => |err| return syscall.unexpectedErrno(err), } } } @@ -16107,3 +16119,381 @@ pub fn chdir(dir_path: []const u8) ChdirError!void { else => |err| return syscall.unexpectedErrno(err), }; } + +fn fileMemoryMapCreate( + userdata: ?*anyopaque, + file: File, + options: File.MemoryMap.CreateOptions, +) File.MemoryMap.CreateError!File.MemoryMap { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const offset = options.offset; + + const page_size = std.heap.pageSize(); + const aligned_len: usize = options.len.?; // TODO query if necessary + + if (createFileMap(file, options.protection, offset, options.populate, aligned_len)) |result| { + return result; + } else |err| switch (err) { + error.Unseekable, error.Canceled => |e| return e, + else => { + if (builtin.mode == .Debug) + std.log.warn("memory mapping failed with {t}, falling back to file operations", .{err}); + }, + } + + const gpa = t.allocator; + const alignment: Alignment = .fromByteUnits(page_size); + const memory = m: { + const ptr = gpa.rawAlloc(aligned_len, alignment, @returnAddress()) orelse + return error.OutOfMemory; + break :m ptr[0..aligned_len]; + }; + errdefer gpa.rawFree(memory, alignment, @returnAddress()); + + // If the mapping does not have read permissions, no need to populate the contents. + if (options.protection.read) try mmSyncRead(file, memory, offset); + + return .{ + .file = file, + .offset = offset, + .memory = memory, + .section = null, + }; +} + +const CreateFileMapError = error{ + /// MaximumSize is greater than the system-defined maximum for sections, or + /// greater than the specified file and the section is not writable. + SectionOversize, + /// A file descriptor refers to a non-regular file. Or a file mapping was requested, + /// but the file descriptor is not open for reading. Or `MAP.SHARED` was requested + /// and `PROT_WRITE` is set, but the file descriptor is not open in `RDWR` mode. + /// Or `PROT_WRITE` is set, but the file is append-only. + AccessDenied, + /// The `prot` argument asks for `PROT_EXEC` but the mapped area belongs to a file on + /// a filesystem that was mounted no-exec. + PermissionDenied, + FileBusy, + LockedMemoryLimitExceeded, + OperationUnsupported, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + OutOfMemory, + MappingAlreadyExists, + Unseekable, +} || Io.Cancelable || Io.UnexpectedError; + +fn createFileMap( + file: File, + protection: std.process.MemoryProtection, + offset: usize, + populate: bool, + aligned_len: usize, +) CreateFileMapError!File.MemoryMap { + if (is_windows) { + try Thread.checkCancel(); + + var section = windows.INVALID_HANDLE_VALUE; + switch (windows.ntdll.NtCreateSection( + §ion, + .{ + .SPECIFIC = .{ .SECTION = .{ + .QUERY = true, + .MAP_WRITE = protection.write, + .MAP_READ = protection.read, + .MAP_EXECUTE = protection.execute, + .EXTEND_SIZE = true, + } }, + .STANDARD = .{ .RIGHTS = .REQUIRED }, + }, + null, + @constCast(&@as(i64, @intCast(aligned_len))), + .{ .READWRITE = true }, + .{ .COMMIT = populate }, + file.handle, + )) { + .SUCCESS => {}, + .FILE_LOCK_CONFLICT => return error.FileLocked, + .INVALID_FILE_FOR_SECTION => return error.OperationUnsupported, + else => |status| return windows.unexpectedStatus(status), + } + const current_process: windows.HANDLE = @ptrFromInt(@as(usize, @bitCast(@as(isize, -1)))); + var contents_ptr: ?[*]u8 = null; + var contents_len = aligned_len; + switch (windows.ntdll.NtMapViewOfSection( + section, + current_process, + @ptrCast(&contents_ptr), + null, + 0, + null, + &contents_len, + .Unmap, + .{}, + .{ .READWRITE = true }, + )) { + .SUCCESS => {}, + .CONFLICTING_ADDRESSES => return error.MappingAlreadyExists, + .SECTION_PROTECTION => return error.PermissionDenied, + else => |status| return windows.unexpectedStatus(status), + } + return .{ + .file = file, + .offset = offset, + .memory = contents_ptr.?[0..contents_len], + .section = section, + }; + } else if (have_mmap) { + const prot: posix.PROT = .{ + .READ = protection.read, + .WRITE = protection.write, + .EXEC = protection.execute, + }; + const flags: posix.MAP = .{ + .TYPE = if (native_os == .linux) .SHARED_VALIDATE else .SHARED, + .POPULATE = populate, + }; + + const contents = while (true) { + const syscall: Syscall = try .start(); + const casted_offset = std.math.cast(i64, offset) orelse return error.Unseekable; + const rc = mmap_sym(null, aligned_len, prot, flags, file.handle, casted_offset); + syscall.finish(); + const err: posix.E = if (builtin.link_libc) e: { + if (rc != std.c.MAP_FAILED) { + break @as([*]u8, @ptrCast(@alignCast(rc)))[0..aligned_len]; + } + break :e @enumFromInt(posix.system._errno().*); + } else e: { + const err = posix.errno(rc); + if (err == .SUCCESS) { + break @as([*]u8, @ptrFromInt(rc))[0..aligned_len]; + } + break :e err; + }; + switch (err) { + .SUCCESS => unreachable, + .INTR => continue, + .ACCES => return error.AccessDenied, + .AGAIN => return error.LockedMemoryLimitExceeded, + .EXIST => return error.MappingAlreadyExists, + .MFILE => return error.ProcessFdQuotaExceeded, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.OperationUnsupported, + .NOMEM => return error.OutOfMemory, + .PERM => return error.PermissionDenied, + .TXTBSY => return error.FileBusy, + .OVERFLOW => return error.Unseekable, + .BADF => return errnoBug(err), // Always a race condition. + .INVAL => return errnoBug(err), // Invalid parameters to mmap() + else => return posix.unexpectedErrno(err), + } + }; + return .{ + .file = file, + .offset = offset, + .memory = contents, + .section = {}, + }; + } + + return error.OperationUnsupported; +} + +fn fileMemoryMapDestroy(userdata: ?*anyopaque, mm: *File.MemoryMap) void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const memory = mm.memory; + if (mm.section) |section| { + if (is_windows) { + const current_process: windows.HANDLE = @ptrFromInt(@as(usize, @bitCast(@as(isize, -1)))); + _ = windows.ntdll.NtUnmapViewOfSection(current_process, memory.ptr); + windows.CloseHandle(section); + } else { + switch (posix.errno(posix.system.munmap(memory.ptr, memory.len))) { + .SUCCESS => {}, + else => |e| { + if (builtin.mode == .Debug) + std.log.err("failed to unmap {d} bytes at {*}: {t}", .{ memory.len, memory.ptr, e }); + }, + } + } + } else { + const gpa = t.allocator; + const page_size = std.heap.pageSize(); + const alignment: Alignment = .fromByteUnits(page_size); + gpa.rawFree(memory, alignment, @returnAddress()); + } + mm.* = undefined; +} + +fn fileMemoryMapSetLength( + userdata: ?*anyopaque, + mm: *File.MemoryMap, + new_len: usize, +) File.MemoryMap.SetLengthError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + if (mm.section) |section| switch (native_os) { + .windows => { + _ = section; + @panic("TODO"); + }, + .wasi => unreachable, + else => { + const flags: posix.MREMAP = .{ .MAYMOVE = true }; + const addr_hint: ?[*]const u8 = null; + const new_memory = while (true) { + const syscall: Syscall = try .start(); + const rc = posix.system.mremap(mm.memory.ptr, mm.memory.len, new_len, flags, addr_hint); + syscall.finish(); + const err: posix.E = if (builtin.link_libc) e: { + if (rc != std.c.MAP_FAILED) break @as([*]u8, @ptrCast(@alignCast(rc)))[0..new_len]; + break :e @enumFromInt(posix.system._errno().*); + } else e: { + const err = posix.errno(rc); + if (err == .SUCCESS) break @as([*]u8, @ptrFromInt(rc))[0..new_len]; + break :e err; + }; + switch (err) { + .SUCCESS => unreachable, + .INTR => continue, + .AGAIN => return error.LockedMemoryLimitExceeded, + .NOMEM => return error.OutOfMemory, + .INVAL => return errnoBug(err), + .FAULT => return errnoBug(err), + else => return posix.unexpectedErrno(err), + } + }; + mm.memory = new_memory; + }, + } else { + const gpa = t.allocator; + const page_size = std.heap.pageSize(); + const alignment: Alignment = .fromByteUnits(page_size); + if (gpa.rawRemap(mm.memory, alignment, new_len, @returnAddress())) |new_ptr| { + mm.memory = new_ptr[0..new_len]; + } else { + const new_ptr = gpa.rawAlloc(new_len, alignment, @returnAddress()) orelse + return error.OutOfMemory; + const copy_len = @min(new_len, mm.memory.len); + @memcpy(new_ptr[0..copy_len], mm.memory[0..copy_len]); + mm.memory = new_ptr[0..new_len]; + } + } +} + +fn fileMemoryMapRead(userdata: ?*anyopaque, mm: *File.MemoryMap) File.ReadPositionalError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + if (mm.section != null) return; + return mmSyncRead(mm.file, mm.memory, mm.offset); +} + +fn fileMemoryMapWrite(userdata: ?*anyopaque, mm: *File.MemoryMap) File.WritePositionalError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + if (mm.section != null) return; + return mmSyncWrite(mm.file, mm.memory, mm.offset); +} + +fn mmSyncRead(file: File, memory: []u8, offset: u64) File.ReadPositionalError!void { + switch (native_os) { + .windows => @panic("TODO"), + .wasi => @panic("TODO"), + else => { + var i: usize = 0; + const syscall: Syscall = try .start(); + while (true) { + const buf = memory[i..]; + if (buf.len == 0) { + syscall.finish(); + break; + } + const rc = pread_sym(file.handle, buf.ptr, buf.len, @intCast(offset + i)); + switch (posix.errno(rc)) { + .SUCCESS => { + const n: usize = @intCast(rc); + if (n == 0) { + syscall.finish(); + @memset(memory[i..], 0); + break; + } + i += n; + try syscall.checkCancel(); + continue; + }, + .INTR, .TIMEDOUT => { + try syscall.checkCancel(); + continue; + }, + .NXIO => return syscall.fail(error.Unseekable), + .SPIPE => return syscall.fail(error.Unseekable), + .OVERFLOW => return syscall.fail(error.Unseekable), + .NOBUFS => return syscall.fail(error.SystemResources), + .NOMEM => return syscall.fail(error.SystemResources), + .AGAIN => return syscall.fail(error.WouldBlock), + .IO => return syscall.fail(error.InputOutput), + .ISDIR => return syscall.fail(error.IsDir), + .NOTCONN => |err| return syscall.errnoBug(err), // not a socket + .CONNRESET => |err| return syscall.errnoBug(err), // not a socket + .INVAL => |err| return syscall.errnoBug(err), + .FAULT => |err| return syscall.errnoBug(err), + .BADF => |err| { + syscall.finish(); + if (native_os == .wasi) return error.IsDir; // File operation on directory. + return errnoBug(err); // File descriptor used after closed. + }, + else => |err| return syscall.unexpectedErrno(err), + } + } + }, + } +} + +fn mmSyncWrite(file: File, memory: []u8, offset: u64) File.WritePositionalError!void { + switch (native_os) { + .windows => @panic("TODO"), + .wasi => @panic("TODO"), + else => { + var i: usize = 0; + const syscall: Syscall = try .start(); + while (true) { + const buf = memory[i..]; + if (buf.len == 0) { + syscall.finish(); + break; + } + const rc = pwrite_sym(file.handle, buf.ptr, buf.len, @intCast(offset)); + switch (posix.errno(rc)) { + .SUCCESS => { + const n: usize = @bitCast(rc); + i += n; + try syscall.checkCancel(); + continue; + }, + .INTR => { + try syscall.checkCancel(); + continue; + }, + .INVAL => |err| return syscall.errnoBug(err), + .FAULT => |err| return syscall.errnoBug(err), + .DESTADDRREQ => |err| return syscall.errnoBug(err), // not a socket + .CONNRESET => |err| return syscall.errnoBug(err), // not a socket + .BADF => |err| return syscall.errnoBug(err), // use after free + .AGAIN => return syscall.fail(error.WouldBlock), + .DQUOT => return syscall.fail(error.DiskQuota), + .FBIG => return syscall.fail(error.FileTooBig), + .IO => return syscall.fail(error.InputOutput), + .NOSPC => return syscall.fail(error.NoSpaceLeft), + .PERM => return syscall.fail(error.PermissionDenied), + .PIPE => return syscall.fail(error.BrokenPipe), + .BUSY => return syscall.fail(error.DeviceBusy), + .TXTBSY => return syscall.fail(error.FileBusy), + .NXIO => return syscall.fail(error.Unseekable), + .SPIPE => return syscall.fail(error.Unseekable), + .OVERFLOW => return syscall.fail(error.Unseekable), + else => |err| return syscall.unexpectedErrno(err), + } + } + }, + } +} diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 93e711238a68764b9e0719c90be00e356fa21887..b607cc813440f9d0e9d762946965c363071d3ffa 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -433,7 +433,7 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize { .FAULT => unreachable, .AGAIN => return error.WouldBlock, .CANCELED => return error.Canceled, - .BADF => return error.NotOpenForReading, // Can be a race condition. + .BADF => return error.Unexpected, // use after free .IO => return error.InputOutput, .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, diff --git a/lib/std/process.zig b/lib/std/process.zig index 64badc1cba33a1dbf06ec6ce711167fa58ba2b24..3f085037a30b5761687ba0195b96791f38049736 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -1018,22 +1018,19 @@ pub const ProtectMemoryError = error{ OutOfMemory, } || Io.UnexpectedError; -pub const ProtectMemoryOptions = packed struct(u3) { +pub const MemoryProtection = packed struct(u3) { read: bool = false, write: bool = false, execute: bool = false, }; -pub fn protectMemory( - memory: []align(std.heap.page_size_min) u8, - options: ProtectMemoryOptions, -) ProtectMemoryError!void { +pub fn protectMemory(memory: []align(std.heap.page_size_min) u8, protection: MemoryProtection) ProtectMemoryError!void { if (native_os == .windows) { var addr = memory.ptr; // ntdll takes an extra level of indirection here var size = memory.len; // ntdll takes an extra level of indirection here var old: windows.PAGE = undefined; const current_process: windows.HANDLE = @ptrFromInt(@as(usize, @bitCast(@as(isize, -1)))); - const new: windows.PAGE = switch (@as(u3, @bitCast(options))) { + const new: windows.PAGE = switch (@as(u3, @bitCast(protection))) { 0b000 => .{ .NOACCESS = true }, 0b001 => .{ .READONLY = true }, 0b010 => return error.AccessDenied, // +w -r not allowed @@ -1050,9 +1047,9 @@ pub fn protectMemory( } } else if (posix.PROT != void) { const flags: posix.PROT = .{ - .READ = options.read, - .WRITE = options.write, - .EXEC = options.execute, + .READ = protection.read, + .WRITE = protection.write, + .EXEC = protection.execute, }; switch (posix.errno(posix.system.mprotect(memory.ptr, memory.len, flags))) { .SUCCESS => return, diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 45e777bb939677b02d18ace2ddcbb2abd7e29e9d..d0cd0721bbff3218818bce17b3e07a561e1c3bf8 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -421,7 +421,6 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target { error.BrokenPipe => return error.Unexpected, error.ConnectionResetByPeer => return error.Unexpected, error.Timeout => return error.Unexpected, - error.NotOpenForReading => return error.Unexpected, error.SocketUnconnected => return error.Unexpected, error.AccessDenied, diff --git a/src/link/MappedFile.zig b/src/link/MappedFile.zig index c6e6535961f5e9abdf4bb08c32720dabdf92c354..2580ed5c2a36cee4d9cc63c35f3843f2b8ba881d 100644 --- a/src/link/MappedFile.zig +++ b/src/link/MappedFile.zig @@ -1,4 +1,3 @@ -/// TODO add a mapped file abstraction to std.Io const MappedFile = @This(); const builtin = @import("builtin"); -- 2.54.0 From a70e0061579a4365da0092554470ffeadb4594a4 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 15:11:37 -0800 Subject: [PATCH 02/19] std: add unit test for memory mapping --- lib/std/Io/File.zig | 4 ++++ lib/std/Io/File/MemoryMap.zig | 9 ++++++++ lib/std/Io/Threaded.zig | 3 +-- lib/std/Io/test.zig | 41 +++++++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index e622a6dd3cd457aa8351757f5c8729b4d09416b0..312729867683768d3d16a738fd618cb5db2f85f2 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -784,6 +784,10 @@ pub fn hardLink( return io.vtable.fileHardLink(io.userdata, file, new_dir, new_sub_path, options); } +pub fn createMemoryMap(file: File, io: Io, options: MemoryMap.CreateOptions) MemoryMap.CreateError!MemoryMap { + return .create(io, file, options); +} + test { _ = Reader; _ = Writer; diff --git a/lib/std/Io/File/MemoryMap.zig b/lib/std/Io/File/MemoryMap.zig index 848a8837d12b8553c0189b4cf9e4fbd23fd644c3..1f210f4c186e7d821b5e26b4ce5bb47b520bdee3 100644 --- a/lib/std/Io/File/MemoryMap.zig +++ b/lib/std/Io/File/MemoryMap.zig @@ -36,7 +36,14 @@ pub const CreateError = error{ } || Allocator.Error || File.ReadPositionalError; pub const CreateOptions = struct { + /// When this has read set to false, bytes that are not modified before a + /// sync may have the original file contents, or may be set to zero. protection: std.process.MemoryProtection = .{ .read = true, .write = true }, + /// If set to `true`, allows bytes observed before calling `read` to be + /// undefined, and bytes unwritten before calling `write` to write + /// undefined memory to the file. + undefined_contents: bool = false, + /// Prefault the pages. populate: bool = true, /// Byte index of file to start from. offset: u64 = 0, @@ -46,6 +53,8 @@ pub const CreateOptions = struct { len: ?usize = null, }; +/// To release the resources associated with the returned `MemoryMap`, call +/// `destroy`. pub fn create(io: Io, file: File, options: CreateOptions) CreateError!MemoryMap { return io.vtable.fileMemoryMapCreate(io.userdata, file, options); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 09d69682cee9c03c09a7d76638481ca6b2d35ed0..e48ce886a28d572a50a9281bafeb338a5a613148 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -16150,8 +16150,7 @@ fn fileMemoryMapCreate( }; errdefer gpa.rawFree(memory, alignment, @returnAddress()); - // If the mapping does not have read permissions, no need to populate the contents. - if (options.protection.read) try mmSyncRead(file, memory, offset); + if (!options.undefined_contents) try mmSyncRead(file, memory, offset); return .{ .file = file, diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index da928c610259963916657863495ddfd5ed65434a..aff2aec048a27b657dcc0bf5ec2e38f0edbbb1eb 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -592,3 +592,44 @@ test "randomSecure" { // that two sets of 50 bytes were equal. try expect(!mem.eql(u8, &buf_a, &buf_b)); } + +test "memory mapping" { + const io = testing.io; + + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + try tmp.dir.writeFile(io, .{ + .sub_path = "blah.txt", + .data = "this is my data123", + }); + + { + var file = try tmp.dir.openFile(io, "blah.txt", .{}); + defer file.close(io); + + var mm = try file.createMemoryMap(io, .{}); + defer mm.destroy(io); + + try expectEqualStrings("this is my data123", mm.memory); + mm.memory[5] = '9'; + mm.memory[8] = '9'; + + try mm.write(io); + } + + var buffer: [100]u8 = undefined; + const updated_contents = try tmp.dir.readFile(io, "blah.txt", &buffer); + try expectEqualStrings("this9is9my data123", updated_contents); + + var file = try tmp.dir.openFile(io, "blah.txt", .{}); + defer file.close(io); + + var mm = try file.createMemoryMap(io, .{ + .protection = .{ .read = true }, + .offset = 2, + }); + defer mm.destroy(io); + + try expectEqualStrings("is9is9my data123", mm.memory); +} -- 2.54.0 From bed7bc37c43afce335610867aedbcd506ade65f4 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 18:23:40 -0800 Subject: [PATCH 03/19] std.File.MemoryMap updates - change offset to u64 - make len non-optional - make write take a file_size parameter - std.Io.Threaded: introduce disable_memory_mapping flag to force it to take the fallback path. Additionally: - introduce BlockSize to File.Stat. On Windows, based on cached call to NtQuerySystemInformation. On unsupported OS's, set to 1. - support File.NLink on Windows. this was available the whole time, we just didn't see the field at first. - remove EBADF / INVALID_HANDLE from reading/writing file error sets --- lib/std/Io.zig | 2 +- lib/std/Io/File.zig | 9 +++ lib/std/Io/File/MemoryMap.zig | 47 +++++++++----- lib/std/Io/Threaded.zig | 111 ++++++++++++++++++++++++---------- lib/std/Io/test.zig | 26 +++----- lib/std/c.zig | 2 +- lib/std/posix.zig | 1 + 7 files changed, 131 insertions(+), 67 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 1da10a027fa172a208228600d7558378509d2094..b1af9f150c8779e1c0b42416bc9390ca90c5a27f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -658,7 +658,7 @@ pub const VTable = struct { fileMemoryMapDestroy: *const fn (?*anyopaque, *File.MemoryMap) void, fileMemoryMapSetLength: *const fn (?*anyopaque, *File.MemoryMap, n: usize) File.MemoryMap.SetLengthError!void, fileMemoryMapRead: *const fn (?*anyopaque, *File.MemoryMap) File.ReadPositionalError!void, - fileMemoryMapWrite: *const fn (?*anyopaque, *File.MemoryMap) File.WritePositionalError!void, + fileMemoryMapWrite: *const fn (?*anyopaque, *File.MemoryMap, file_size: u64) File.WritePositionalError!void, processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File, processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize, diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 312729867683768d3d16a738fd618cb5db2f85f2..dda3f9fe8e809a8fc392a5570e23c3a991b34256 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -22,6 +22,7 @@ pub const INode = std.posix.ino_t; pub const NLink = std.posix.nlink_t; pub const Uid = std.posix.uid_t; pub const Gid = std.posix.gid_t; +pub const BlockSize = u32; pub const Kind = enum { block_device, @@ -65,6 +66,14 @@ pub const Stat = struct { mtime: Io.Timestamp, /// Last status/metadata change time in nanoseconds, relative to UTC 1970-01-01. ctime: Io.Timestamp, + /// Smallest chunk length in bytes appropriate for optimal I/O. This will + /// be set to `1` for operating systems or file systems that do not + /// recognize this concept. Not always a power of two. When creating a + /// `MemoryMap`, the mapping length must be a multiple of this value. + /// + /// On Windows, this is whichever is larger: PageSize or + /// AllocationGranularity. + block_size: BlockSize, }; pub fn stdout() File { diff --git a/lib/std/Io/File/MemoryMap.zig b/lib/std/Io/File/MemoryMap.zig index 1f210f4c186e7d821b5e26b4ce5bb47b520bdee3..3751a0c76db4486e914d418387fb0541e7089e5e 100644 --- a/lib/std/Io/File/MemoryMap.zig +++ b/lib/std/Io/File/MemoryMap.zig @@ -10,10 +10,11 @@ const File = Io.File; const Allocator = std.mem.Allocator; file: File, -/// Byte index inside `file` where `memory` starts. -offset: usize, +/// Byte index inside `file` where `memory` starts. Page-aligned. +offset: u64, /// Memory that may or may not remain consistent with file contents. Use `read` -/// and `write` to ensure synchronization points. +/// and `write` to ensure synchronization points. No minimum alignment on the +/// pointer is guaranteed, but the length is page-aligned. memory: []u8, /// Tells whether it is memory-mapped or file operations. On Windows this also /// has a section handle. @@ -22,10 +23,10 @@ section: ?Section, pub const Section = if (is_windows) std.os.windows.HANDLE else void; pub const CreateError = error{ - /// A file descriptor refers to a non-regular file. Or a file mapping was requested, - /// but the file descriptor is not open for reading. Or `MAP.SHARED` was requested - /// and `PROT_WRITE` is set, but the file descriptor is not open in `RDWR` mode. - /// Or `PROT_WRITE` is set, but the file is append-only. + /// One of the following: + /// * The `File.Kind` is not `file`. + /// * The file is not open for reading and read access protections enabled. + /// * The file is not open for writing and write access protections enabled. AccessDenied, /// The `prot` argument asks for `PROT_EXEC` but the mapped area belongs to a file on /// a filesystem that was mounted no-exec. @@ -36,6 +37,12 @@ pub const CreateError = error{ } || Allocator.Error || File.ReadPositionalError; pub const CreateOptions = struct { + /// Size of the mapping, in bytes. If this is longer than the file size, it + /// will be filled with zeroes. + /// + /// Asserted to be a multiple of page size which can be obtained via + /// `std.heap.pageSize`. + len: usize, /// When this has read set to false, bytes that are not modified before a /// sync may have the original file contents, or may be set to zero. protection: std.process.MemoryProtection = .{ .read = true, .write = true }, @@ -45,12 +52,9 @@ pub const CreateOptions = struct { undefined_contents: bool = false, /// Prefault the pages. populate: bool = true, - /// Byte index of file to start from. + /// Asserted to be a multiple of page size which can be obtained via + /// `std.heap.pageSize`. offset: u64 = 0, - /// `null` indicates to map the entire file. If mapping the entire file is - /// desired and the file size is known, it is more efficient to populate - /// the value here. - len: ?usize = null, }; /// To release the resources associated with the returned `MemoryMap`, call @@ -73,8 +77,15 @@ pub const SetLengthError = error{ /// of the file after calling this is unspecified until `write` is called. /// /// May change the pointer address of `memory`. -pub fn setLength(mm: *MemoryMap, io: Io, n: usize) File.SetLengthError!void { - return io.vtable.fileMemoryMapSetLength(io.userdata, mm, n); +pub fn setLength( + mm: *MemoryMap, + io: Io, + /// New size of the mapping, in bytes. If this is longer than the file + /// size, it will be filled with zeroes. Asserted to be a multiple of page + /// size which can be obtained with `std.heap.pageSize`. + new_length: usize, +) File.SetLengthError!void { + return io.vtable.fileMemoryMapSetLength(io.userdata, mm, new_length); } /// Synchronizes the contents of `memory` from `file`. @@ -83,6 +94,10 @@ pub fn read(mm: *MemoryMap, io: Io) File.ReadPositionalError!void { } /// Synchronizes the contents of `memory` to `file`. -pub fn write(mm: *MemoryMap, io: Io) File.WritePositionalError!void { - return io.vtable.fileMemoryMapWrite(io.userdata, mm); +/// +/// Size of the mapping may be longer than the file size, so the `file_size` +/// argument is used to avoid writing too many bytes. If `file_size` is not +/// handy, use `File.length` to get it. +pub fn write(mm: *MemoryMap, io: Io, file_size: u64) File.WritePositionalError!void { + return io.vtable.fileMemoryMapWrite(io.userdata, mm, file_size); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index e48ce886a28d572a50a9281bafeb338a5a613148..e4fd954707097a9437d00eea2f9ff8832d86128b 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -57,6 +57,7 @@ use_sendfile: UseSendfile = .default, use_copy_file_range: UseCopyFileRange = .default, use_fcopyfile: UseFcopyfile = .default, use_fchmodat2: UseFchmodat2 = .default, +disable_memory_mapping: bool, stderr_writer: File.Writer = .{ .io = undefined, @@ -75,6 +76,13 @@ random_file: RandomFile = .{}, csprng: Csprng = .{}, +system_basic_information: SystemBasicInformation = .{}, + +const SystemBasicInformation = if (!is_windows) struct {} else struct { + buffer: windows.SYSTEM_BASIC_INFORMATION = undefined, + initialized: std.atomic.Value(bool) = .{ .raw = false }, +}; + pub const Csprng = struct { rng: std.Random.DefaultCsprng = .{ .state = undefined, @@ -1220,6 +1228,8 @@ pub const InitOptions = struct { /// * `processExecutablePath` on OpenBSD and Haiku (observes "PATH"). /// * `processSpawn`, `processSpawnPath`, `processReplace`, `processReplacePath` environ: process.Environ, + /// If set to `true`, `File.MemoryMap` APIs will always take the fallback path. + disable_memory_mapping: bool = false, }; /// Related: @@ -1247,6 +1257,7 @@ pub fn init( .argv0 = options.argv0, .environ = .{ .process_environ = options.environ }, .worker_threads = init_single_threaded.worker_threads, + .disable_memory_mapping = options.disable_memory_mapping, }; const cpu_count = std.Thread.getCpuCount(); @@ -1263,6 +1274,7 @@ pub fn init( .argv0 = options.argv0, .environ = .{ .process_environ = options.environ }, .worker_threads = .init(null), + .disable_memory_mapping = options.disable_memory_mapping, }; if (posix.Sigaction != void) { @@ -1299,6 +1311,7 @@ pub const init_single_threaded: Threaded = .{ .argv0 = .empty, .environ = .{}, .worker_threads = .init(null), + .disable_memory_mapping = false, }; var global_single_threaded_instance: Threaded = .init_single_threaded; @@ -2935,7 +2948,11 @@ fn fileStatLinux(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { fn fileStatWindows(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { const t: *Threaded = @ptrCast(@alignCast(userdata)); - _ = t; + + const block_size: u32 = if (t.systemBasicInformation()) |sbi| + @intCast(@max(sbi.PageSize, sbi.AllocationGranularity)) + else + std.heap.page_size_max; var io_status_block: windows.IO_STATUS_BLOCK = undefined; var info: windows.FILE.ALL_INFORMATION = undefined; @@ -2997,10 +3014,31 @@ fn fileStatWindows(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), .ctime = windows.fromSysTime(info.BasicInformation.ChangeTime), - .nlink = 0, + .nlink = info.StandardInformation.NumberOfLinks, + .block_size = block_size, }; } +fn systemBasicInformation(t: *Threaded) ?*const windows.SYSTEM_BASIC_INFORMATION { + if (!t.system_basic_information.initialized.load(.acquire)) { + t.mutex.lock(); + defer t.mutex.unlock(); + + switch (windows.ntdll.NtQuerySystemInformation( + .SystemBasicInformation, + &t.system_basic_information.buffer, + @sizeOf(windows.SYSTEM_BASIC_INFORMATION), + null, + )) { + .SUCCESS => {}, + else => return null, + } + + t.system_basic_information.initialized.store(true, .release); + } + return &t.system_basic_information.buffer; +} + fn fileStatWasi(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { if (builtin.link_libc) return fileStatPosix(userdata, file); @@ -8008,10 +8046,10 @@ fn fileReadStreamingWindows(userdata: ?*anyopaque, file: File, data: []const []u syscall.finish(); return 0; }, - .NETNAME_DELETED => return syscall.fail(error.ConnectionResetByPeer), + .NETNAME_DELETED => if (is_debug) unreachable else return error.Unexpected, .LOCK_VIOLATION => return syscall.fail(error.LockViolation), .ACCESS_DENIED => return syscall.fail(error.AccessDenied), - .INVALID_HANDLE => return syscall.fail(error.NotOpenForReading), + .INVALID_HANDLE => if (is_debug) unreachable else return error.Unexpected, // TODO: Determine if INVALID_FUNCTION is possible in more scenarios than just passing // a handle to a directory. .INVALID_FUNCTION => return syscall.fail(error.IsDir), @@ -8158,10 +8196,10 @@ fn fileReadPositionalWindows(userdata: ?*anyopaque, file: File, data: []const [] syscall.finish(); return 0; }, - .NETNAME_DELETED => return syscall.fail(error.ConnectionResetByPeer), + .NETNAME_DELETED => if (is_debug) unreachable else return error.Unexpected, .LOCK_VIOLATION => return syscall.fail(error.LockViolation), .ACCESS_DENIED => return syscall.fail(error.AccessDenied), - .INVALID_HANDLE => return syscall.fail(error.NotOpenForReading), + .INVALID_HANDLE => if (is_debug) unreachable else return error.Unexpected, // TODO: Determine if INVALID_FUNCTION is possible in more scenarios than just passing // a handle to a directory. .INVALID_FUNCTION => return syscall.fail(error.IsDir), @@ -8842,7 +8880,7 @@ fn writeFilePositionalWindows( .NOT_ENOUGH_MEMORY => return syscall.fail(error.SystemResources), .NOT_ENOUGH_QUOTA => return syscall.fail(error.SystemResources), .NO_DATA => return syscall.fail(error.BrokenPipe), - .INVALID_HANDLE => return syscall.fail(error.NotOpenForWriting), + .INVALID_HANDLE => if (is_debug) unreachable else return error.Unexpected, // use after free .LOCK_VIOLATION => return syscall.fail(error.LockViolation), .ACCESS_DENIED => return syscall.fail(error.AccessDenied), .WORKING_SET_QUOTA => return syscall.fail(error.SystemResources), @@ -12470,6 +12508,7 @@ const linux_statx_request: std.os.linux.STATX = .{ .INO = true, .SIZE = true, .NLINK = true, + .BLOCKS = true, }; const linux_statx_check: std.os.linux.STATX = .{ @@ -12481,6 +12520,7 @@ const linux_statx_check: std.os.linux.STATX = .{ .INO = true, .SIZE = true, .NLINK = true, + .BLOCKS = false, }; fn statFromLinux(stx: *const std.os.linux.Statx) Io.UnexpectedError!File.Stat { @@ -12499,6 +12539,7 @@ fn statFromLinux(stx: *const std.os.linux.Statx) Io.UnexpectedError!File.Stat { }, .mtime = .{ .nanoseconds = @intCast(@as(i128, stx.mtime.sec) * std.time.ns_per_s + stx.mtime.nsec) }, .ctime = .{ .nanoseconds = @intCast(@as(i128, stx.ctime.sec) * std.time.ns_per_s + stx.ctime.nsec) }, + .block_size = if (stx.mask.BLOCKS) stx.blksize else 1, }; } @@ -12547,6 +12588,7 @@ fn statFromPosix(st: *const posix.Stat) File.Stat { .atime = timestampFromPosix(&atime), .mtime = timestampFromPosix(&mtime), .ctime = timestampFromPosix(&ctime), + .block_size = st.blksize, }; } @@ -12568,6 +12610,7 @@ fn statFromWasi(st: *const std.os.wasi.filestat_t) File.Stat { .atime = .fromNanoseconds(st.atim), .mtime = .fromNanoseconds(st.mtim), .ctime = .fromNanoseconds(st.ctim), + .block_size = 1, }; } @@ -16127,28 +16170,29 @@ fn fileMemoryMapCreate( ) File.MemoryMap.CreateError!File.MemoryMap { const t: *Threaded = @ptrCast(@alignCast(userdata)); const offset = options.offset; + const len = options.len; - const page_size = std.heap.pageSize(); - const aligned_len: usize = options.len.?; // TODO query if necessary + assert(std.mem.isAligned(len, std.heap.page_size_min)); - if (createFileMap(file, options.protection, offset, options.populate, aligned_len)) |result| { - return result; - } else |err| switch (err) { - error.Unseekable, error.Canceled => |e| return e, - else => { - if (builtin.mode == .Debug) - std.log.warn("memory mapping failed with {t}, falling back to file operations", .{err}); - }, + if (!t.disable_memory_mapping) { + if (createFileMap(file, options.protection, offset, options.populate, len)) |result| { + return result; + } else |err| switch (err) { + error.Unseekable, error.Canceled, error.AccessDenied => |e| return e, + else => { + if (builtin.mode == .Debug) + std.log.warn("memory mapping failed with {t}, falling back to file operations", .{err}); + }, + } } const gpa = t.allocator; - const alignment: Alignment = .fromByteUnits(page_size); const memory = m: { - const ptr = gpa.rawAlloc(aligned_len, alignment, @returnAddress()) orelse + const ptr = gpa.rawAlloc(len, .@"1", @returnAddress()) orelse return error.OutOfMemory; - break :m ptr[0..aligned_len]; + break :m ptr[0..len]; }; - errdefer gpa.rawFree(memory, alignment, @returnAddress()); + errdefer gpa.rawFree(memory, .@"1", @returnAddress()); if (!options.undefined_contents) try mmSyncRead(file, memory, offset); @@ -16180,12 +16224,13 @@ const CreateFileMapError = error{ OutOfMemory, MappingAlreadyExists, Unseekable, + FileLockConflict, } || Io.Cancelable || Io.UnexpectedError; fn createFileMap( file: File, protection: std.process.MemoryProtection, - offset: usize, + offset: u64, populate: bool, aligned_len: usize, ) CreateFileMapError!File.MemoryMap { @@ -16212,7 +16257,7 @@ fn createFileMap( file.handle, )) { .SUCCESS => {}, - .FILE_LOCK_CONFLICT => return error.FileLocked, + .FILE_LOCK_CONFLICT => return error.FileLockConflict, .INVALID_FILE_FOR_SECTION => return error.OperationUnsupported, else => |status| return windows.unexpectedStatus(status), } @@ -16318,9 +16363,7 @@ fn fileMemoryMapDestroy(userdata: ?*anyopaque, mm: *File.MemoryMap) void { } } else { const gpa = t.allocator; - const page_size = std.heap.pageSize(); - const alignment: Alignment = .fromByteUnits(page_size); - gpa.rawFree(memory, alignment, @returnAddress()); + gpa.rawFree(memory, .@"1", @returnAddress()); } mm.* = undefined; } @@ -16331,6 +16374,7 @@ fn fileMemoryMapSetLength( new_len: usize, ) File.MemoryMap.SetLengthError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); + assert(std.mem.isAligned(new_len, std.heap.page_size_min)); if (mm.section) |section| switch (native_os) { .windows => { _ = section; @@ -16366,12 +16410,10 @@ fn fileMemoryMapSetLength( }, } else { const gpa = t.allocator; - const page_size = std.heap.pageSize(); - const alignment: Alignment = .fromByteUnits(page_size); - if (gpa.rawRemap(mm.memory, alignment, new_len, @returnAddress())) |new_ptr| { + if (gpa.rawRemap(mm.memory, .@"1", new_len, @returnAddress())) |new_ptr| { mm.memory = new_ptr[0..new_len]; } else { - const new_ptr = gpa.rawAlloc(new_len, alignment, @returnAddress()) orelse + const new_ptr = gpa.rawAlloc(new_len, .@"1", @returnAddress()) orelse return error.OutOfMemory; const copy_len = @min(new_len, mm.memory.len); @memcpy(new_ptr[0..copy_len], mm.memory[0..copy_len]); @@ -16387,11 +16429,16 @@ fn fileMemoryMapRead(userdata: ?*anyopaque, mm: *File.MemoryMap) File.ReadPositi return mmSyncRead(mm.file, mm.memory, mm.offset); } -fn fileMemoryMapWrite(userdata: ?*anyopaque, mm: *File.MemoryMap) File.WritePositionalError!void { +fn fileMemoryMapWrite( + userdata: ?*anyopaque, + mm: *File.MemoryMap, + file_size: u64, +) File.WritePositionalError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; if (mm.section != null) return; - return mmSyncWrite(mm.file, mm.memory, mm.offset); + const offset = mm.offset; + return mmSyncWrite(mm.file, mm.memory[0..@intCast(file_size - offset)], offset); } fn mmSyncRead(file: File, memory: []u8, offset: u64) File.ReadPositionalError!void { diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index aff2aec048a27b657dcc0bf5ec2e38f0edbbb1eb..43237fcb6f84c1eede28dc62b968eb0ec6d7b1dd 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -605,31 +605,23 @@ test "memory mapping" { }); { - var file = try tmp.dir.openFile(io, "blah.txt", .{}); + var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_write }); defer file.close(io); - var mm = try file.createMemoryMap(io, .{}); + const stat = try file.stat(io); + const aligned_len = std.mem.alignForward(usize, @intCast(stat.size), std.heap.pageSize()); + + var mm = try file.createMemoryMap(io, .{ .len = aligned_len }); defer mm.destroy(io); - try expectEqualStrings("this is my data123", mm.memory); - mm.memory[5] = '9'; - mm.memory[8] = '9'; + try expectEqualStrings("this is my data123", std.mem.sliceTo(mm.memory, 0)); + mm.memory[4] = '9'; + mm.memory[7] = '9'; - try mm.write(io); + try mm.write(io, stat.size); } var buffer: [100]u8 = undefined; const updated_contents = try tmp.dir.readFile(io, "blah.txt", &buffer); try expectEqualStrings("this9is9my data123", updated_contents); - - var file = try tmp.dir.openFile(io, "blah.txt", .{}); - defer file.close(io); - - var mm = try file.createMemoryMap(io, .{ - .protection = .{ .read = true }, - .offset = 2, - }); - defer mm.destroy(io); - - try expectEqualStrings("is9is9my data123", mm.memory); } diff --git a/lib/std/c.zig b/lib/std/c.zig index 5180dfbfa7bb4052e58d85d0cbfa8ffde33d9058..f10833cf0da4d7b033958f71708c5d85c4d12eff 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -212,7 +212,7 @@ pub const nlink_t = switch (native_os) { .wasi => c_ulonglong, // https://github.com/SerenityOS/serenity/blob/b98f537f117b341788023ab82e0c11ca9ae29a57/Kernel/API/POSIX/sys/types.h#L45 .freebsd, .serenity => u64, - .openbsd, .netbsd, .dragonfly, .illumos => u32, + .openbsd, .netbsd, .dragonfly, .illumos, .windows => u32, .haiku => i32, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => u16, else => u0, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index b607cc813440f9d0e9d762946965c363071d3ffa..6b029141622d4a9eaec35183e8200c26e2efcf84 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -55,6 +55,7 @@ else switch (native_os) { pub const gid_t = void; pub const mode_t = u0; pub const nlink_t = u0; + pub const blksize_t = void; pub const ino_t = void; pub const IFNAMESIZE = {}; pub const SIG = void; -- 2.54.0 From 482189843220d4bd21ab4fdb737c20ee57580769 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 19:16:09 -0800 Subject: [PATCH 04/19] std.Io.File.MemoryMap API tuning - remove file_size parameter from MemoryMap.write - remove requirement for mapping length to be aligned - align allocated fallback memory - add unit test for std.Io.Threaded.disable_memory_mapping = true - add unit test for MemoryMap.setLength --- lib/std/Io.zig | 2 +- lib/std/Io/File.zig | 6 +-- lib/std/Io/File/MemoryMap.zig | 29 ++++++----- lib/std/Io/Threaded.zig | 98 ++++++++++++++++++----------------- lib/std/Io/Threaded/test.zig | 57 ++++++++++++++++++++ lib/std/Io/test.zig | 27 +++++++--- 6 files changed, 145 insertions(+), 74 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index b1af9f150c8779e1c0b42416bc9390ca90c5a27f..1da10a027fa172a208228600d7558378509d2094 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -658,7 +658,7 @@ pub const VTable = struct { fileMemoryMapDestroy: *const fn (?*anyopaque, *File.MemoryMap) void, fileMemoryMapSetLength: *const fn (?*anyopaque, *File.MemoryMap, n: usize) File.MemoryMap.SetLengthError!void, fileMemoryMapRead: *const fn (?*anyopaque, *File.MemoryMap) File.ReadPositionalError!void, - fileMemoryMapWrite: *const fn (?*anyopaque, *File.MemoryMap, file_size: u64) File.WritePositionalError!void, + fileMemoryMapWrite: *const fn (?*anyopaque, *File.MemoryMap) File.WritePositionalError!void, processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File, processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize, diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index dda3f9fe8e809a8fc392a5570e23c3a991b34256..e7114653517573be5a27d0573f3e41c06fb0c2ac 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -68,11 +68,7 @@ pub const Stat = struct { ctime: Io.Timestamp, /// Smallest chunk length in bytes appropriate for optimal I/O. This will /// be set to `1` for operating systems or file systems that do not - /// recognize this concept. Not always a power of two. When creating a - /// `MemoryMap`, the mapping length must be a multiple of this value. - /// - /// On Windows, this is whichever is larger: PageSize or - /// AllocationGranularity. + /// recognize this concept. Not always a power of two. block_size: BlockSize, }; diff --git a/lib/std/Io/File/MemoryMap.zig b/lib/std/Io/File/MemoryMap.zig index 3751a0c76db4486e914d418387fb0541e7089e5e..1c3c6d5fa2a32314483a4ed3c66f1750ff8240f5 100644 --- a/lib/std/Io/File/MemoryMap.zig +++ b/lib/std/Io/File/MemoryMap.zig @@ -13,8 +13,8 @@ file: File, /// Byte index inside `file` where `memory` starts. Page-aligned. offset: u64, /// Memory that may or may not remain consistent with file contents. Use `read` -/// and `write` to ensure synchronization points. No minimum alignment on the -/// pointer is guaranteed, but the length is page-aligned. +/// and `write` to ensure synchronization points. Pointer is page-aligned but +/// length is not. memory: []u8, /// Tells whether it is memory-mapped or file operations. On Windows this also /// has a section handle. @@ -37,11 +37,13 @@ pub const CreateError = error{ } || Allocator.Error || File.ReadPositionalError; pub const CreateOptions = struct { - /// Size of the mapping, in bytes. If this is longer than the file size, it - /// will be filled with zeroes. + /// Size of the mapping, in bytes. If this is longer than the file size, + /// `memory` beyond the file end will be filled with zeroes and it is + /// unspecified whether, after calling `write`, the file length will be + /// set to `len` or remain unchanged. /// - /// Asserted to be a multiple of page size which can be obtained via - /// `std.heap.pageSize`. + /// This value has no minimum alignment requirement, but may gain + /// efficiency benefits from being a multiple of `File.Stat.block_size`. len: usize, /// When this has read set to false, bytes that are not modified before a /// sync may have the original file contents, or may be set to zero. @@ -81,10 +83,9 @@ pub fn setLength( mm: *MemoryMap, io: Io, /// New size of the mapping, in bytes. If this is longer than the file - /// size, it will be filled with zeroes. Asserted to be a multiple of page - /// size which can be obtained with `std.heap.pageSize`. + /// size, it will be filled with zeroes. No alignment requirement. new_length: usize, -) File.SetLengthError!void { +) SetLengthError!void { return io.vtable.fileMemoryMapSetLength(io.userdata, mm, new_length); } @@ -95,9 +96,9 @@ pub fn read(mm: *MemoryMap, io: Io) File.ReadPositionalError!void { /// Synchronizes the contents of `memory` to `file`. /// -/// Size of the mapping may be longer than the file size, so the `file_size` -/// argument is used to avoid writing too many bytes. If `file_size` is not -/// handy, use `File.length` to get it. -pub fn write(mm: *MemoryMap, io: Io, file_size: u64) File.WritePositionalError!void { - return io.vtable.fileMemoryMapWrite(io.userdata, mm, file_size); +/// If `memory.len` is greater than file size, the bytes beyond the end of the +/// file may be dropped, or they may be written, extending the size of the +/// file. +pub fn write(mm: *MemoryMap, io: Io) File.WritePositionalError!void { + return io.vtable.fileMemoryMapWrite(io.userdata, mm); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index e4fd954707097a9437d00eea2f9ff8832d86128b..bbd375e21f68fe31bb547ff7bdc08095f11d5f57 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -16172,8 +16172,6 @@ fn fileMemoryMapCreate( const offset = options.offset; const len = options.len; - assert(std.mem.isAligned(len, std.heap.page_size_min)); - if (!t.disable_memory_mapping) { if (createFileMap(file, options.protection, offset, options.populate, len)) |result| { return result; @@ -16187,12 +16185,13 @@ fn fileMemoryMapCreate( } const gpa = t.allocator; + const page_size = std.heap.pageSize(); + const alignment: Alignment = .fromByteUnits(page_size); const memory = m: { - const ptr = gpa.rawAlloc(len, .@"1", @returnAddress()) orelse - return error.OutOfMemory; + const ptr = gpa.rawAlloc(len, alignment, @returnAddress()) orelse return error.OutOfMemory; break :m ptr[0..len]; }; - errdefer gpa.rawFree(memory, .@"1", @returnAddress()); + errdefer gpa.rawFree(memory, alignment, @returnAddress()); if (!options.undefined_contents) try mmSyncRead(file, memory, offset); @@ -16363,7 +16362,7 @@ fn fileMemoryMapDestroy(userdata: ?*anyopaque, mm: *File.MemoryMap) void { } } else { const gpa = t.allocator; - gpa.rawFree(memory, .@"1", @returnAddress()); + gpa.rawFree(memory, .fromByteUnits(std.heap.pageSize()), @returnAddress()); } mm.* = undefined; } @@ -16374,46 +16373,54 @@ fn fileMemoryMapSetLength( new_len: usize, ) File.MemoryMap.SetLengthError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); - assert(std.mem.isAligned(new_len, std.heap.page_size_min)); - if (mm.section) |section| switch (native_os) { - .windows => { - _ = section; - @panic("TODO"); - }, - .wasi => unreachable, - else => { - const flags: posix.MREMAP = .{ .MAYMOVE = true }; - const addr_hint: ?[*]const u8 = null; - const new_memory = while (true) { - const syscall: Syscall = try .start(); - const rc = posix.system.mremap(mm.memory.ptr, mm.memory.len, new_len, flags, addr_hint); - syscall.finish(); - const err: posix.E = if (builtin.link_libc) e: { - if (rc != std.c.MAP_FAILED) break @as([*]u8, @ptrCast(@alignCast(rc)))[0..new_len]; - break :e @enumFromInt(posix.system._errno().*); - } else e: { - const err = posix.errno(rc); - if (err == .SUCCESS) break @as([*]u8, @ptrFromInt(rc))[0..new_len]; - break :e err; + const page_size = std.heap.pageSize(); + const alignment: Alignment = .fromByteUnits(page_size); + + if (mm.section) |section| { + if (alignment.forward(new_len) == alignment.forward(mm.memory.len)) { + mm.memory.len = new_len; + return; + } + switch (native_os) { + .windows => { + _ = section; + @panic("TODO"); + }, + .wasi => unreachable, + else => { + const flags: posix.MREMAP = .{ .MAYMOVE = true }; + const addr_hint: ?[*]const u8 = null; + const new_memory = while (true) { + const syscall: Syscall = try .start(); + const rc = posix.system.mremap(mm.memory.ptr, mm.memory.len, new_len, flags, addr_hint); + syscall.finish(); + const err: posix.E = if (builtin.link_libc) e: { + if (rc != std.c.MAP_FAILED) break @as([*]u8, @ptrCast(@alignCast(rc)))[0..new_len]; + break :e @enumFromInt(posix.system._errno().*); + } else e: { + const err = posix.errno(rc); + if (err == .SUCCESS) break @as([*]u8, @ptrFromInt(rc))[0..new_len]; + break :e err; + }; + switch (err) { + .SUCCESS => unreachable, + .INTR => continue, + .AGAIN => return error.LockedMemoryLimitExceeded, + .NOMEM => return error.OutOfMemory, + .INVAL => return errnoBug(err), + .FAULT => return errnoBug(err), + else => return posix.unexpectedErrno(err), + } }; - switch (err) { - .SUCCESS => unreachable, - .INTR => continue, - .AGAIN => return error.LockedMemoryLimitExceeded, - .NOMEM => return error.OutOfMemory, - .INVAL => return errnoBug(err), - .FAULT => return errnoBug(err), - else => return posix.unexpectedErrno(err), - } - }; - mm.memory = new_memory; - }, + mm.memory = new_memory; + }, + } } else { const gpa = t.allocator; - if (gpa.rawRemap(mm.memory, .@"1", new_len, @returnAddress())) |new_ptr| { + if (gpa.rawRemap(mm.memory, alignment, new_len, @returnAddress())) |new_ptr| { mm.memory = new_ptr[0..new_len]; } else { - const new_ptr = gpa.rawAlloc(new_len, .@"1", @returnAddress()) orelse + const new_ptr = gpa.rawAlloc(new_len, alignment, @returnAddress()) orelse return error.OutOfMemory; const copy_len = @min(new_len, mm.memory.len); @memcpy(new_ptr[0..copy_len], mm.memory[0..copy_len]); @@ -16429,16 +16436,11 @@ fn fileMemoryMapRead(userdata: ?*anyopaque, mm: *File.MemoryMap) File.ReadPositi return mmSyncRead(mm.file, mm.memory, mm.offset); } -fn fileMemoryMapWrite( - userdata: ?*anyopaque, - mm: *File.MemoryMap, - file_size: u64, -) File.WritePositionalError!void { +fn fileMemoryMapWrite(userdata: ?*anyopaque, mm: *File.MemoryMap) File.WritePositionalError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; if (mm.section != null) return; - const offset = mm.offset; - return mmSyncWrite(mm.file, mm.memory[0..@intCast(file_size - offset)], offset); + return mmSyncWrite(mm.file, mm.memory, mm.offset); } fn mmSyncRead(file: File, memory: []u8, offset: u64) File.ReadPositionalError!void { diff --git a/lib/std/Io/Threaded/test.zig b/lib/std/Io/Threaded/test.zig index 34aa4a8c689d67ceef1d1ca8e985b5487da9e2f1..f90ab6e745b5431aa438d03bad223e9cddeb76ec 100644 --- a/lib/std/Io/Threaded/test.zig +++ b/lib/std/Io/Threaded/test.zig @@ -204,3 +204,60 @@ test "cancel blocked read from pipe" { try io.sleep(.fromMilliseconds(10), .awake); try future.cancel(io); } + +test "memory mapping fallback" { + var threaded: std.Io.Threaded = .init(std.testing.allocator, .{ + .argv0 = .empty, + .environ = .empty, + .disable_memory_mapping = true, + }); + defer threaded.deinit(); + const io = threaded.io(); + + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + + try tmp.dir.writeFile(io, .{ + .sub_path = "blah.txt", + .data = "this is my data123", + }); + + { + var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_write }); + defer file.close(io); + + // The `Io.File.MemoryMap` API does not specify what happens if we supply a + // length greater than file size, but this is testing specifically std.Io.Threaded + // with disable_memory_mapping = true. + var mm = try file.createMemoryMap(io, .{ .len = "this is my data123".len + 3 }); + defer mm.destroy(io); + + try testing.expectEqualStrings("this is my data123\x00\x00\x00", mm.memory); + mm.memory[4] = '9'; + mm.memory[7] = '9'; + + try mm.write(io); + } + + var buffer: [100]u8 = undefined; + const updated_contents = try tmp.dir.readFile(io, "blah.txt", &buffer); + try testing.expectEqualStrings("this9is9my data123\x00\x00\x00", updated_contents); + + { + var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_only }); + defer file.close(io); + + var mm = try file.createMemoryMap(io, .{ + .len = "this9is9my".len, + .protection = .{ .read = true }, + }); + defer mm.destroy(io); + + try testing.expectEqualStrings("this9is9my", mm.memory); + + try mm.setLength(io, "this9is9my data123".len); + try mm.read(io); + + try testing.expectEqualStrings("this9is9my data123", mm.memory); + } +} diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index 43237fcb6f84c1eede28dc62b968eb0ec6d7b1dd..1cfcd7a22193eb842f7f06233cffddc6ae8baaa0 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -608,20 +608,35 @@ test "memory mapping" { var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_write }); defer file.close(io); - const stat = try file.stat(io); - const aligned_len = std.mem.alignForward(usize, @intCast(stat.size), std.heap.pageSize()); - - var mm = try file.createMemoryMap(io, .{ .len = aligned_len }); + var mm = try file.createMemoryMap(io, .{ .len = "this is my data123".len }); defer mm.destroy(io); - try expectEqualStrings("this is my data123", std.mem.sliceTo(mm.memory, 0)); + try expectEqualStrings("this is my data123", mm.memory); mm.memory[4] = '9'; mm.memory[7] = '9'; - try mm.write(io, stat.size); + try mm.write(io); } var buffer: [100]u8 = undefined; const updated_contents = try tmp.dir.readFile(io, "blah.txt", &buffer); try expectEqualStrings("this9is9my data123", updated_contents); + + { + var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_only }); + defer file.close(io); + + var mm = try file.createMemoryMap(io, .{ + .len = "this9is9my".len, + .protection = .{ .read = true }, + }); + defer mm.destroy(io); + + try expectEqualStrings("this9is9my", mm.memory); + + try mm.setLength(io, "this9is9my data123".len); + try mm.read(io); + + try expectEqualStrings("this9is9my data123", mm.memory); + } } -- 2.54.0 From eb163361d9d7b61ee0b93b6f49efd24943c6027d Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 19:30:37 -0800 Subject: [PATCH 05/19] std.Io.File.MemoryMap: add some alignment type safety --- lib/std/Io/File/MemoryMap.zig | 6 +++--- lib/std/Io/Threaded.zig | 22 +++++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/std/Io/File/MemoryMap.zig b/lib/std/Io/File/MemoryMap.zig index 1c3c6d5fa2a32314483a4ed3c66f1750ff8240f5..0b7ff69bbaab0f7857719a5084a304a78089c9ed 100644 --- a/lib/std/Io/File/MemoryMap.zig +++ b/lib/std/Io/File/MemoryMap.zig @@ -13,9 +13,9 @@ file: File, /// Byte index inside `file` where `memory` starts. Page-aligned. offset: u64, /// Memory that may or may not remain consistent with file contents. Use `read` -/// and `write` to ensure synchronization points. Pointer is page-aligned but -/// length is not. -memory: []u8, +/// and `write` to ensure synchronization points. Length has no alignment +/// requirement. +memory: []align(std.heap.page_size_min) u8, /// Tells whether it is memory-mapped or file operations. On Windows this also /// has a section handle. section: ?Section, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index bbd375e21f68fe31bb547ff7bdc08095f11d5f57..5c4776b9b88e52efdc00e3988cf7eeb297ab3f8a 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -16198,7 +16198,7 @@ fn fileMemoryMapCreate( return .{ .file = file, .offset = offset, - .memory = memory, + .memory = @alignCast(memory), .section = null, }; } @@ -16283,7 +16283,7 @@ fn createFileMap( return .{ .file = file, .offset = offset, - .memory = contents_ptr.?[0..contents_len], + .memory = @alignCast(contents_ptr.?[0..contents_len]), .section = section, }; } else if (have_mmap) { @@ -16297,6 +16297,8 @@ fn createFileMap( .POPULATE = populate, }; + const page_align = std.heap.page_size_min; + const contents = while (true) { const syscall: Syscall = try .start(); const casted_offset = std.math.cast(i64, offset) orelse return error.Unseekable; @@ -16304,13 +16306,13 @@ fn createFileMap( syscall.finish(); const err: posix.E = if (builtin.link_libc) e: { if (rc != std.c.MAP_FAILED) { - break @as([*]u8, @ptrCast(@alignCast(rc)))[0..aligned_len]; + break @as([*]align(page_align) u8, @ptrCast(@alignCast(rc)))[0..aligned_len]; } break :e @enumFromInt(posix.system._errno().*); } else e: { const err = posix.errno(rc); if (err == .SUCCESS) { - break @as([*]u8, @ptrFromInt(rc))[0..aligned_len]; + break @as([*]align(page_align) u8, @ptrFromInt(rc))[0..aligned_len]; } break :e err; }; @@ -16375,6 +16377,7 @@ fn fileMemoryMapSetLength( const t: *Threaded = @ptrCast(@alignCast(userdata)); const page_size = std.heap.pageSize(); const alignment: Alignment = .fromByteUnits(page_size); + const page_align = std.heap.page_size_min; if (mm.section) |section| { if (alignment.forward(new_len) == alignment.forward(mm.memory.len)) { @@ -16395,11 +16398,11 @@ fn fileMemoryMapSetLength( const rc = posix.system.mremap(mm.memory.ptr, mm.memory.len, new_len, flags, addr_hint); syscall.finish(); const err: posix.E = if (builtin.link_libc) e: { - if (rc != std.c.MAP_FAILED) break @as([*]u8, @ptrCast(@alignCast(rc)))[0..new_len]; + if (rc != std.c.MAP_FAILED) break @as([*]align(page_align) u8, @ptrCast(@alignCast(rc)))[0..new_len]; break :e @enumFromInt(posix.system._errno().*); } else e: { const err = posix.errno(rc); - if (err == .SUCCESS) break @as([*]u8, @ptrFromInt(rc))[0..new_len]; + if (err == .SUCCESS) break @as([*]align(page_align) u8, @ptrFromInt(rc))[0..new_len]; break :e err; }; switch (err) { @@ -16418,10 +16421,11 @@ fn fileMemoryMapSetLength( } else { const gpa = t.allocator; if (gpa.rawRemap(mm.memory, alignment, new_len, @returnAddress())) |new_ptr| { - mm.memory = new_ptr[0..new_len]; + mm.memory = @alignCast(new_ptr[0..new_len]); } else { - const new_ptr = gpa.rawAlloc(new_len, alignment, @returnAddress()) orelse - return error.OutOfMemory; + const new_ptr: [*]align(page_align) u8 = @alignCast( + gpa.rawAlloc(new_len, alignment, @returnAddress()) orelse return error.OutOfMemory, + ); const copy_len = @min(new_len, mm.memory.len); @memcpy(new_ptr[0..copy_len], mm.memory[0..copy_len]); mm.memory = new_ptr[0..new_len]; -- 2.54.0 From c917f619f041f5494e1c2d13c28ce1b04cec11f7 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 20:12:54 -0800 Subject: [PATCH 06/19] std: fix compilation failures on various targets --- lib/std/Io.zig | 2 +- lib/std/Io/File/MemoryMap.zig | 28 ++++++++----- lib/std/Io/Threaded.zig | 74 ++++++++++++++++++++++++----------- lib/std/Io/Threaded/test.zig | 2 +- lib/std/Io/test.zig | 4 +- lib/std/c.zig | 2 +- 6 files changed, 77 insertions(+), 35 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 1da10a027fa172a208228600d7558378509d2094..bf12ee6c6cd6098e3d4a9e218a3b18326724747f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -656,7 +656,7 @@ pub const VTable = struct { fileMemoryMapCreate: *const fn (?*anyopaque, File, File.MemoryMap.CreateOptions) File.MemoryMap.CreateError!File.MemoryMap, fileMemoryMapDestroy: *const fn (?*anyopaque, *File.MemoryMap) void, - fileMemoryMapSetLength: *const fn (?*anyopaque, *File.MemoryMap, n: usize) File.MemoryMap.SetLengthError!void, + fileMemoryMapSetLength: *const fn (?*anyopaque, *File.MemoryMap, File.MemoryMap.CreateOptions) File.MemoryMap.SetLengthError!void, fileMemoryMapRead: *const fn (?*anyopaque, *File.MemoryMap) File.ReadPositionalError!void, fileMemoryMapWrite: *const fn (?*anyopaque, *File.MemoryMap) File.WritePositionalError!void, diff --git a/lib/std/Io/File/MemoryMap.zig b/lib/std/Io/File/MemoryMap.zig index 0b7ff69bbaab0f7857719a5084a304a78089c9ed..2b917840a14409ce0008c63f47698aff4dee32ab 100644 --- a/lib/std/Io/File/MemoryMap.zig +++ b/lib/std/Io/File/MemoryMap.zig @@ -52,7 +52,9 @@ pub const CreateOptions = struct { /// undefined, and bytes unwritten before calling `write` to write /// undefined memory to the file. undefined_contents: bool = false, - /// Prefault the pages. + /// Prefault the pages. If this option is unsupported, it is silently + /// ignored. Aside from custom Io implementations, this option is only + /// supported on Linux. populate: bool = true, /// Asserted to be a multiple of page size which can be obtained via /// `std.heap.pageSize`. @@ -72,21 +74,29 @@ pub fn destroy(mm: *MemoryMap, io: Io) void { } pub const SetLengthError = error{ + /// One of the following: + /// * The `File.Kind` is not `file`. + /// * The file is not open for reading and read access protections enabled. + /// * The file is not open for writing and write access protections enabled. + AccessDenied, + /// The `prot` argument asks for `PROT_EXEC` but the mapped area belongs to a file on + /// a filesystem that was mounted no-exec. + PermissionDenied, LockedMemoryLimitExceeded, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, } || Allocator.Error || File.SetLengthError; /// Change the size of the mapping. This does not sync the contents. The size /// of the file after calling this is unspecified until `write` is called. /// /// May change the pointer address of `memory`. -pub fn setLength( - mm: *MemoryMap, - io: Io, - /// New size of the mapping, in bytes. If this is longer than the file - /// size, it will be filled with zeroes. No alignment requirement. - new_length: usize, -) SetLengthError!void { - return io.vtable.fileMemoryMapSetLength(io.userdata, mm, new_length); +/// +/// `options` is needed because the mapping may need to be destroyed and +/// re-created. All the same options must be provided except for `len` which is +/// the new length. +pub fn setLength(mm: *MemoryMap, io: Io, options: CreateOptions) SetLengthError!void { + return io.vtable.fileMemoryMapSetLength(io.userdata, mm, options); } /// Synchronizes the contents of `memory` from `file`. diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 5c4776b9b88e52efdc00e3988cf7eeb297ab3f8a..f39bdd2eaafab94a19d159d3063a764b90e8b5b0 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -7998,7 +7998,7 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: File, data: []const []u8) .FAULT => |err| return errnoBug(err), .AGAIN => return error.WouldBlock, .BADF => |err| { - if (native_os == .wasi) return error.NotOpenForReading; // File operation on directory. + if (native_os == .wasi) return error.IsDir; // File operation on directory. return errnoBug(err); // File descriptor used after closed. }, .IO => return error.InputOutput, @@ -8089,7 +8089,7 @@ fn fileReadPositionalPosix(userdata: ?*anyopaque, file: File, data: []const []u8 syscall.finish(); return nread; }, - .INTR => { + .INTR, .TIMEDOUT => { try syscall.checkCancel(); continue; }, @@ -8103,7 +8103,6 @@ fn fileReadPositionalPosix(userdata: ?*anyopaque, file: File, data: []const []u8 .ISDIR => return syscall.fail(error.IsDir), .NOBUFS => return syscall.fail(error.SystemResources), .NOMEM => return syscall.fail(error.SystemResources), - .TIMEDOUT => return syscall.fail(error.Timeout), .NXIO => return syscall.fail(error.Unseekable), .SPIPE => return syscall.fail(error.Unseekable), .OVERFLOW => return syscall.fail(error.Unseekable), @@ -12588,7 +12587,7 @@ fn statFromPosix(st: *const posix.Stat) File.Stat { .atime = timestampFromPosix(&atime), .mtime = timestampFromPosix(&mtime), .ctime = timestampFromPosix(&ctime), - .block_size = st.blksize, + .block_size = @intCast(st.blksize), }; } @@ -16292,9 +16291,14 @@ fn createFileMap( .WRITE = protection.write, .EXEC = protection.execute, }; - const flags: posix.MAP = .{ - .TYPE = if (native_os == .linux) .SHARED_VALIDATE else .SHARED, - .POPULATE = populate, + const flags: posix.MAP = switch (native_os) { + .linux => .{ + .TYPE = .SHARED_VALIDATE, + .POPULATE = populate, + }, + else => .{ + .TYPE = .SHARED, + }, }; const page_align = std.heap.page_size_min; @@ -16348,20 +16352,20 @@ fn createFileMap( fn fileMemoryMapDestroy(userdata: ?*anyopaque, mm: *File.MemoryMap) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); const memory = mm.memory; - if (mm.section) |section| { - if (is_windows) { + if (mm.section) |section| switch (native_os) { + .windows => { const current_process: windows.HANDLE = @ptrFromInt(@as(usize, @bitCast(@as(isize, -1)))); _ = windows.ntdll.NtUnmapViewOfSection(current_process, memory.ptr); windows.CloseHandle(section); - } else { - switch (posix.errno(posix.system.munmap(memory.ptr, memory.len))) { - .SUCCESS => {}, - else => |e| { - if (builtin.mode == .Debug) - std.log.err("failed to unmap {d} bytes at {*}: {t}", .{ memory.len, memory.ptr, e }); - }, - } - } + }, + .wasi => unreachable, + else => switch (posix.errno(posix.system.munmap(memory.ptr, memory.len))) { + .SUCCESS => {}, + else => |e| { + if (builtin.mode == .Debug) + std.log.err("failed to unmap {d} bytes at {*}: {t}", .{ memory.len, memory.ptr, e }); + }, + }, } else { const gpa = t.allocator; gpa.rawFree(memory, .fromByteUnits(std.heap.pageSize()), @returnAddress()); @@ -16372,15 +16376,17 @@ fn fileMemoryMapDestroy(userdata: ?*anyopaque, mm: *File.MemoryMap) void { fn fileMemoryMapSetLength( userdata: ?*anyopaque, mm: *File.MemoryMap, - new_len: usize, + options: File.MemoryMap.CreateOptions, ) File.MemoryMap.SetLengthError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); const page_size = std.heap.pageSize(); const alignment: Alignment = .fromByteUnits(page_size); const page_align = std.heap.page_size_min; + const old_memory = mm.memory; + const new_len = options.len; if (mm.section) |section| { - if (alignment.forward(new_len) == alignment.forward(mm.memory.len)) { + if (alignment.forward(new_len) == alignment.forward(old_memory.len)) { mm.memory.len = new_len; return; } @@ -16390,12 +16396,12 @@ fn fileMemoryMapSetLength( @panic("TODO"); }, .wasi => unreachable, - else => { + .linux => { const flags: posix.MREMAP = .{ .MAYMOVE = true }; const addr_hint: ?[*]const u8 = null; const new_memory = while (true) { const syscall: Syscall = try .start(); - const rc = posix.system.mremap(mm.memory.ptr, mm.memory.len, new_len, flags, addr_hint); + const rc = posix.system.mremap(old_memory.ptr, old_memory.len, new_len, flags, addr_hint); syscall.finish(); const err: posix.E = if (builtin.link_libc) e: { if (rc != std.c.MAP_FAILED) break @as([*]align(page_align) u8, @ptrCast(@alignCast(rc)))[0..new_len]; @@ -16417,6 +16423,30 @@ fn fileMemoryMapSetLength( }; mm.memory = new_memory; }, + else => { + switch (posix.errno(posix.system.munmap(old_memory.ptr, old_memory.len))) { + .SUCCESS => {}, + else => |e| { + if (builtin.mode == .Debug) std.log.err("failed to unmap {d} bytes at {*}: {t}", .{ + old_memory.len, old_memory.ptr, e, + }); + // munmap must be infallible, or we cannot design reliable software. + return error.Unexpected; + }, + } + if (createFileMap(mm.file, options.protection, mm.offset, options.populate, new_len)) |result| { + mm.* = result; + return; + } else |err| switch (err) { + error.OperationUnsupported, + error.Unseekable, + error.SectionOversize, + error.MappingAlreadyExists, + error.FileLockConflict, + => return error.Unexpected, // It worked before on the same open file. + else => |e| return e, + } + }, } } else { const gpa = t.allocator; diff --git a/lib/std/Io/Threaded/test.zig b/lib/std/Io/Threaded/test.zig index f90ab6e745b5431aa438d03bad223e9cddeb76ec..0e5d90e6d4bdbf8363d8f48ce0abbf03cc3f662d 100644 --- a/lib/std/Io/Threaded/test.zig +++ b/lib/std/Io/Threaded/test.zig @@ -255,7 +255,7 @@ test "memory mapping fallback" { try testing.expectEqualStrings("this9is9my", mm.memory); - try mm.setLength(io, "this9is9my data123".len); + try mm.setLength(io, .{ .len = "this9is9my data123".len }); try mm.read(io); try testing.expectEqualStrings("this9is9my data123", mm.memory); diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index 1cfcd7a22193eb842f7f06233cffddc6ae8baaa0..e1c5bfea83934ae74e4754a28220ff95f12f873c 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -594,6 +594,8 @@ test "randomSecure" { } test "memory mapping" { + if (builtin.cpu.arch == .hexagon) return error.SkipZigTest; // mmap returned EINVAL + const io = testing.io; var tmp = tmpDir(.{}); @@ -634,7 +636,7 @@ test "memory mapping" { try expectEqualStrings("this9is9my", mm.memory); - try mm.setLength(io, "this9is9my data123".len); + try mm.setLength(io, .{ .len = "this9is9my data123".len }); try mm.read(io); try expectEqualStrings("this9is9my data123", mm.memory); diff --git a/lib/std/c.zig b/lib/std/c.zig index f10833cf0da4d7b033958f71708c5d85c4d12eff..535f0f79090bd88e70bbef0f112f64cd7ff76529 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -10334,7 +10334,7 @@ pub extern "c" fn getgrgid(gid: gid_t) ?*group; pub extern "c" fn getgrgid_r(gid: gid_t, grp: *group, buf: [*]u8, buflen: usize, result: *?*group) c_int; pub extern "c" fn getrlimit64(resource: rlimit_resource, rlim: *rlimit) c_int; pub extern "c" fn lseek64(fd: fd_t, offset: i64, whence: c_int) i64; -pub extern "c" fn mmap64(addr: ?*align(page_size) anyopaque, len: usize, prot: PROT, flags: c_uint, fd: fd_t, offset: i64) *anyopaque; +pub extern "c" fn mmap64(addr: ?*align(page_size) anyopaque, len: usize, prot: PROT, flags: MAP, fd: fd_t, offset: i64) *anyopaque; pub extern "c" fn open64(path: [*:0]const u8, oflag: O, ...) c_int; pub extern "c" fn openat64(fd: c_int, path: [*:0]const u8, oflag: O, ...) c_int; pub extern "c" fn pread64(fd: fd_t, buf: [*]u8, nbyte: usize, offset: i64) isize; -- 2.54.0 From e70def70ece846433b7a2140291ed416a6c34f43 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 20:41:40 -0800 Subject: [PATCH 07/19] std.Io.Threaded: passing memory mapping tests on wasm32-wasi --- lib/std/Io/Threaded.zig | 263 ++++++++++++++++++++++++++-------------- 1 file changed, 169 insertions(+), 94 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index f39bdd2eaafab94a19d159d3063a764b90e8b5b0..ceed776fbae20433d469cad95ef6dd42493695cd 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -7963,7 +7963,7 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: File, data: []const []u8) switch (e) { .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), - .BADF => return error.NotOpenForReading, // File operation on directory. + .BADF => return error.IsDir, // File operation on directory. .IO => return error.InputOutput, .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, @@ -8793,7 +8793,7 @@ fn fileWritePositional( .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .AGAIN => |err| return errnoBug(err), - .BADF => return error.NotOpenForWriting, // can be a race condition. + .BADF => |err| return errnoBug(err), // use after free .DESTADDRREQ => |err| return errnoBug(err), // `connect` was never called. .DQUOT => return error.DiskQuota, .FBIG => return error.FileTooBig, @@ -16176,6 +16176,7 @@ fn fileMemoryMapCreate( return result; } else |err| switch (err) { error.Unseekable, error.Canceled, error.AccessDenied => |e| return e, + error.OperationUnsupported => {}, else => { if (builtin.mode == .Debug) std.log.warn("memory mapping failed with {t}, falling back to file operations", .{err}); @@ -16478,104 +16479,178 @@ fn fileMemoryMapWrite(userdata: ?*anyopaque, mm: *File.MemoryMap) File.WritePosi } fn mmSyncRead(file: File, memory: []u8, offset: u64) File.ReadPositionalError!void { - switch (native_os) { - .windows => @panic("TODO"), - .wasi => @panic("TODO"), - else => { - var i: usize = 0; - const syscall: Syscall = try .start(); - while (true) { - const buf = memory[i..]; - if (buf.len == 0) { - syscall.finish(); - break; - } - const rc = pread_sym(file.handle, buf.ptr, buf.len, @intCast(offset + i)); - switch (posix.errno(rc)) { - .SUCCESS => { - const n: usize = @intCast(rc); - if (n == 0) { - syscall.finish(); - @memset(memory[i..], 0); - break; - } - i += n; - try syscall.checkCancel(); - continue; - }, - .INTR, .TIMEDOUT => { - try syscall.checkCancel(); - continue; - }, - .NXIO => return syscall.fail(error.Unseekable), - .SPIPE => return syscall.fail(error.Unseekable), - .OVERFLOW => return syscall.fail(error.Unseekable), - .NOBUFS => return syscall.fail(error.SystemResources), - .NOMEM => return syscall.fail(error.SystemResources), - .AGAIN => return syscall.fail(error.WouldBlock), - .IO => return syscall.fail(error.InputOutput), - .ISDIR => return syscall.fail(error.IsDir), - .NOTCONN => |err| return syscall.errnoBug(err), // not a socket - .CONNRESET => |err| return syscall.errnoBug(err), // not a socket - .INVAL => |err| return syscall.errnoBug(err), - .FAULT => |err| return syscall.errnoBug(err), - .BADF => |err| { + if (is_windows) { + @panic("TODO"); + } else if (native_os == .wasi and !builtin.link_libc) { + var i: usize = 0; + const syscall: Syscall = try .start(); + while (true) { + const buf = memory[i..]; + if (buf.len == 0) { + syscall.finish(); + break; + } + var n: usize = undefined; + const vec: std.os.wasi.iovec_t = .{ .base = buf.ptr, .len = buf.len }; + switch (std.os.wasi.fd_pread(file.handle, (&vec)[0..1], 1, offset + i, &n)) { + .SUCCESS => { + if (n == 0) { + syscall.finish(); + @memset(memory[i..], 0); + break; + } + i += n; + try syscall.checkCancel(); + continue; + }, + .INTR, .TIMEDOUT => { + try syscall.checkCancel(); + continue; + }, + .NOTCONN => |err| return syscall.errnoBug(err), // not a socket + .CONNRESET => |err| return syscall.errnoBug(err), // not a socket + .BADF => |err| return syscall.errnoBug(err), // use after free + .INVAL => |err| return syscall.errnoBug(err), + .FAULT => |err| return syscall.errnoBug(err), // segmentation fault + .AGAIN => |err| return syscall.errnoBug(err), + .IO => return syscall.fail(error.InputOutput), + .ISDIR => return syscall.fail(error.IsDir), + .NOBUFS => return syscall.fail(error.SystemResources), + .NOMEM => return syscall.fail(error.SystemResources), + .NXIO => return syscall.fail(error.Unseekable), + .SPIPE => return syscall.fail(error.Unseekable), + .OVERFLOW => return syscall.fail(error.Unseekable), + .NOTCAPABLE => return syscall.fail(error.AccessDenied), + else => |err| return syscall.unexpectedErrno(err), + } + } + } else { + var i: usize = 0; + const syscall: Syscall = try .start(); + while (true) { + const buf = memory[i..]; + if (buf.len == 0) { + syscall.finish(); + break; + } + const rc = pread_sym(file.handle, buf.ptr, buf.len, @intCast(offset + i)); + switch (posix.errno(rc)) { + .SUCCESS => { + const n: usize = @intCast(rc); + if (n == 0) { syscall.finish(); - if (native_os == .wasi) return error.IsDir; // File operation on directory. - return errnoBug(err); // File descriptor used after closed. - }, - else => |err| return syscall.unexpectedErrno(err), - } + @memset(memory[i..], 0); + break; + } + i += n; + try syscall.checkCancel(); + continue; + }, + .INTR, .TIMEDOUT => { + try syscall.checkCancel(); + continue; + }, + .NXIO => return syscall.fail(error.Unseekable), + .SPIPE => return syscall.fail(error.Unseekable), + .OVERFLOW => return syscall.fail(error.Unseekable), + .NOBUFS => return syscall.fail(error.SystemResources), + .NOMEM => return syscall.fail(error.SystemResources), + .AGAIN => return syscall.fail(error.WouldBlock), + .IO => return syscall.fail(error.InputOutput), + .ISDIR => return syscall.fail(error.IsDir), + .NOTCONN => |err| return syscall.errnoBug(err), // not a socket + .CONNRESET => |err| return syscall.errnoBug(err), // not a socket + .INVAL => |err| return syscall.errnoBug(err), + .FAULT => |err| return syscall.errnoBug(err), + .BADF => |err| return syscall.errnoBug(err), // use after free + else => |err| return syscall.unexpectedErrno(err), } - }, + } } } fn mmSyncWrite(file: File, memory: []u8, offset: u64) File.WritePositionalError!void { - switch (native_os) { - .windows => @panic("TODO"), - .wasi => @panic("TODO"), - else => { - var i: usize = 0; - const syscall: Syscall = try .start(); - while (true) { - const buf = memory[i..]; - if (buf.len == 0) { - syscall.finish(); - break; - } - const rc = pwrite_sym(file.handle, buf.ptr, buf.len, @intCast(offset)); - switch (posix.errno(rc)) { - .SUCCESS => { - const n: usize = @bitCast(rc); - i += n; - try syscall.checkCancel(); - continue; - }, - .INTR => { - try syscall.checkCancel(); - continue; - }, - .INVAL => |err| return syscall.errnoBug(err), - .FAULT => |err| return syscall.errnoBug(err), - .DESTADDRREQ => |err| return syscall.errnoBug(err), // not a socket - .CONNRESET => |err| return syscall.errnoBug(err), // not a socket - .BADF => |err| return syscall.errnoBug(err), // use after free - .AGAIN => return syscall.fail(error.WouldBlock), - .DQUOT => return syscall.fail(error.DiskQuota), - .FBIG => return syscall.fail(error.FileTooBig), - .IO => return syscall.fail(error.InputOutput), - .NOSPC => return syscall.fail(error.NoSpaceLeft), - .PERM => return syscall.fail(error.PermissionDenied), - .PIPE => return syscall.fail(error.BrokenPipe), - .BUSY => return syscall.fail(error.DeviceBusy), - .TXTBSY => return syscall.fail(error.FileBusy), - .NXIO => return syscall.fail(error.Unseekable), - .SPIPE => return syscall.fail(error.Unseekable), - .OVERFLOW => return syscall.fail(error.Unseekable), - else => |err| return syscall.unexpectedErrno(err), - } + if (is_windows) { + @panic("TODO"); + } else if (native_os == .wasi and !builtin.link_libc) { + var i: usize = 0; + var n: usize = undefined; + const syscall: Syscall = try .start(); + while (true) { + const buf = memory[i..]; + if (buf.len == 0) { + syscall.finish(); + break; } - }, + const iovec: std.os.wasi.ciovec_t = .{ .base = buf.ptr, .len = buf.len }; + switch (std.os.wasi.fd_pwrite(file.handle, (&iovec)[0..1], 1, offset + i, &n)) { + .SUCCESS => { + i += n; + try syscall.checkCancel(); + continue; + }, + .INTR => { + try syscall.checkCancel(); + continue; + }, + .DQUOT => return syscall.fail(error.DiskQuota), + .FBIG => return syscall.fail(error.FileTooBig), + .IO => return syscall.fail(error.InputOutput), + .NOSPC => return syscall.fail(error.NoSpaceLeft), + .PERM => return syscall.fail(error.PermissionDenied), + .PIPE => return syscall.fail(error.BrokenPipe), + .NOTCAPABLE => return syscall.fail(error.AccessDenied), + .NXIO => return syscall.fail(error.Unseekable), + .SPIPE => return syscall.fail(error.Unseekable), + .OVERFLOW => return syscall.fail(error.Unseekable), + .INVAL => |err| return syscall.errnoBug(err), + .FAULT => |err| return syscall.errnoBug(err), + .AGAIN => |err| return syscall.errnoBug(err), + .BADF => |err| return syscall.errnoBug(err), // use after free + .DESTADDRREQ => |err| return syscall.errnoBug(err), // not a socket + else => |err| return syscall.unexpectedErrno(err), + } + } + } else { + var i: usize = 0; + const syscall: Syscall = try .start(); + while (true) { + const buf = memory[i..]; + if (buf.len == 0) { + syscall.finish(); + break; + } + const rc = pwrite_sym(file.handle, buf.ptr, buf.len, @intCast(offset + i)); + switch (posix.errno(rc)) { + .SUCCESS => { + const n: usize = @bitCast(rc); + i += n; + try syscall.checkCancel(); + continue; + }, + .INTR => { + try syscall.checkCancel(); + continue; + }, + .INVAL => |err| return syscall.errnoBug(err), + .FAULT => |err| return syscall.errnoBug(err), + .DESTADDRREQ => |err| return syscall.errnoBug(err), // not a socket + .CONNRESET => |err| return syscall.errnoBug(err), // not a socket + .BADF => |err| return syscall.errnoBug(err), // use after free + .AGAIN => return syscall.fail(error.WouldBlock), + .DQUOT => return syscall.fail(error.DiskQuota), + .FBIG => return syscall.fail(error.FileTooBig), + .IO => return syscall.fail(error.InputOutput), + .NOSPC => return syscall.fail(error.NoSpaceLeft), + .PERM => return syscall.fail(error.PermissionDenied), + .PIPE => return syscall.fail(error.BrokenPipe), + .BUSY => return syscall.fail(error.DeviceBusy), + .TXTBSY => return syscall.fail(error.FileBusy), + .NXIO => return syscall.fail(error.Unseekable), + .SPIPE => return syscall.fail(error.Unseekable), + .OVERFLOW => return syscall.fail(error.Unseekable), + else => |err| return syscall.unexpectedErrno(err), + } + } } } -- 2.54.0 From c4df980ac2678b45dbe6fffbc8d3d96cb28b40ea Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 20:48:01 -0800 Subject: [PATCH 08/19] std.fs: fix test cases regarding error.IsDir on WASI why were we testing for shitty behavior? --- lib/std/fs/test.zig | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 07e9a7760573603c562d8f770283cee1bb832ff8..2d5dd78b23c753bc49d50b02785330728dfa8d60 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -827,11 +827,6 @@ test "file operations on directories" { const buf = try ctx.dir.readFileAlloc(io, test_dir_name, testing.allocator, .unlimited); testing.allocator.free(buf); }, - .wasi => { - // WASI return EBADF, which gets mapped to NotOpenForReading. - // See https://github.com/bytecodealliance/wasmtime/issues/1935 - try expectError(error.NotOpenForReading, ctx.dir.readFileAlloc(io, test_dir_name, testing.allocator, .unlimited)); - }, else => { try expectError(error.IsDir, ctx.dir.readFileAlloc(io, test_dir_name, testing.allocator, .unlimited)); }, @@ -851,13 +846,9 @@ test "file operations on directories" { defer handle.close(io); // Reading from the handle should fail - const expected_err = switch (native_os) { - .wasi => error.NotOpenForReading, - else => error.IsDir, - }; var buf: [1]u8 = undefined; - try expectError(expected_err, handle.readStreaming(io, &.{&buf})); - try expectError(expected_err, handle.readPositional(io, &.{&buf}, 0)); + try expectError(error.IsDir, handle.readStreaming(io, &.{&buf})); + try expectError(error.IsDir, handle.readPositional(io, &.{&buf}, 0)); } try expectError(error.IsDir, ctx.dir.openFile(io, test_dir_name, .{ .allow_directory = false, .mode = .read_only })); -- 2.54.0 From 470d0dd9c61571669a41c2e25f9eca66054be784 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 20:54:10 -0800 Subject: [PATCH 09/19] link: add some errors to the Dwarf.UpdateError mega error set --- src/link/Dwarf.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/link/Dwarf.zig b/src/link/Dwarf.zig index d3fff513f43623f296b59b3ee770aa72a15d9471..8bf1dccbcbde557f230a526cbea90240704162ea 100644 --- a/src/link/Dwarf.zig +++ b/src/link/Dwarf.zig @@ -49,6 +49,14 @@ pub const UpdateError = error{ Underflow, UnexpectedEndOfFile, NonResizable, + /// TODO why is this in the error set? + Timeout, + /// TODO why is this in the error set? + ConnectionResetByPeer, + /// TODO why is this in the error set? + SocketUnconnected, + /// TODO why is this in the error set? + NotOpenForWriting, } || codegen.GenerateSymbolError || Io.File.OpenError || -- 2.54.0 From 08a33c06713912da92a03effc6d3753bf87f8a51 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 23:18:33 -0800 Subject: [PATCH 10/19] std: workaround for #30842 --- lib/std/heap.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/std/heap.zig b/lib/std/heap.zig index f584e3f72e46241728ba1448ae2c7a766401b348..cfe943fc2bfe6c373418e59d540a48f507a5bc8a 100644 --- a/lib/std/heap.zig +++ b/lib/std/heap.zig @@ -53,8 +53,9 @@ pub var next_mmap_addr_hint: ?[*]align(page_size_min) u8 = null; /// /// On many systems, the actual page size can only be determined at runtime /// with `pageSize`. -pub const page_size_min: usize = std.options.page_size_min orelse (page_size_min_default orelse - @compileError(@tagName(builtin.cpu.arch) ++ "-" ++ @tagName(builtin.os.tag) ++ " has unknown page_size_min; populate std.options.page_size_min")); +pub const page_size_min: usize = std.options.page_size_min orelse (page_size_min_default orelse 1); +//`orelse 1` is a workaround for https://codeberg.org/ziglang/zig/issues/30842 +//@compileError(@tagName(builtin.cpu.arch) ++ "-" ++ @tagName(builtin.os.tag) ++ " has unknown page_size_min; populate std.options.page_size_min")); /// comptime-known maximum page size of the target. /// -- 2.54.0 From cb37a5c1fbfd991c6c299b27f35bbbe0b2b8d8de Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 23:21:00 -0800 Subject: [PATCH 11/19] std.Io.Threaded: fix fileReadPositional error.IsDir for wasi --- lib/std/Io/Threaded.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index ceed776fbae20433d469cad95ef6dd42493695cd..87b47b4a1e7af19991f45c8910165d263624a4ce 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -8095,12 +8095,12 @@ fn fileReadPositionalPosix(userdata: ?*anyopaque, file: File, data: []const []u8 }, .NOTCONN => |err| return syscall.errnoBug(err), // not a socket .CONNRESET => |err| return syscall.errnoBug(err), // not a socket - .BADF => |err| return syscall.errnoBug(err), // use after free .INVAL => |err| return syscall.errnoBug(err), .FAULT => |err| return syscall.errnoBug(err), // segmentation fault .AGAIN => |err| return syscall.errnoBug(err), .IO => return syscall.fail(error.InputOutput), .ISDIR => return syscall.fail(error.IsDir), + .BADF => return syscall.fail(error.IsDir), .NOBUFS => return syscall.fail(error.SystemResources), .NOMEM => return syscall.fail(error.SystemResources), .NXIO => return syscall.fail(error.Unseekable), -- 2.54.0 From 2b01764d86d8c1b808a1ffb5913442e8f055762f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 23:37:36 -0800 Subject: [PATCH 12/19] std: fix handling of EBADF error code In the context of read/write it's ambiguous, means file was opened without read/write respectively. --- lib/std/Io/File.zig | 4 ++++ lib/std/Io/File/Reader.zig | 3 ++- lib/std/Io/Threaded.zig | 20 +++++++++----------- lib/std/posix.zig | 2 +- lib/std/zig/system.zig | 2 +- src/link/Dwarf.zig | 4 ---- 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index e7114653517573be5a27d0573f3e41c06fb0c2ac..cc1a58a15e2fa78878eca1fd84458397cb4b6412 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -554,6 +554,8 @@ pub const ReadPositionalError = error{ LockViolation, /// This file cannot be read positionally. Unseekable, + /// File was not opened with read capability. + NotOpenForReading, } || Io.Cancelable || Io.UnexpectedError; /// Returns 0 on stream end or if `buffer` has no space available for data. @@ -588,6 +590,8 @@ pub const WritePositionalError = error{ FileBusy, /// This file cannot be written positionally. Unseekable, + /// File was not opened with write capability. + NotOpenForWriting, } || Io.Cancelable || Io.UnexpectedError; /// See also: diff --git a/lib/std/Io/File/Reader.zig b/lib/std/Io/File/Reader.zig index c2842ee45ffbbeb55fd898fa34d4a4f58d7cc869..f400f2c51439b65bbe7e11ef3dfa0d5b49b4f3bf 100644 --- a/lib/std/Io/File/Reader.zig +++ b/lib/std/Io/File/Reader.zig @@ -33,7 +33,8 @@ pub const Error = error{ IsDir, BrokenPipe, ConnectionResetByPeer, - Timeout, + /// File was not opened with read capability. + NotOpenForReading, SocketUnconnected, /// Non-blocking has been enabled, and reading from the file descriptor /// would block. diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 87b47b4a1e7af19991f45c8910165d263624a4ce..a02c190330fddb75e53de379907b7a11a432692e 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -7954,7 +7954,7 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: File, data: []const []u8) syscall.finish(); return nread; }, - .INTR => { + .INTR, .TIMEDOUT => { try syscall.checkCancel(); continue; }, @@ -7970,7 +7970,6 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: File, data: []const []u8) .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.Timeout, .NOTCAPABLE => return error.AccessDenied, else => |err| return posix.unexpectedErrno(err), } @@ -7987,7 +7986,7 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: File, data: []const []u8) syscall.finish(); return @intCast(rc); }, - .INTR => { + .INTR, .TIMEDOUT => { try syscall.checkCancel(); continue; }, @@ -7997,9 +7996,9 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: File, data: []const []u8) .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .AGAIN => return error.WouldBlock, - .BADF => |err| { + .BADF => { if (native_os == .wasi) return error.IsDir; // File operation on directory. - return errnoBug(err); // File descriptor used after closed. + return error.NotOpenForReading; }, .IO => return error.InputOutput, .ISDIR => return error.IsDir, @@ -8007,7 +8006,6 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: File, data: []const []u8) .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.Timeout, else => |err| return posix.unexpectedErrno(err), } }, @@ -8136,10 +8134,10 @@ fn fileReadPositionalPosix(userdata: ?*anyopaque, file: File, data: []const []u8 .CONNRESET => |err| return syscall.errnoBug(err), // not a socket .INVAL => |err| return syscall.errnoBug(err), .FAULT => |err| return syscall.errnoBug(err), - .BADF => |err| { + .BADF => { syscall.finish(); if (native_os == .wasi) return error.IsDir; // File operation on directory. - return errnoBug(err); // File descriptor used after closed. + return error.NotOpenForReading; }, else => |err| return syscall.unexpectedErrno(err), } @@ -8793,7 +8791,7 @@ fn fileWritePositional( .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .AGAIN => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), // use after free + .BADF => return error.NotOpenForWriting, .DESTADDRREQ => |err| return errnoBug(err), // `connect` was never called. .DQUOT => return error.DiskQuota, .FBIG => return error.FileTooBig, @@ -8828,7 +8826,7 @@ fn fileWritePositional( .FAULT => |err| return syscall.errnoBug(err), .DESTADDRREQ => |err| return syscall.errnoBug(err), // `connect` was never called. .CONNRESET => |err| return syscall.errnoBug(err), // Not a socket handle. - .BADF => |err| return syscall.errnoBug(err), // use after free + .BADF => return syscall.fail(error.NotOpenForWriting), .AGAIN => return syscall.fail(error.WouldBlock), .DQUOT => return syscall.fail(error.DiskQuota), .FBIG => return syscall.fail(error.FileTooBig), @@ -16636,7 +16634,7 @@ fn mmSyncWrite(file: File, memory: []u8, offset: u64) File.WritePositionalError! .FAULT => |err| return syscall.errnoBug(err), .DESTADDRREQ => |err| return syscall.errnoBug(err), // not a socket .CONNRESET => |err| return syscall.errnoBug(err), // not a socket - .BADF => |err| return syscall.errnoBug(err), // use after free + .BADF => return syscall.fail(error.NotOpenForWriting), .AGAIN => return syscall.fail(error.WouldBlock), .DQUOT => return syscall.fail(error.DiskQuota), .FBIG => return syscall.fail(error.FileTooBig), diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 6b029141622d4a9eaec35183e8200c26e2efcf84..f1f4e279d1726ad4997dc2f2009755887aa3c520 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -441,7 +441,7 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize { .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.Timeout, + .TIMEDOUT => return error.Unexpected, else => |err| return unexpectedErrno(err), } } diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index d0cd0721bbff3218818bce17b3e07a561e1c3bf8..0e7d814a71f1f28f424509d481a1106282924bd2 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -420,7 +420,7 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target { error.WouldBlock => return error.Unexpected, error.BrokenPipe => return error.Unexpected, error.ConnectionResetByPeer => return error.Unexpected, - error.Timeout => return error.Unexpected, + error.NotOpenForReading => return error.Unexpected, error.SocketUnconnected => return error.Unexpected, error.AccessDenied, diff --git a/src/link/Dwarf.zig b/src/link/Dwarf.zig index 8bf1dccbcbde557f230a526cbea90240704162ea..19bbee45b3a12d6883f3e7803475eb960c5a1784 100644 --- a/src/link/Dwarf.zig +++ b/src/link/Dwarf.zig @@ -50,13 +50,9 @@ pub const UpdateError = error{ UnexpectedEndOfFile, NonResizable, /// TODO why is this in the error set? - Timeout, - /// TODO why is this in the error set? ConnectionResetByPeer, /// TODO why is this in the error set? SocketUnconnected, - /// TODO why is this in the error set? - NotOpenForWriting, } || codegen.GenerateSymbolError || Io.File.OpenError || -- 2.54.0 From 3c708b19fea5e855197c6f3f8130a39a903b3f77 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 23:34:07 -0800 Subject: [PATCH 13/19] disable test due to wasmtime bug tracked by #20747 --- lib/std/Io/Threaded/test.zig | 5 +++++ lib/std/Io/test.zig | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/lib/std/Io/Threaded/test.zig b/lib/std/Io/Threaded/test.zig index 0e5d90e6d4bdbf8363d8f48ce0abbf03cc3f662d..9ab9c6dcd98b51414611c7e492663d6e01aa795f 100644 --- a/lib/std/Io/Threaded/test.zig +++ b/lib/std/Io/Threaded/test.zig @@ -206,6 +206,11 @@ test "cancel blocked read from pipe" { } test "memory mapping fallback" { + if (builtin.os.tag == .wasi and builtin.link_libc) { + // https://github.com/ziglang/zig/issues/20747 (open fd does not have write permission) + return error.SkipZigTest; + } + var threaded: std.Io.Threaded = .init(std.testing.allocator, .{ .argv0 = .empty, .environ = .empty, diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index e1c5bfea83934ae74e4754a28220ff95f12f873c..00a2d872418167d53c5d09d3ce2d8072d21026fa 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -595,6 +595,10 @@ test "randomSecure" { test "memory mapping" { if (builtin.cpu.arch == .hexagon) return error.SkipZigTest; // mmap returned EINVAL + if (builtin.os.tag == .wasi and builtin.link_libc) { + // https://github.com/ziglang/zig/issues/20747 (open fd does not have write permission) + return error.SkipZigTest; + } const io = testing.io; -- 2.54.0 From 8527ec9c7b229bcc8a15824e885d16abbab57534 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 15 Jan 2026 10:14:03 -0800 Subject: [PATCH 14/19] std.Io.Threaded: implement mmap fallback for Windows --- lib/std/Io/Threaded.zig | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index a02c190330fddb75e53de379907b7a11a432692e..bfa012c8c2f177773cf4bc2cb6bd8d077c924bac 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -8153,14 +8153,17 @@ fn fileReadPositionalWindows(userdata: ?*anyopaque, file: File, data: []const [] const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; - const DWORD = windows.DWORD; - var index: usize = 0; while (index < data.len and data[index].len == 0) index += 1; if (index == data.len) return 0; const buffer = data[index]; + + return readFilePositionalWindows(file, buffer, offset); +} + +fn readFilePositionalWindows(file: File, buffer: []u8, offset: u64) File.ReadPositionalError!usize { + const DWORD = windows.DWORD; const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len); - var overlapped: windows.OVERLAPPED = .{ .Internal = 0, .InternalHigh = 0, @@ -16478,7 +16481,17 @@ fn fileMemoryMapWrite(userdata: ?*anyopaque, mm: *File.MemoryMap) File.WritePosi fn mmSyncRead(file: File, memory: []u8, offset: u64) File.ReadPositionalError!void { if (is_windows) { - @panic("TODO"); + var i: usize = 0; + while (true) { + const buf = memory[i..]; + if (buf.len == 0) break; + const n = try readFilePositionalWindows(file, buf, offset + i); + if (n == 0) { + @memset(memory[i..], 0); + break; + } + i += n; + } } else if (native_os == .wasi and !builtin.link_libc) { var i: usize = 0; const syscall: Syscall = try .start(); @@ -16569,7 +16582,12 @@ fn mmSyncRead(file: File, memory: []u8, offset: u64) File.ReadPositionalError!vo fn mmSyncWrite(file: File, memory: []u8, offset: u64) File.WritePositionalError!void { if (is_windows) { - @panic("TODO"); + var i: usize = 0; + while (true) { + const buf = memory[i..]; + if (buf.len == 0) break; + i += try writeFilePositionalWindows(file.handle, memory[i..], offset + i); + } } else if (native_os == .wasi and !builtin.link_libc) { var i: usize = 0; var n: usize = undefined; -- 2.54.0 From b2776d097d4407a8d76bea88e4289297eed65e3e Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 15 Jan 2026 10:30:25 -0800 Subject: [PATCH 15/19] std.Io.Threaded: implement MemoryMap.setLength for Windows --- lib/std/Io/Threaded.zig | 43 +++++++++++++++++++++++++++-------------- lib/std/Io/test.zig | 5 +++-- lib/std/os/windows.zig | 2 ++ 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index bfa012c8c2f177773cf4bc2cb6bd8d077c924bac..c8b2f26d273405000abbe3cfa708f5a313c82f8e 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -16232,7 +16232,7 @@ fn createFileMap( protection: std.process.MemoryProtection, offset: u64, populate: bool, - aligned_len: usize, + len: usize, ) CreateFileMapError!File.MemoryMap { if (is_windows) { try Thread.checkCancel(); @@ -16251,7 +16251,7 @@ fn createFileMap( .STANDARD = .{ .RIGHTS = .REQUIRED }, }, null, - @constCast(&@as(i64, @intCast(aligned_len))), + @constCast(&@as(i64, @intCast(len))), .{ .READWRITE = true }, .{ .COMMIT = populate }, file.handle, @@ -16261,12 +16261,11 @@ fn createFileMap( .INVALID_FILE_FOR_SECTION => return error.OperationUnsupported, else => |status| return windows.unexpectedStatus(status), } - const current_process: windows.HANDLE = @ptrFromInt(@as(usize, @bitCast(@as(isize, -1)))); - var contents_ptr: ?[*]u8 = null; - var contents_len = aligned_len; + var contents_ptr: ?[*]align(std.heap.page_size_min) u8 = null; + var contents_len = len; switch (windows.ntdll.NtMapViewOfSection( section, - current_process, + windows.current_process, @ptrCast(&contents_ptr), null, 0, @@ -16284,7 +16283,7 @@ fn createFileMap( return .{ .file = file, .offset = offset, - .memory = @alignCast(contents_ptr.?[0..contents_len]), + .memory = contents_ptr.?[0..contents_len], .section = section, }; } else if (have_mmap) { @@ -16308,17 +16307,17 @@ fn createFileMap( const contents = while (true) { const syscall: Syscall = try .start(); const casted_offset = std.math.cast(i64, offset) orelse return error.Unseekable; - const rc = mmap_sym(null, aligned_len, prot, flags, file.handle, casted_offset); + const rc = mmap_sym(null, len, prot, flags, file.handle, casted_offset); syscall.finish(); const err: posix.E = if (builtin.link_libc) e: { if (rc != std.c.MAP_FAILED) { - break @as([*]align(page_align) u8, @ptrCast(@alignCast(rc)))[0..aligned_len]; + break @as([*]align(page_align) u8, @ptrCast(@alignCast(rc)))[0..len]; } break :e @enumFromInt(posix.system._errno().*); } else e: { const err = posix.errno(rc); if (err == .SUCCESS) { - break @as([*]align(page_align) u8, @ptrFromInt(rc))[0..aligned_len]; + break @as([*]align(page_align) u8, @ptrFromInt(rc))[0..len]; } break :e err; }; @@ -16356,8 +16355,7 @@ fn fileMemoryMapDestroy(userdata: ?*anyopaque, mm: *File.MemoryMap) void { const memory = mm.memory; if (mm.section) |section| switch (native_os) { .windows => { - const current_process: windows.HANDLE = @ptrFromInt(@as(usize, @bitCast(@as(isize, -1)))); - _ = windows.ntdll.NtUnmapViewOfSection(current_process, memory.ptr); + _ = windows.ntdll.NtUnmapViewOfSection(windows.current_process, memory.ptr); windows.CloseHandle(section); }, .wasi => unreachable, @@ -16394,8 +16392,25 @@ fn fileMemoryMapSetLength( } switch (native_os) { .windows => { - _ = section; - @panic("TODO"); + var contents_ptr: ?[*]align(page_align) u8 = null; + var contents_len = new_len; + switch (windows.ntdll.NtMapViewOfSection( + section, + windows.current_process, + @ptrCast(&contents_ptr), + null, + 0, + null, + &contents_len, + .Unmap, + .{}, + .{ .READWRITE = true }, + )) { + .SUCCESS => {}, + .SECTION_PROTECTION => return error.PermissionDenied, + else => |status| return windows.unexpectedStatus(status), + } + mm.memory = contents_ptr.?[0..contents_len]; }, .wasi => unreachable, .linux => { diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index 00a2d872418167d53c5d09d3ce2d8072d21026fa..0f4285b9bd92183cb9efa265e22ac2aec4042baa 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -640,9 +640,10 @@ test "memory mapping" { try expectEqualStrings("this9is9my", mm.memory); - try mm.setLength(io, .{ .len = "this9is9my data123".len }); + // Cross a page boundary to require an actual remap. + try mm.setLength(io, .{ .len = std.heap.pageSize() * 2 }); try mm.read(io); - try expectEqualStrings("this9is9my data123", mm.memory); + try expectEqualStrings("this9is9my data123\x00\x00", mm.memory[0.."this9is9my data123\x00\x00".len]); } } diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 1e864d14c277f4d09954f388559bf4255a4bd821..91f3cb6513fd6969b98dd0cc67b2e922268bde42 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -28,6 +28,8 @@ pub const ws2_32 = @import("windows/ws2_32.zig"); pub const crypt32 = @import("windows/crypt32.zig"); pub const nls = @import("windows/nls.zig"); +pub const current_process: HANDLE = @ptrFromInt(@as(usize, @bitCast(@as(isize, -1)))); + pub const FILE = struct { // ref: km/ntddk.h -- 2.54.0 From 7ff6bcc1115b329cfb8ff1498f5d6cc74ae67dd3 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 15 Jan 2026 10:44:44 -0800 Subject: [PATCH 16/19] std.Io.Threaded: track unaligned mapping length on Windows --- lib/std/Io/Threaded.zig | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index c8b2f26d273405000abbe3cfa708f5a313c82f8e..3d9b2301e9c34c77cf00a0576c9aca312fa6f4e2 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -16259,6 +16259,7 @@ fn createFileMap( .SUCCESS => {}, .FILE_LOCK_CONFLICT => return error.FileLockConflict, .INVALID_FILE_FOR_SECTION => return error.OperationUnsupported, + .ACCESS_DENIED => return error.AccessDenied, else => |status| return windows.unexpectedStatus(status), } var contents_ptr: ?[*]align(std.heap.page_size_min) u8 = null; @@ -16280,10 +16281,15 @@ fn createFileMap( .SECTION_PROTECTION => return error.PermissionDenied, else => |status| return windows.unexpectedStatus(status), } + if (builtin.mode == .Debug) { + const page_size = std.heap.pageSize(); + const alignment: Alignment = .fromByteUnits(page_size); + assert(contents_len == alignment.forward(len)); + } return .{ .file = file, .offset = offset, - .memory = contents_ptr.?[0..contents_len], + .memory = contents_ptr.?[0..len], .section = section, }; } else if (have_mmap) { @@ -16410,7 +16416,8 @@ fn fileMemoryMapSetLength( .SECTION_PROTECTION => return error.PermissionDenied, else => |status| return windows.unexpectedStatus(status), } - mm.memory = contents_ptr.?[0..contents_len]; + assert(contents_len == alignment.forward(new_len)); + mm.memory = contents_ptr.?[0..new_len]; }, .wasi => unreachable, .linux => { @@ -16483,15 +16490,15 @@ fn fileMemoryMapSetLength( fn fileMemoryMapRead(userdata: ?*anyopaque, mm: *File.MemoryMap) File.ReadPositionalError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; - if (mm.section != null) return; - return mmSyncRead(mm.file, mm.memory, mm.offset); + const section = mm.section orelse return mmSyncRead(mm.file, mm.memory, mm.offset); + _ = section; } fn fileMemoryMapWrite(userdata: ?*anyopaque, mm: *File.MemoryMap) File.WritePositionalError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; - if (mm.section != null) return; - return mmSyncWrite(mm.file, mm.memory, mm.offset); + const section = mm.section orelse return mmSyncWrite(mm.file, mm.memory, mm.offset); + _ = section; } fn mmSyncRead(file: File, memory: []u8, offset: u64) File.ReadPositionalError!void { -- 2.54.0 From de347dd6a981151609f1b9e79183d3f44046abc0 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 15 Jan 2026 13:02:20 -0800 Subject: [PATCH 17/19] std.Io.Threaded: add missing free in MemoryMap.setLength --- lib/std/Io/Threaded.zig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 3d9b2301e9c34c77cf00a0576c9aca312fa6f4e2..c7cccf08d496d7401e4c0286989919e18bf9c9ac 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -16474,15 +16474,16 @@ fn fileMemoryMapSetLength( } } else { const gpa = t.allocator; - if (gpa.rawRemap(mm.memory, alignment, new_len, @returnAddress())) |new_ptr| { + if (gpa.rawRemap(old_memory, alignment, new_len, @returnAddress())) |new_ptr| { mm.memory = @alignCast(new_ptr[0..new_len]); } else { const new_ptr: [*]align(page_align) u8 = @alignCast( gpa.rawAlloc(new_len, alignment, @returnAddress()) orelse return error.OutOfMemory, ); - const copy_len = @min(new_len, mm.memory.len); - @memcpy(new_ptr[0..copy_len], mm.memory[0..copy_len]); + const copy_len = @min(new_len, old_memory.len); + @memcpy(new_ptr[0..copy_len], old_memory[0..copy_len]); mm.memory = new_ptr[0..new_len]; + gpa.rawFree(old_memory, alignment, @returnAddress()); } } } -- 2.54.0 From d2585e68a79b8e43166e797d6adc33282acac046 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 15 Jan 2026 13:48:27 -0800 Subject: [PATCH 18/19] std.Io.Threaded: use NtExtendSection in fileMemoryMapSetLength is this a good idea? not sure yet. reverting in the next commit --- lib/std/Io/Threaded.zig | 57 +++++++++++++++++++++++------------- lib/std/os/windows/ntdll.zig | 5 ++++ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index c7cccf08d496d7401e4c0286989919e18bf9c9ac..e0f5804abafe373c3dae01ed2df0585d3a6c0fae 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -16279,6 +16279,8 @@ fn createFileMap( .SUCCESS => {}, .CONFLICTING_ADDRESSES => return error.MappingAlreadyExists, .SECTION_PROTECTION => return error.PermissionDenied, + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_VIEW_SIZE => |status| return windows.statusBug(status), else => |status| return windows.unexpectedStatus(status), } if (builtin.mode == .Debug) { @@ -16392,32 +16394,47 @@ fn fileMemoryMapSetLength( const new_len = options.len; if (mm.section) |section| { - if (alignment.forward(new_len) == alignment.forward(old_memory.len)) { + const aligned_old_len = alignment.forward(old_memory.len); + const aligned_new_len = alignment.forward(new_len); + if (aligned_new_len == aligned_old_len) { mm.memory.len = new_len; return; } switch (native_os) { .windows => { - var contents_ptr: ?[*]align(page_align) u8 = null; - var contents_len = new_len; - switch (windows.ntdll.NtMapViewOfSection( - section, - windows.current_process, - @ptrCast(&contents_ptr), - null, - 0, - null, - &contents_len, - .Unmap, - .{}, - .{ .READWRITE = true }, - )) { - .SUCCESS => {}, - .SECTION_PROTECTION => return error.PermissionDenied, - else => |status| return windows.unexpectedStatus(status), + if (aligned_new_len > aligned_old_len) { + var new_section_size: windows.LARGE_INTEGER = @intCast(aligned_new_len); + switch (windows.ntdll.NtExtendSection(section, &new_section_size)) { + .SUCCESS => {}, + else => |status| return windows.unexpectedStatus(status), + } + assert(new_section_size == aligned_new_len); + mm.memory.len = new_len; + } else { + _ = windows.ntdll.NtUnmapViewOfSection(windows.current_process, old_memory.ptr); + windows.CloseHandle(section); + var contents_len = aligned_new_len; + var contents_ptr: ?[*]align(std.heap.page_size_min) u8 = null; + switch (windows.ntdll.NtMapViewOfSection( + section, + windows.current_process, + @ptrCast(&contents_ptr), + null, + 0, + null, + &contents_len, + .Unmap, + .{}, + .{ .READWRITE = true }, + )) { + .SUCCESS => {}, + .SECTION_PROTECTION => return error.PermissionDenied, + .INVALID_VIEW_SIZE => |status| return windows.statusBug(status), + else => |status| return windows.unexpectedStatus(status), + } + assert(contents_len == aligned_new_len); + mm.memory = contents_ptr.?[0..new_len]; } - assert(contents_len == alignment.forward(new_len)); - mm.memory = contents_ptr.?[0..new_len]; }, .wasi => unreachable, .linux => { diff --git a/lib/std/os/windows/ntdll.zig b/lib/std/os/windows/ntdll.zig index 0a424fa7b2f150384bff5bbdae61d84b5e5fabb2..1cc17c0be6f56ef03909df8af7742c15472c9b93 100644 --- a/lib/std/os/windows/ntdll.zig +++ b/lib/std/os/windows/ntdll.zig @@ -253,6 +253,11 @@ pub extern "ntdll" fn NtCreateSection( FileHandle: ?HANDLE, ) callconv(.winapi) NTSTATUS; +pub extern "ntdll" fn NtExtendSection( + SectionHandle: HANDLE, + NewSectionSize: *LARGE_INTEGER, +) callconv(.winapi) NTSTATUS; + pub extern "ntdll" fn NtAllocateVirtualMemory( ProcessHandle: HANDLE, BaseAddress: *PVOID, -- 2.54.0 From 7b21fd7244ed1b0459b542346e96a20e91138f69 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 15 Jan 2026 14:10:09 -0800 Subject: [PATCH 19/19] std.Io.Threaded: fix memory mapping on windows - set protection flags properly - handle when mapping fails after unmapping --- lib/std/Io/File/MemoryMap.zig | 5 ++ lib/std/Io/Threaded.zig | 92 +++++++++++++---------------------- lib/std/Io/test.zig | 7 +-- lib/std/os/windows.zig | 14 ++++++ lib/std/process.zig | 11 +---- 5 files changed, 59 insertions(+), 70 deletions(-) diff --git a/lib/std/Io/File/MemoryMap.zig b/lib/std/Io/File/MemoryMap.zig index 2b917840a14409ce0008c63f47698aff4dee32ab..b3196aab3e420a95113822afd4bf99d584556d3e 100644 --- a/lib/std/Io/File/MemoryMap.zig +++ b/lib/std/Io/File/MemoryMap.zig @@ -95,6 +95,11 @@ pub const SetLengthError = error{ /// `options` is needed because the mapping may need to be destroyed and /// re-created. All the same options must be provided except for `len` which is /// the new length. +/// +/// This operation cannot be completed atomically on all operating systems. +/// When this function fails, the `MemoryMap` may be left in an unmapped state, +/// which can be detected by checking if `memory.len` is zero. In such case it +/// is safe to call `destroy` which will have no effect. pub fn setLength(mm: *MemoryMap, io: Io, options: CreateOptions) SetLengthError!void { return io.vtable.fileMemoryMapSetLength(io.userdata, mm, options); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index e0f5804abafe373c3dae01ed2df0585d3a6c0fae..db85f2e52c0a5f2c5bfeb6ddfaf15edc7c14e27b 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -16238,6 +16238,8 @@ fn createFileMap( try Thread.checkCancel(); var section = windows.INVALID_HANDLE_VALUE; + const section_size: windows.LARGE_INTEGER = @intCast(len); + const page = windows.PAGE.fromProtection(protection) orelse return error.AccessDenied; switch (windows.ntdll.NtCreateSection( §ion, .{ @@ -16251,8 +16253,8 @@ fn createFileMap( .STANDARD = .{ .RIGHTS = .REQUIRED }, }, null, - @constCast(&@as(i64, @intCast(len))), - .{ .READWRITE = true }, + §ion_size, + page, .{ .COMMIT = populate }, file.handle, )) { @@ -16260,6 +16262,7 @@ fn createFileMap( .FILE_LOCK_CONFLICT => return error.FileLockConflict, .INVALID_FILE_FOR_SECTION => return error.OperationUnsupported, .ACCESS_DENIED => return error.AccessDenied, + .SECTION_TOO_BIG => return error.SectionOversize, else => |status| return windows.unexpectedStatus(status), } var contents_ptr: ?[*]align(std.heap.page_size_min) u8 = null; @@ -16274,7 +16277,7 @@ fn createFileMap( &contents_len, .Unmap, .{}, - .{ .READWRITE = true }, + page, )) { .SUCCESS => {}, .CONFLICTING_ADDRESSES => return error.MappingAlreadyExists, @@ -16363,16 +16366,20 @@ fn fileMemoryMapDestroy(userdata: ?*anyopaque, mm: *File.MemoryMap) void { const memory = mm.memory; if (mm.section) |section| switch (native_os) { .windows => { + if (section == windows.INVALID_HANDLE_VALUE) return; _ = windows.ntdll.NtUnmapViewOfSection(windows.current_process, memory.ptr); windows.CloseHandle(section); }, .wasi => unreachable, - else => switch (posix.errno(posix.system.munmap(memory.ptr, memory.len))) { - .SUCCESS => {}, - else => |e| { - if (builtin.mode == .Debug) - std.log.err("failed to unmap {d} bytes at {*}: {t}", .{ memory.len, memory.ptr, e }); - }, + else => { + if (memory.len == 0) return; + switch (posix.errno(posix.system.munmap(memory.ptr, memory.len))) { + .SUCCESS => {}, + else => |e| { + if (builtin.mode == .Debug) + std.log.err("failed to unmap {d} bytes at {*}: {t}", .{ memory.len, memory.ptr, e }); + }, + } }, } else { const gpa = t.allocator; @@ -16394,47 +16401,16 @@ fn fileMemoryMapSetLength( const new_len = options.len; if (mm.section) |section| { - const aligned_old_len = alignment.forward(old_memory.len); - const aligned_new_len = alignment.forward(new_len); - if (aligned_new_len == aligned_old_len) { + if (alignment.forward(new_len) == alignment.forward(old_memory.len)) { mm.memory.len = new_len; return; } switch (native_os) { .windows => { - if (aligned_new_len > aligned_old_len) { - var new_section_size: windows.LARGE_INTEGER = @intCast(aligned_new_len); - switch (windows.ntdll.NtExtendSection(section, &new_section_size)) { - .SUCCESS => {}, - else => |status| return windows.unexpectedStatus(status), - } - assert(new_section_size == aligned_new_len); - mm.memory.len = new_len; - } else { - _ = windows.ntdll.NtUnmapViewOfSection(windows.current_process, old_memory.ptr); - windows.CloseHandle(section); - var contents_len = aligned_new_len; - var contents_ptr: ?[*]align(std.heap.page_size_min) u8 = null; - switch (windows.ntdll.NtMapViewOfSection( - section, - windows.current_process, - @ptrCast(&contents_ptr), - null, - 0, - null, - &contents_len, - .Unmap, - .{}, - .{ .READWRITE = true }, - )) { - .SUCCESS => {}, - .SECTION_PROTECTION => return error.PermissionDenied, - .INVALID_VIEW_SIZE => |status| return windows.statusBug(status), - else => |status| return windows.unexpectedStatus(status), - } - assert(contents_len == aligned_new_len); - mm.memory = contents_ptr.?[0..new_len]; - } + _ = windows.ntdll.NtUnmapViewOfSection(windows.current_process, old_memory.ptr); + windows.CloseHandle(section); + mm.section = windows.INVALID_HANDLE_VALUE; + mm.memory = &.{}; }, .wasi => unreachable, .linux => { @@ -16463,6 +16439,7 @@ fn fileMemoryMapSetLength( } }; mm.memory = new_memory; + return; }, else => { switch (posix.errno(posix.system.munmap(old_memory.ptr, old_memory.len))) { @@ -16475,20 +16452,21 @@ fn fileMemoryMapSetLength( return error.Unexpected; }, } - if (createFileMap(mm.file, options.protection, mm.offset, options.populate, new_len)) |result| { - mm.* = result; - return; - } else |err| switch (err) { - error.OperationUnsupported, - error.Unseekable, - error.SectionOversize, - error.MappingAlreadyExists, - error.FileLockConflict, - => return error.Unexpected, // It worked before on the same open file. - else => |e| return e, - } + mm.memory = &.{}; }, } + if (createFileMap(mm.file, options.protection, mm.offset, options.populate, new_len)) |result| { + mm.* = result; + return; + } else |err| switch (err) { + error.OperationUnsupported, + error.Unseekable, + error.SectionOversize, + error.MappingAlreadyExists, + error.FileLockConflict, + => return error.Unexpected, // It worked before on the same open file. + else => |e| return e, + } } else { const gpa = t.allocator; if (gpa.rawRemap(old_memory, alignment, new_len, @returnAddress())) |new_ptr| { diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index 0f4285b9bd92183cb9efa265e22ac2aec4042baa..6d06ebacaaefffcec8d1b4f377b38bb983a42f78 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -629,19 +629,20 @@ test "memory mapping" { try expectEqualStrings("this9is9my data123", updated_contents); { - var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_only }); + var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_write }); defer file.close(io); var mm = try file.createMemoryMap(io, .{ .len = "this9is9my".len, - .protection = .{ .read = true }, }); defer mm.destroy(io); try expectEqualStrings("this9is9my", mm.memory); // Cross a page boundary to require an actual remap. - try mm.setLength(io, .{ .len = std.heap.pageSize() * 2 }); + try mm.setLength(io, .{ + .len = std.heap.pageSize() * 2, + }); try mm.read(io); try expectEqualStrings("this9is9my data123\x00\x00", mm.memory[0.."this9is9my data123\x00\x00".len]); diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 91f3cb6513fd6969b98dd0cc67b2e922268bde42..c94596de8e55be5c8d5689c5e4a53b815aafc7ae 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -2126,6 +2126,20 @@ pub const PAGE = packed struct(ULONG) { Reserved19: u12 = 0, REVERT_TO_FILE_MAP: bool = false, + + pub fn fromProtection(protection: std.process.MemoryProtection) ?PAGE { + // TODO https://github.com/ziglang/zig/issues/22214 + return switch (@as(u3, @bitCast(protection))) { + 0b000 => .{ .NOACCESS = true }, + 0b001 => .{ .READONLY = true }, + 0b010 => null, + 0b011 => .{ .READWRITE = true }, + 0b100 => .{ .EXECUTE = true }, + 0b101 => .{ .EXECUTE_READ = true }, + 0b110 => null, + 0b111 => .{ .EXECUTE_READWRITE = true }, + }; + } }; pub const MEM = struct { diff --git a/lib/std/process.zig b/lib/std/process.zig index 3f085037a30b5761687ba0195b96791f38049736..8c531830e54240c320ffab82f12db006bbdf47d6 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -1030,16 +1030,7 @@ pub fn protectMemory(memory: []align(std.heap.page_size_min) u8, protection: Mem var size = memory.len; // ntdll takes an extra level of indirection here var old: windows.PAGE = undefined; const current_process: windows.HANDLE = @ptrFromInt(@as(usize, @bitCast(@as(isize, -1)))); - const new: windows.PAGE = switch (@as(u3, @bitCast(protection))) { - 0b000 => .{ .NOACCESS = true }, - 0b001 => .{ .READONLY = true }, - 0b010 => return error.AccessDenied, // +w -r not allowed - 0b011 => .{ .READWRITE = true }, - 0b100 => .{ .EXECUTE = true }, - 0b101 => .{ .EXECUTE_READ = true }, - 0b110 => return error.AccessDenied, // +w -r not allowed - 0b111 => .{ .EXECUTE_READWRITE = true }, - }; + const new = windows.PAGE.fromProtection(protection) orelse return error.AccessDenied; switch (windows.ntdll.NtProtectVirtualMemory(current_process, @ptrCast(&addr), &size, new, &old)) { .SUCCESS => return, .INVALID_ADDRESS => return error.AccessDenied, -- 2.54.0