diff --git a/src/cli.zig b/src/cli.zig index a58804c3..d7a7a7ee 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -156,7 +156,7 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer var max_memory_bytes: ?u64 = null; var fuel: ?u64 = null; var timeout_ms: ?u64 = null; - var force_interpreter = false; + var force_interpreter: ?bool = null; // Parse options var i: usize = 0; @@ -458,9 +458,9 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer // Apply resource limits module.vm.max_memory_bytes = max_memory_bytes; - module.vm.fuel = fuel; - module.vm.force_interpreter = force_interpreter; - if (timeout_ms) |ms| module.vm.setDeadlineTimeoutMs(ms); + module.fuel = fuel; + module.timeout_ms = timeout_ms; + module.force_interpreter = force_interpreter; // Lookup export info for type-aware parsing and validation const export_info = module.getExportInfo(func_name); @@ -607,9 +607,9 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer // Apply resource limits module.vm.max_memory_bytes = max_memory_bytes; - module.vm.fuel = fuel; - module.vm.force_interpreter = force_interpreter; - if (timeout_ms) |ms| module.vm.setDeadlineTimeoutMs(ms); + module.fuel = fuel; + module.timeout_ms = timeout_ms; + module.force_interpreter = force_interpreter; var no_args = [_]u64{}; var no_results = [_]u64{}; diff --git a/src/fuzz_gen.zig b/src/fuzz_gen.zig index e3ada46b..3c2d631c 100644 --- a/src/fuzz_gen.zig +++ b/src/fuzz_gen.zig @@ -1721,8 +1721,8 @@ fn loadAndExercise(alloc: Allocator, wasm: []const u8) void { // Call multiple times to trigger JIT compilation for (0..FUZZ_JIT_CALLS) |_| { - module.invoke(ei.name, arg_slice, result_slice) catch break; module.vm.fuel = FUZZ_FUEL; + module.invoke(ei.name, arg_slice, result_slice) catch break; } } } diff --git a/src/fuzz_loader.zig b/src/fuzz_loader.zig index c06be0e6..36012fd7 100644 --- a/src/fuzz_loader.zig +++ b/src/fuzz_loader.zig @@ -48,7 +48,7 @@ fn loadModule(allocator: std.mem.Allocator, input: []const u8) ?*zwasm.WasmModul const m = zwasm.WasmModule.loadWasiWithOptions(allocator, input, .{ .caps = zwasm.Capabilities.sandbox, }) catch return null; - m.vm.fuel = FUEL_LIMIT; + m.fuel = FUEL_LIMIT; return m; } @@ -79,8 +79,8 @@ fn fuzzOne(allocator: std.mem.Allocator, input: []const u8) void { // Call multiple times to trigger JIT compilation for (0..JIT_CALLS) |_| { - module.invoke(ei.name, arg_slice, result_slice) catch break; module.vm.fuel = FUEL_LIMIT; + module.invoke(ei.name, arg_slice, result_slice) catch break; } } } diff --git a/src/fuzz_wat_loader.zig b/src/fuzz_wat_loader.zig index f7f03c2a..2c791532 100644 --- a/src/fuzz_wat_loader.zig +++ b/src/fuzz_wat_loader.zig @@ -64,8 +64,8 @@ fn fuzzOne(allocator: std.mem.Allocator, wat_source: []const u8) void { // Call multiple times to trigger JIT compilation for (0..JIT_CALLS) |_| { - module.invoke(ei.name, arg_slice, result_slice) catch break; module.vm.fuel = FUEL_LIMIT; + module.invoke(ei.name, arg_slice, result_slice) catch break; } } } diff --git a/src/module.zig b/src/module.zig index fdfc9c08..2f5d21a2 100644 --- a/src/module.zig +++ b/src/module.zig @@ -2059,7 +2059,7 @@ test "fuzz — full pipeline (load+instantiate) does not panic" { var results: [1]u64 = .{0}; const result_slice = results[0..ei.result_types.len]; module.invoke(ei.name, &.{}, result_slice) catch continue; - module.vm.fuel = 100_000; + module.fuel = 100_000; } } } diff --git a/src/types.zig b/src/types.zig index 604936cd..daef1134 100644 --- a/src/types.zig +++ b/src/types.zig @@ -233,14 +233,23 @@ pub const WasmModule = struct { /// Owned wasm bytes (from WAT conversion). Freed on deinit. owned_wasm_bytes: ?[]const u8 = null, + /// Persistent fuel budget. + fuel: ?u64 = null, + /// Persistent timeout setting. + timeout_ms: ?u64 = null, + /// Persistent interpreter-only flag. When non-null, `invoke()` applies this + /// to `vm.force_interpreter` before each call; when null, `vm.force_interpreter` + /// is left untouched so callers may set it directly on `self.vm`. + force_interpreter: ?bool = null, + /// Load a Wasm module from binary bytes, decode, and instantiate. pub fn load(allocator: Allocator, wasm_bytes: []const u8) !*WasmModule { - return loadCore(allocator, wasm_bytes, false, null, null); + return loadCore(allocator, wasm_bytes, false, null, null, null, null); } /// Load with a fuel limit (traps start function if it exceeds the limit). pub fn loadWithFuel(allocator: Allocator, wasm_bytes: []const u8, fuel: u64) !*WasmModule { - return loadCore(allocator, wasm_bytes, false, null, fuel); + return loadCore(allocator, wasm_bytes, false, null, fuel, null, null); } /// Load a module from WAT (WebAssembly Text Format) source. @@ -248,7 +257,7 @@ pub const WasmModule = struct { pub fn loadFromWat(allocator: Allocator, wat_source: []const u8) !*WasmModule { const wasm_bytes = try rt.wat.watToWasm(allocator, wat_source); errdefer allocator.free(wasm_bytes); - const self = try loadCore(allocator, wasm_bytes, false, null, null); + const self = try loadCore(allocator, wasm_bytes, false, null, null, null, null); self.owned_wasm_bytes = wasm_bytes; return self; } @@ -257,14 +266,14 @@ pub const WasmModule = struct { pub fn loadFromWatWithFuel(allocator: Allocator, wat_source: []const u8, fuel: u64) !*WasmModule { const wasm_bytes = try rt.wat.watToWasm(allocator, wat_source); errdefer allocator.free(wasm_bytes); - const self = try loadCore(allocator, wasm_bytes, false, null, fuel); + const self = try loadCore(allocator, wasm_bytes, false, null, fuel, null, null); self.owned_wasm_bytes = wasm_bytes; return self; } /// Load a WASI module — registers wasi_snapshot_preview1 imports. pub fn loadWasi(allocator: Allocator, wasm_bytes: []const u8) !*WasmModule { - return loadCore(allocator, wasm_bytes, true, null, null); + return loadCore(allocator, wasm_bytes, true, null, null, null, null); } /// Apply WasiOptions to a WasiContext (shared logic for all WASI loaders). @@ -301,30 +310,22 @@ pub const WasmModule = struct { /// Load a WASI module with custom args, env, and preopened directories. pub fn loadWasiWithOptions(allocator: Allocator, wasm_bytes: []const u8, opts: WasiOptions) !*WasmModule { - const self = try loadCore(allocator, wasm_bytes, true, null, null); + const self = try loadCore(allocator, wasm_bytes, true, null, null, null, null); errdefer self.deinit(); - - if (self.wasi_ctx) |*wc| { - try applyWasiOptions(wc, opts); - } - + if (self.wasi_ctx) |*wc| try applyWasiOptions(wc, opts); return self; } /// Load with imports from other modules or host functions. pub fn loadWithImports(allocator: Allocator, wasm_bytes: []const u8, imports: []const ImportEntry) !*WasmModule { - return loadCore(allocator, wasm_bytes, false, imports, null); + return loadCore(allocator, wasm_bytes, false, imports, null, null, null); } /// Load with combined WASI + import support. Used by CLI for --link + WASI fallback. pub fn loadWasiWithImports(allocator: Allocator, wasm_bytes: []const u8, imports: ?[]const ImportEntry, opts: WasiOptions) !*WasmModule { - const self = try loadCore(allocator, wasm_bytes, true, imports, null); + const self = try loadCore(allocator, wasm_bytes, true, imports, null, null, null); errdefer self.deinit(); - - if (self.wasi_ctx) |*wc| { - try applyWasiOptions(wc, opts); - } - + if (self.wasi_ctx) |*wc| try applyWasiOptions(wc, opts); return self; } @@ -365,6 +366,9 @@ pub const WasmModule = struct { self.owned_wasm_bytes = null; self.store = rt.store_mod.Store.init(allocator); self.wasi_ctx = null; + self.fuel = null; + self.timeout_ms = null; + self.force_interpreter = null; self.module = rt.module_mod.Module.init(allocator, wasm_bytes); self.module.decode() catch |err| { @@ -404,7 +408,7 @@ pub const WasmModule = struct { return .{ .module = self, .apply_error = apply_error }; } - fn loadCore(allocator: Allocator, wasm_bytes: []const u8, wasi: bool, imports: ?[]const ImportEntry, fuel: ?u64) !*WasmModule { + fn loadCore(allocator: Allocator, wasm_bytes: []const u8, wasi: bool, imports: ?[]const ImportEntry, fuel: ?u64, timeout_ms: ?u64, force_interpreter: ?bool) !*WasmModule { const self = try allocator.create(WasmModule); errdefer allocator.destroy(self); @@ -440,13 +444,21 @@ pub const WasmModule = struct { self.wit_funcs = &[_]wit_parser.WitFunc{}; self.vm = try allocator.create(rt.vm_mod.Vm); + self.vm.* = rt.vm_mod.Vm.init(allocator); - self.vm.fuel = fuel; - // Execute start function if present + self.fuel = fuel; + self.timeout_ms = timeout_ms; + self.force_interpreter = force_interpreter; + + // Execute start function if present. + // Only apply persistent settings to the VM when explicitly set — a null + // persistent field means "inherit whatever the caller set on self.vm.*". if (self.module.start) |start_idx| { self.vm.reset(); - self.vm.fuel = fuel; + if (self.fuel) |f| self.vm.fuel = f; + if (self.force_interpreter) |fi| self.vm.force_interpreter = fi; + if (self.timeout_ms) |ms| self.vm.setDeadlineTimeoutMs(ms); try self.vm.invokeByIndex(&self.instance, start_idx, &.{}, &.{}); } @@ -474,17 +486,31 @@ pub const WasmModule = struct { /// Invoke an exported function by name. /// Args and results are passed as u64 arrays. + /// + /// Persistent module settings (`self.fuel` / `self.timeout_ms` / + /// `self.force_interpreter`) override `self.vm.*` only when set (non-null). + /// A null persistent field preserves whatever the caller set directly on + /// `self.vm`, since `self.vm.reset()` does not clear these fields. pub fn invoke(self: *WasmModule, name: []const u8, args: []const u64, results: []u64) !void { self.vm.reset(); + if (self.fuel) |f| self.vm.fuel = f; + if (self.force_interpreter) |fi| self.vm.force_interpreter = fi; + if (self.timeout_ms) |ms| self.vm.setDeadlineTimeoutMs(ms); try self.vm.invoke(&self.instance, name, args, results); } /// Invoke using only the stack-based interpreter, bypassing RegIR and JIT. /// Used by differential testing to get a reference result. + /// Restores the prior `vm.force_interpreter` value on return so the caller's + /// mode selection — whether set via `module.force_interpreter` or directly + /// on `module.vm.force_interpreter` — survives a diagnostic interpreter call. pub fn invokeInterpreterOnly(self: *WasmModule, name: []const u8, args: []const u64, results: []u64) !void { self.vm.reset(); + if (self.fuel) |f| self.vm.fuel = f; + if (self.timeout_ms) |ms| self.vm.setDeadlineTimeoutMs(ms); + const saved_fi = self.vm.force_interpreter; self.vm.force_interpreter = true; - defer self.vm.force_interpreter = false; + defer self.vm.force_interpreter = saved_fi; try self.vm.invoke(&self.instance, name, args, results); } @@ -1385,3 +1411,91 @@ test "loadWasiWithOptions explicit all grants full access" { try testing.expect(caps.allow_env); try testing.expect(caps.allow_path); } + +test "force_interpreter — persistence across invoke and invokeInterpreterOnly" { + if (!@import("build_options").enable_wat) return error.SkipZigTest; + var wasm_mod = try WasmModule.loadFromWat(testing.allocator, + \\(module + \\ (func (export "f") (result i32) + \\ i32.const 42 + \\ ) + \\) + ); + defer wasm_mod.deinit(); + + var results = [_]u64{0}; + + // Pattern A — legacy direct-vm: caller sets vm.force_interpreter; persistent + // field left null; invoke() must not clobber the caller's choice. + wasm_mod.force_interpreter = null; + wasm_mod.vm.force_interpreter = true; + try wasm_mod.invoke("f", &.{}, &results); + try testing.expect(wasm_mod.vm.force_interpreter == true); + try testing.expectEqual(@as(u64, 42), results[0]); + + // invokeInterpreterOnly under Pattern A must restore vm.force_interpreter + // to the caller's value (true), not to the persistent-field default. + try wasm_mod.invokeInterpreterOnly("f", &.{}, &results); + try testing.expect(wasm_mod.vm.force_interpreter == true); + + // Pattern B — new persistent-field override. vm.force_interpreter gets + // overridden from `module.force_interpreter` on every invoke. + wasm_mod.vm.force_interpreter = false; + wasm_mod.force_interpreter = true; + try wasm_mod.invoke("f", &.{}, &results); + try testing.expect(wasm_mod.vm.force_interpreter == true); + + // invokeInterpreterOnly under Pattern B restores to true (the value live on + // vm at entry), so a subsequent regular invoke still sees interpreter mode. + try wasm_mod.invokeInterpreterOnly("f", &.{}, &results); + try testing.expect(wasm_mod.vm.force_interpreter == true); + try wasm_mod.invoke("f", &.{}, &results); + try testing.expect(wasm_mod.vm.force_interpreter == true); + + // Pattern C — persistent field explicitly cleared to false wins over a + // prior vm.force_interpreter = true caller mutation. + wasm_mod.force_interpreter = false; + wasm_mod.vm.force_interpreter = true; + try wasm_mod.invoke("f", &.{}, &results); + try testing.expect(wasm_mod.vm.force_interpreter == false); + + // Pattern D — null persistent + false vm stays false. + wasm_mod.force_interpreter = null; + wasm_mod.vm.force_interpreter = false; + try wasm_mod.invoke("f", &.{}, &results); + try testing.expect(wasm_mod.vm.force_interpreter == false); +} + +test "fuel and timeout — persistence and caller-set preservation" { + if (!@import("build_options").enable_wat) return error.SkipZigTest; + var wasm_mod = try WasmModule.loadFromWat(testing.allocator, + \\(module + \\ (func (export "f") (result i32) + \\ i32.const 42 + \\ ) + \\) + ); + defer wasm_mod.deinit(); + var results = [_]u64{0}; + + // Pattern A — caller sets vm.fuel directly; persistent null must not wipe it. + wasm_mod.fuel = null; + wasm_mod.vm.fuel = 1_000; + try wasm_mod.invoke("f", &.{}, &results); + try testing.expect(wasm_mod.vm.fuel != null); + + // Pattern B — persistent module.fuel overrides per-invoke. + wasm_mod.fuel = 500; + wasm_mod.vm.fuel = null; + try wasm_mod.invoke("f", &.{}, &results); + try testing.expect(wasm_mod.vm.fuel != null); + try testing.expect(wasm_mod.vm.fuel.? <= 500); + + // timeout — caller-set deadline must not be wiped by null persistent. + wasm_mod.timeout_ms = null; + wasm_mod.vm.setDeadlineTimeoutMs(5_000); + const deadline_before = wasm_mod.vm.deadline_ns; + try wasm_mod.invoke("f", &.{}, &results); + try testing.expect(wasm_mod.vm.deadline_ns != null); + try testing.expectEqual(deadline_before, wasm_mod.vm.deadline_ns); +}