Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -458,8 +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;
module.fuel = fuel;
module.timeout_ms = timeout_ms;
module.force_interpreter = force_interpreter;
if (timeout_ms) |ms| module.vm.setDeadlineTimeoutMs(ms);

// Lookup export info for type-aware parsing and validation
Expand Down Expand Up @@ -607,8 +608,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;
module.fuel = fuel;
module.timeout_ms = timeout_ms;
module.force_interpreter = force_interpreter;
if (timeout_ms) |ms| module.vm.setDeadlineTimeoutMs(ms);

var no_args = [_]u64{};
Expand Down
2 changes: 1 addition & 1 deletion src/fuzz_gen.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/fuzz_loader.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
}
}
2 changes: 1 addition & 1 deletion src/fuzz_wat_loader.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
2 changes: 1 addition & 1 deletion src/module.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Expand Down
95 changes: 73 additions & 22 deletions src/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -233,22 +233,29 @@ 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.
force_interpreter: bool = false,

/// 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, false);
}

/// 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, false);
}

/// Load a module from WAT (WebAssembly Text Format) source.
/// Requires `-Dwat=true` (default). Returns error.WatNotEnabled if disabled.
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, false);
self.owned_wasm_bytes = wasm_bytes;
return self;
}
Expand All @@ -257,14 +264,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, false);
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, false);
}

/// Apply WasiOptions to a WasiContext (shared logic for all WASI loaders).
Expand Down Expand Up @@ -301,30 +308,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, false);
errdefer self.deinit();

if (self.wasi_ctx) |*wc| {
try applyWasiOptions(wc, opts);
}

try applyWasiOptions(&self.wasi_ctx.?, 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, false);
}

/// 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, false);
errdefer self.deinit();

if (self.wasi_ctx) |*wc| {
try applyWasiOptions(wc, opts);
}

try applyWasiOptions(&self.wasi_ctx.?, opts);
return self;
}

Expand Down Expand Up @@ -365,6 +364,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 = false;

self.module = rt.module_mod.Module.init(allocator, wasm_bytes);
self.module.decode() catch |err| {
Expand Down Expand Up @@ -404,7 +406,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);

Expand Down Expand Up @@ -440,13 +442,19 @@ 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;

self.fuel = fuel;
self.timeout_ms = timeout_ms;
self.force_interpreter = force_interpreter;

// Execute start function if present
if (self.module.start) |start_idx| {
self.vm.reset();
self.vm.fuel = fuel;
self.vm.fuel = self.fuel;
self.vm.force_interpreter = self.force_interpreter;
self.vm.setDeadlineTimeoutMs(self.timeout_ms);
try self.vm.invokeByIndex(&self.instance, start_idx, &.{}, &.{});
}

Expand Down Expand Up @@ -476,15 +484,20 @@ pub const WasmModule = struct {
/// Args and results are passed as u64 arrays.
pub fn invoke(self: *WasmModule, name: []const u8, args: []const u64, results: []u64) !void {
self.vm.reset();
self.vm.fuel = self.fuel;
self.vm.force_interpreter = self.force_interpreter;
self.vm.setDeadlineTimeoutMs(self.timeout_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.
pub fn invokeInterpreterOnly(self: *WasmModule, name: []const u8, args: []const u64, results: []u64) !void {
self.vm.reset();
self.vm.fuel = self.fuel;
self.vm.force_interpreter = true;
defer self.vm.force_interpreter = false;
self.vm.setDeadlineTimeoutMs(self.timeout_ms);
defer self.vm.force_interpreter = self.force_interpreter;
try self.vm.invoke(&self.instance, name, args, results);
}

Expand Down Expand Up @@ -1385,3 +1398,41 @@ test "loadWasiWithOptions explicit all grants full access" {
try testing.expect(caps.allow_env);
try testing.expect(caps.allow_path);
}

test "force_interpreter — persistence across invokeInterpreterOnly calls" {
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();

// Set force_interpreter to true on load
wasm_mod.force_interpreter = true;
wasm_mod.vm.force_interpreter = true;

var results = [_]u64{0};

// 1. Regular invoke should follow persistent setting
try wasm_mod.invoke("f", &.{}, &results);
try testing.expect(wasm_mod.vm.force_interpreter == true);
try testing.expectEqual(@as(u64, 42), results[0]);

// 2. invokeInterpreterOnly should restore the persistent setting afterward
try wasm_mod.invokeInterpreterOnly("f", &.{}, &results);
try testing.expect(wasm_mod.vm.force_interpreter == true); // must NOT be reset to false
try testing.expectEqual(@as(u64, 42), results[0]);

// 3. Subsequent regular invoke still sees interpreter mode
try wasm_mod.invoke("f", &.{}, &results);
try testing.expect(wasm_mod.vm.force_interpreter == true);
try testing.expectEqual(@as(u64, 42), results[0]);

// 4. Clearing the flag persists to the next invoke
wasm_mod.force_interpreter = false;
try wasm_mod.invoke("f", &.{}, &results);
try testing.expect(wasm_mod.vm.force_interpreter == false);
}
Loading