From 5a7dc4b0fae62c8b7ec798043c93e4d90704a638 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 14:41:31 -0800 Subject: [PATCH] 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 b4d6efc715..1da10a027f 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 27833d4d8a..e622a6dd3c 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 0000000000..848a8837d1 --- /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 0c573c9ae1..c2842ee45f 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 735efa9992..09d69682ce 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 93e711238a..b607cc8134 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 64badc1cba..3f085037a3 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 45e777bb93..d0cd0721bb 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 c6e6535961..2580ed5c2a 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");