From c4ae69e6e18628dbd85127c95a0303d76f6f6175 Mon Sep 17 00:00:00 2001 From: Reuben Dunnington Date: Fri, 17 May 2024 12:47:51 -0700 Subject: [PATCH] mem64 now actually tests offsets >4GB, fixed a bunch of bugs --- .github/workflows/ci.yml | 6 +++- src/definition.zig | 33 ++++++++++++++++-- src/instance.zig | 12 +++---- src/vm_stack.zig | 16 ++++++--- src/zig-stable-array/stable_array.zig | 49 +++++++++++++++++++++++++-- test/mem64/memtest.c | 6 ++-- test/wasm/main.zig | 28 +++++++++++++-- 7 files changed, 127 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a224f86..1a58486 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: python-version: '3.11' cache: pip - - name: Install dependencies + - name: Install python dependencies working-directory: test/wasi/wasi-testsuite/test-runner run: python3 -m pip install -r requirements.txt @@ -44,6 +44,10 @@ jobs: run: | zig build test-wasm -- --log-suite + - name: Run mem64 test + run: | + zig build test-mem64 + - name: Run wasi testsuite run: | zig build test-wasi diff --git a/src/definition.zig b/src/definition.zig index 92a8555..c78dcdc 100644 --- a/src/definition.zig +++ b/src/definition.zig @@ -284,6 +284,14 @@ pub const Limits = struct { // 0x06 n:u64 ⇒ i64, {min n, max ?}, 1 ;; from threads proposal // 0x07 n:u64 m:u64 ⇒ i64, {min n, max m}, 1 ;; from threads proposal + const k_max_bytes_i32 = k_max_pages_i32 * MemoryDefinition.k_page_size; + const k_max_pages_i32 = std.math.powi(usize, 2, 16) catch unreachable; + + // Technically the max bytes should be maxInt(u64), but that is wayyy more memory than PCs have available and + // is just a waste of virtual address space in the implementation. Instead we'll set the upper limit to 128GB. + const k_max_bytes_i64 = (1024 * 1024 * 1024 * 128); + const k_max_pages_i64 = k_max_bytes_i64 / MemoryDefinition.k_page_size; + fn decode(reader: anytype) !Limits { const limit_type: u8 = try reader.readByte(); @@ -327,6 +335,18 @@ pub const Limits = struct { pub fn indexType(self: Limits) ValType { return if (self.limit_type < 4) .I32 else .I64; } + + pub fn maxPages(self: Limits) usize { + if (self.max) |max| { + return @max(1, max); + } + + return self.indexTypeMaxPages(); + } + + pub fn indexTypeMaxPages(self: Limits) usize { + return if (self.limit_type < 4) k_max_pages_i32 else k_max_pages_i64; + } }; const BlockType = enum { @@ -652,7 +672,6 @@ pub const MemoryDefinition = struct { limits: Limits, pub const k_page_size: usize = 64 * 1024; - pub const k_max_pages: usize = std.math.powi(usize, 2, 16) catch unreachable; }; pub const ElementMode = enum { @@ -2942,7 +2961,11 @@ pub const ModuleDefinition = struct { while (memory_index < num_memories) : (memory_index += 1) { var limits = try Limits.decode(reader); - if (limits.min > MemoryDefinition.k_max_pages) { + if (limits.min > limits.maxPages()) { + self.log.err( + "Validation error: max memory pages exceeded. Got {} but max is {}", + .{ limits.min, limits.indexTypeMaxPages() }, + ); return error.ValidationMemoryMaxPagesExceeded; } @@ -2950,7 +2973,11 @@ pub const ModuleDefinition = struct { if (max < limits.min) { return error.ValidationMemoryInvalidMaxLimit; } - if (max > MemoryDefinition.k_max_pages) { + if (max > limits.indexTypeMaxPages()) { + self.log.err( + "Validation error: max memory pages exceeded. Got {} but max is {}", + .{ max, limits.indexTypeMaxPages() }, + ); return error.ValidationMemoryMaxPagesExceeded; } } diff --git a/src/instance.zig b/src/instance.zig index 428206e..01b3585 100644 --- a/src/instance.zig +++ b/src/instance.zig @@ -240,16 +240,16 @@ pub const MemoryInstance = struct { }; pub const k_page_size: usize = MemoryDefinition.k_page_size; - pub const k_max_pages: usize = MemoryDefinition.k_max_pages; limits: Limits, mem: BackingMemory, pub fn init(limits: Limits, params: ?WasmMemoryExternal) MemoryInstance { - const max_pages = if (limits.max) |max| @max(1, max) else k_max_pages; + const max_pages = limits.maxPages(); + const max_bytes: u64 = max_pages * k_page_size; var mem = if (params == null) BackingMemory{ - .Internal = StableArray(u8).init(max_pages * k_page_size), + .Internal = StableArray(u8).init(@intCast(max_bytes)), } else BackingMemory{ .External = .{ .buffer = &[0]u8{}, .params = params.?, @@ -258,7 +258,7 @@ pub const MemoryInstance = struct { var instance = MemoryInstance{ .limits = Limits{ .min = 0, - .max = @as(u32, @intCast(max_pages)), + .max = max_pages, .limit_type = limits.limit_type, }, .mem = mem, @@ -287,7 +287,7 @@ pub const MemoryInstance = struct { } const total_pages = self.limits.min + num_pages; - const max_pages = if (self.limits.max) |max| max else k_max_pages; + const max_pages = self.limits.maxPages(); if (total_pages > max_pages) { return false; @@ -306,7 +306,7 @@ pub const MemoryInstance = struct { }, } - self.limits.min = @as(u32, @intCast(total_pages)); + self.limits.min = total_pages; return true; } diff --git a/src/vm_stack.zig b/src/vm_stack.zig index 93eaa20..5f840c5 100644 --- a/src/vm_stack.zig +++ b/src/vm_stack.zig @@ -262,16 +262,16 @@ const Stack = struct { stack.num_labels -= 1; } - fn findLabel(stack: *const Stack, id: u32) *const Label { + fn findLabel(stack: Stack, id: u32) *const Label { const index: usize = (stack.num_labels - 1) - id; return &stack.labels[index]; } - fn topLabel(stack: *const Stack) *const Label { + fn topLabel(stack: Stack) *const Label { return &stack.labels[stack.num_labels - 1]; } - fn frameLabel(stack: *const Stack) *const Label { + fn frameLabel(stack: Stack) *const Label { var frame: *const CallFrame = stack.topFrame(); var frame_label: *const Label = &stack.labels[frame.start_offset_labels]; return frame_label; @@ -359,7 +359,7 @@ const Stack = struct { return null; } - fn topFrame(stack: *const Stack) *CallFrame { + fn topFrame(stack: Stack) *CallFrame { return &stack.frames[stack.num_frames - 1]; } @@ -368,6 +368,14 @@ const Stack = struct { stack.num_labels = 0; stack.num_frames = 0; } + + fn debugDump(stack: Stack) void { + std.debug.print("===== stack dump =====\n", .{}); + for (stack.values[0..stack.num_values]) |val| { + std.debug.print("I32: {}, I64: {}, F32: {}, F64: {}\n", .{ val.I32, val.I64, val.F32, val.F64 }); + } + std.debug.print("======================\n", .{}); + } }; // TODO move all definition stuff into definition.zig and vm stuff into vm_stack.zig diff --git a/src/zig-stable-array/stable_array.zig b/src/zig-stable-array/stable_array.zig index e6d39a7..626459e 100644 --- a/src/zig-stable-array/stable_array.zig +++ b/src/zig-stable-array/stable_array.zig @@ -263,11 +263,14 @@ pub fn StableArrayAligned(comptime T: type, comptime alignment: u29) type { self.items.ptr = @alignCast(@ptrCast(addr)); self.items.len = 0; } else { - const prot: u32 = std.c.PROT.READ | std.c.PROT.WRITE; + const prot: u32 = std.c.PROT.NONE; const map: u32 = std.c.MAP.PRIVATE | std.c.MAP.ANONYMOUS; const fd: os.fd_t = -1; const offset: usize = 0; - var slice = try os.mmap(null, self.max_virtual_alloc_bytes, prot, map, fd, offset); + var slice = os.mmap(null, self.max_virtual_alloc_bytes, prot, map, fd, offset) catch |e| { + std.debug.print("caught initial sizing error {}, total bytes: {}\n", .{ e, self.max_virtual_alloc_bytes }); + return e; + }; self.items.ptr = @alignCast(@ptrCast(slice.ptr)); self.items.len = 0; } @@ -279,6 +282,20 @@ pub fn StableArrayAligned(comptime T: type, comptime alignment: u29) type { if (builtin.os.tag == .windows) { const w = std.os.windows; _ = try w.VirtualAlloc(@as(w.PVOID, @ptrCast(self.items.ptr)), new_capacity_bytes, w.MEM_COMMIT, w.PAGE_READWRITE); + } else { + const resize_capacity = new_capacity_bytes - current_capacity_bytes; + const region_begin: [*]u8 = @ptrCast(self.items.ptr); + const remap_region_begin: [*]u8 = region_begin + current_capacity_bytes; + + const prot: u32 = std.c.PROT.READ | std.c.PROT.WRITE; + const map: u32 = std.c.MAP.PRIVATE | std.c.MAP.ANONYMOUS | std.c.MAP.FIXED; + const fd: os.fd_t = -1; + const offset: usize = 0; + + _ = os.mmap(@alignCast(remap_region_begin), resize_capacity, prot, map, fd, offset) catch |e| { + std.debug.print("caught error {}\n", .{e}); + return e; + }; } } @@ -395,6 +412,7 @@ test "shrinkAndFree" { test "resize" { const max: usize = 1024 * 1024 * 1; var a = StableArray(u8).init(max); + defer a.deinit(); var size: usize = 512; while (size <= max) { @@ -405,6 +423,8 @@ test "resize" { test "out of memory" { var a = StableArrayAligned(u8, mem.page_size).init(TEST_VIRTUAL_ALLOC_SIZE); + defer a.deinit(); + const max_capacity: usize = TEST_VIRTUAL_ALLOC_SIZE / mem.page_size; try a.appendNTimes(0xFF, max_capacity); for (a.items) |v| { @@ -420,5 +440,28 @@ test "out of memory" { assert(err == error.OutOfMemory); }; assert(didCatchError == true); - a.deinit(); +} + +test "huge max size" { + const KB = 1024; + const MB = KB * 1024; + const GB = MB * 1024; + + var a = StableArray(u8).init(GB * 128); + defer a.deinit(); + + try a.resize(MB * 4); + try a.resize(MB * 8); + try a.resize(MB * 16); + a.items[MB * 16 - 1] = 0xFF; +} + +test "growing retains values" { + var a = StableArray(u8).init(TEST_VIRTUAL_ALLOC_SIZE); + defer a.deinit(); + + try a.resize(mem.page_size); + a.items[0] = 0xFF; + try a.resize(mem.page_size * 2); + assert(a.items[0] == 0xFF); } diff --git a/test/mem64/memtest.c b/test/mem64/memtest.c index 1b1bdf7..1043d55 100644 --- a/test/mem64/memtest.c +++ b/test/mem64/memtest.c @@ -15,15 +15,15 @@ __attribute__((visibility("default"))) int64_t memtest(int32_t val_i32, int64_t val_i64, float val_f32, double val_f64) { - int64_t start_page = __builtin_wasm_memory_grow(0, PAGES_PER_GB * 2); // memory.grow + int64_t start_page = __builtin_wasm_memory_grow(0, PAGES_PER_GB * 6); // memory.grow assert(start_page != -1); - char* mem = (char*)(start_page); + char* mem = (char*)(start_page) + GB * 4; volatile char* mem_stores = mem + MB * 1; volatile char* mem_loads = mem + MB * 2; int64_t num_pages = __builtin_wasm_memory_size(0); // memory.size - assert(num_pages >= (PAGES_PER_GB * 2)); + assert(num_pages >= (PAGES_PER_GB * 6)); *(int32_t*)(mem_loads + 0) = val_i32; // i32.store *(int64_t*)(mem_loads + 8) = val_i64; // i64.store diff --git a/test/wasm/main.zig b/test/wasm/main.zig index 461c1e3..e94f938 100644 --- a/test/wasm/main.zig +++ b/test/wasm/main.zig @@ -630,6 +630,7 @@ const TestOpts = struct { trace_mode: bytebox.DebugTrace.Mode = .None, force_wasm_regen_only: bool = false, log_suite: bool = false, + log: bytebox.Logger = bytebox.Logger.empty(), }; fn makeSpectestImports(allocator: std.mem.Allocator) !bytebox.ModuleImportPackage { @@ -872,7 +873,11 @@ fn run(allocator: std.mem.Allocator, suite_path: []const u8, opts: *const TestOp module.filename = try allocator.dupe(u8, module_filename); - module.def = try bytebox.createModuleDefinition(allocator, .{ .debug_name = std.fs.path.basename(module_filename) }); + const module_def_opts = bytebox.ModuleDefinitionOpts{ + .debug_name = std.fs.path.basename(module_filename), + .log = opts.log, + }; + module.def = try bytebox.createModuleDefinition(allocator, module_def_opts); (module.def.?).decode(module_data) catch |e| { var expected_str_or_null: ?[]const u8 = null; if (decode_expected_error) |unwrapped_expected| { @@ -935,7 +940,12 @@ fn run(allocator: std.mem.Allocator, suite_path: []const u8, opts: *const TestOp } module.inst = try bytebox.createModuleInstance(opts.vm_type, module.def.?, allocator); - (module.inst.?).instantiate(.{ .imports = imports.items }) catch |e| { + + const instantiate_opts = bytebox.ModuleInstantiateOpts{ + .imports = imports.items, + .log = opts.log, + }; + (module.inst.?).instantiate(instantiate_opts) catch |e| { if (instantiate_expected_error) |expected_str| { if (isSameError(e, expected_str)) { logVerbose("\tSuccess!\n", .{}); @@ -1081,7 +1091,14 @@ fn run(allocator: std.mem.Allocator, suite_path: []const u8, opts: *const TestOp print("assert_return: {s}:{s}({any})\n", .{ module.filename, c.action.field, c.action.args.items }); } - print("\tFail on return {}/{}. Expected: {}, Actual: {}\n", .{ i + 1, returns.len, expected_value, r }); + const format_str = "\tFail on return {}/{}. Expected: {}, Actual: {}\n"; + switch (expected_value.type) { + .I32 => print(format_str, .{ i + 1, returns.len, expected_value.val.I32, r.I32 }), + .I64 => print(format_str, .{ i + 1, returns.len, expected_value.val.I64, r.I64 }), + .F32 => print(format_str, .{ i + 1, returns.len, expected_value.val.F32, r.F32 }), + .F64 => print(format_str, .{ i + 1, returns.len, expected_value.val.F64, r.F64 }), + else => unreachable, + } action_succeeded = false; } } else { @@ -1308,6 +1325,9 @@ pub fn main() !void { \\ --log-suite \\ Log the name of each suite and aggregate test result. \\ + \\ --module-logging + \\ Enables logging from inside the module when reporting errors. + \\ \\ --verbose \\ Turn on verbose logging for each step of the test suite run. \\ @@ -1347,6 +1367,8 @@ pub fn main() !void { print("Force-regenerating wasm files and driver .json, skipping test run\n", .{}); } else if (strcmp("--log-suite", arg)) { opts.log_suite = true; + } else if (strcmp("--module-logging", arg)) { + opts.log = bytebox.Logger.default(); } else if (strcmp("--verbose", arg) or strcmp("-v", arg)) { g_verbose_logging = true; print("verbose logging: on\n", .{});