diff --git a/lib/std/Io.zig b/lib/std/Io.zig index b4d6efc715f163e0b59300d355284423d8b33382..bf12ee6c6cd6098e3d4a9e218a3b18326724747f 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, 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, + 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..cc1a58a15e2fa78878eca1fd84458397cb4b6412 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -14,12 +14,15 @@ 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; 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, @@ -63,6 +66,10 @@ 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. + block_size: BlockSize, }; pub fn stdout() File { @@ -529,7 +536,27 @@ 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, + /// 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. /// @@ -539,7 +566,33 @@ 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, + /// File was not opened with write capability. + NotOpenForWriting, +} || Io.Cancelable || Io.UnexpectedError; /// See also: /// * `writer` @@ -740,8 +793,13 @@ 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; _ = Atomic; + _ = MemoryMap; } diff --git a/lib/std/Io/File/MemoryMap.zig b/lib/std/Io/File/MemoryMap.zig new file mode 100644 index 0000000000000000000000000000000000000000..b3196aab3e420a95113822afd4bf99d584556d3e --- /dev/null +++ b/lib/std/Io/File/MemoryMap.zig @@ -0,0 +1,119 @@ +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. Page-aligned. +offset: u64, +/// Memory that may or may not remain consistent with file contents. Use `read` +/// 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, + +pub const Section = if (is_windows) std.os.windows.HANDLE else void; + +pub const CreateError = 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.ReadPositionalError; + +pub const CreateOptions = struct { + /// 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. + /// + /// 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. + 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. 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`. + offset: u64 = 0, +}; + +/// 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); +} + +/// 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{ + /// 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`. +/// +/// `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); +} + +/// 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`. +/// +/// 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/File/Reader.zig b/lib/std/Io/File/Reader.zig index 0c573c9ae1423ef9ebe8d1fee5e26d477cfec008..f400f2c51439b65bbe7e11ef3dfa0d5b49b4f3bf 100644 --- a/lib/std/Io/File/Reader.zig +++ b/lib/std/Io/File/Reader.zig @@ -29,12 +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. + /// File was not opened with read capability. NotOpenForReading, SocketUnconnected, /// Non-blocking has been enabled, and reading from the file descriptor diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 735efa9992d279b8cffeb63cc2abe6ee4a2e70d2..db85f2e52c0a5f2c5bfeb6ddfaf15edc7c14e27b 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 = .{}, @@ -51,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, @@ -69,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, @@ -1214,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: @@ -1241,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(); @@ -1257,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) { @@ -1293,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; @@ -1490,6 +1509,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 +1667,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 +1764,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, @@ -2908,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; @@ -2970,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); @@ -7889,7 +7954,7 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: File, data: []const []u8) syscall.finish(); return nread; }, - .INTR => { + .INTR, .TIMEDOUT => { try syscall.checkCancel(); continue; }, @@ -7898,14 +7963,13 @@ 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, .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), } @@ -7922,7 +7986,7 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: File, data: []const []u8) syscall.finish(); return @intCast(rc); }, - .INTR => { + .INTR, .TIMEDOUT => { try syscall.checkCancel(); continue; }, @@ -7932,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| { - if (native_os == .wasi) return error.NotOpenForReading; // File operation on directory. - return errnoBug(err); // File descriptor used after closed. + .BADF => { + if (native_os == .wasi) return error.IsDir; // File operation on directory. + return error.NotOpenForReading; }, .IO => return error.InputOutput, .ISDIR => return error.IsDir, @@ -7942,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), } }, @@ -7981,10 +8044,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), @@ -8024,31 +8087,25 @@ fn fileReadPositionalPosix(userdata: ?*anyopaque, file: File, data: []const []u8 syscall.finish(); return nread; }, - .INTR => { + .INTR, .TIMEDOUT => { 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 + .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), + .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 +8118,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 => { 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 error.NotOpenForReading; }, + else => |err| return syscall.unexpectedErrno(err), } } } @@ -8101,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, @@ -8141,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), @@ -8739,7 +8794,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 => return error.NotOpenForWriting, .DESTADDRREQ => |err| return errnoBug(err), // `connect` was never called. .DQUOT => return error.DiskQuota, .FBIG => return error.FileTooBig, @@ -8770,29 +8825,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 => return syscall.fail(error.NotOpenForWriting), + .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), } } } @@ -8830,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), @@ -12458,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 = .{ @@ -12469,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 { @@ -12487,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, }; } @@ -12535,6 +12588,7 @@ fn statFromPosix(st: *const posix.Stat) File.Stat { .atime = timestampFromPosix(&atime), .mtime = timestampFromPosix(&mtime), .ctime = timestampFromPosix(&ctime), + .block_size = @intCast(st.blksize), }; } @@ -12556,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, }; } @@ -16107,3 +16162,529 @@ 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 len = options.len; + + 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, + error.OperationUnsupported => {}, + else => { + if (builtin.mode == .Debug) + std.log.warn("memory mapping failed with {t}, falling back to file operations", .{err}); + }, + } + } + + const gpa = t.allocator; + const page_size = std.heap.pageSize(); + const alignment: Alignment = .fromByteUnits(page_size); + const memory = m: { + const ptr = gpa.rawAlloc(len, alignment, @returnAddress()) orelse return error.OutOfMemory; + break :m ptr[0..len]; + }; + errdefer gpa.rawFree(memory, alignment, @returnAddress()); + + if (!options.undefined_contents) try mmSyncRead(file, memory, offset); + + return .{ + .file = file, + .offset = offset, + .memory = @alignCast(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, + FileLockConflict, +} || Io.Cancelable || Io.UnexpectedError; + +fn createFileMap( + file: File, + protection: std.process.MemoryProtection, + offset: u64, + populate: bool, + len: usize, +) CreateFileMapError!File.MemoryMap { + if (is_windows) { + 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, + .{ + .SPECIFIC = .{ .SECTION = .{ + .QUERY = true, + .MAP_WRITE = protection.write, + .MAP_READ = protection.read, + .MAP_EXECUTE = protection.execute, + .EXTEND_SIZE = true, + } }, + .STANDARD = .{ .RIGHTS = .REQUIRED }, + }, + null, + §ion_size, + page, + .{ .COMMIT = populate }, + file.handle, + )) { + .SUCCESS => {}, + .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; + var contents_len = len; + switch (windows.ntdll.NtMapViewOfSection( + section, + windows.current_process, + @ptrCast(&contents_ptr), + null, + 0, + null, + &contents_len, + .Unmap, + .{}, + page, + )) { + .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) { + 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..len], + .section = section, + }; + } else if (have_mmap) { + const prot: posix.PROT = .{ + .READ = protection.read, + .WRITE = protection.write, + .EXEC = protection.execute, + }; + const flags: posix.MAP = switch (native_os) { + .linux => .{ + .TYPE = .SHARED_VALIDATE, + .POPULATE = populate, + }, + else => .{ + .TYPE = .SHARED, + }, + }; + + 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; + 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..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..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| 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 => { + 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; + gpa.rawFree(memory, .fromByteUnits(std.heap.pageSize()), @returnAddress()); + } + mm.* = undefined; +} + +fn fileMemoryMapSetLength( + userdata: ?*anyopaque, + mm: *File.MemoryMap, + 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(old_memory.len)) { + mm.memory.len = new_len; + return; + } + switch (native_os) { + .windows => { + _ = windows.ntdll.NtUnmapViewOfSection(windows.current_process, old_memory.ptr); + windows.CloseHandle(section); + mm.section = windows.INVALID_HANDLE_VALUE; + mm.memory = &.{}; + }, + .wasi => unreachable, + .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(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]; + 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..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; + return; + }, + 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; + }, + } + 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| { + 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, 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()); + } + } +} + +fn fileMemoryMapRead(userdata: ?*anyopaque, mm: *File.MemoryMap) File.ReadPositionalError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + 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; + 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 { + if (is_windows) { + 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(); + 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(); + @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 { + if (is_windows) { + 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; + 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 => return syscall.fail(error.NotOpenForWriting), + .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/Io/Threaded/test.zig b/lib/std/Io/Threaded/test.zig index 34aa4a8c689d67ceef1d1ca8e985b5487da9e2f1..9ab9c6dcd98b51414611c7e492663d6e01aa795f 100644 --- a/lib/std/Io/Threaded/test.zig +++ b/lib/std/Io/Threaded/test.zig @@ -204,3 +204,65 @@ test "cancel blocked read from pipe" { try io.sleep(.fromMilliseconds(10), .awake); try future.cancel(io); } + +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, + .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, .{ .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 da928c610259963916657863495ddfd5ed65434a..6d06ebacaaefffcec8d1b4f377b38bb983a42f78 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -592,3 +592,59 @@ test "randomSecure" { // that two sets of 50 bytes were equal. try expect(!mem.eql(u8, &buf_a, &buf_b)); } + +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; + + 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", .{ .mode = .read_write }); + defer file.close(io); + + var mm = try file.createMemoryMap(io, .{ .len = "this is my data123".len }); + defer mm.destroy(io); + + try expectEqualStrings("this is my data123", 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 expectEqualStrings("this9is9my data123", updated_contents); + + { + 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, + }); + 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.read(io); + + try expectEqualStrings("this9is9my data123\x00\x00", mm.memory[0.."this9is9my data123\x00\x00".len]); + } +} diff --git a/lib/std/c.zig b/lib/std/c.zig index 5180dfbfa7bb4052e58d85d0cbfa8ffde33d9058..535f0f79090bd88e70bbef0f112f64cd7ff76529 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, @@ -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; 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 })); 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. /// diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 1e864d14c277f4d09954f388559bf4255a4bd821..c94596de8e55be5c8d5689c5e4a53b815aafc7ae 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 @@ -2124,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/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, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 93e711238a68764b9e0719c90be00e356fa21887..f1f4e279d1726ad4997dc2f2009755887aa3c520 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; @@ -433,14 +434,14 @@ 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, .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/process.zig b/lib/std/process.zig index 64badc1cba33a1dbf06ec6ce711167fa58ba2b24..8c531830e54240c320ffab82f12db006bbdf47d6 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -1018,31 +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))) { - 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, @@ -1050,9 +1038,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..0e7d814a71f1f28f424509d481a1106282924bd2 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -420,7 +420,6 @@ 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, diff --git a/src/link/Dwarf.zig b/src/link/Dwarf.zig index d3fff513f43623f296b59b3ee770aa72a15d9471..19bbee45b3a12d6883f3e7803475eb960c5a1784 100644 --- a/src/link/Dwarf.zig +++ b/src/link/Dwarf.zig @@ -49,6 +49,10 @@ pub const UpdateError = error{ Underflow, UnexpectedEndOfFile, NonResizable, + /// TODO why is this in the error set? + ConnectionResetByPeer, + /// TODO why is this in the error set? + SocketUnconnected, } || codegen.GenerateSymbolError || Io.File.OpenError || 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");