From f619b8531f2ff8cb8e2daaff7c285b1d206c66e6 Mon Sep 17 00:00:00 2001 From: Benoit Giannangeli Date: Fri, 19 Jan 2024 15:25:49 +0100 Subject: [PATCH] feat(sandboxing): -Dcycle_limit closes #182 --- CHANGELOG.md | 1 + build.zig | 9 ++++++++- src/Parser.zig | 2 +- src/main.zig | 2 +- src/repl.zig | 16 +++++++++++++--- src/vm.zig | 24 +++++++++++++++++++++--- 6 files changed, 45 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc07641..d5adcc92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Added - REPL (https://github.com/buzz-language/buzz/issues/17) available by running buzz without any argument - Function argument names and object property names can be ommitted if the provided value is a named variable with the same name (https://github.com/buzz-language/buzz/issues/204) +- Sandboxing build options `memory_limit` and `cycle_limit` (https://github.com/buzz-language/buzz/issues/182) ## Changed - Map type notation has changed from `{K, V}` to `{K: V}`. Similarly map expression with specified typed went from `{, ...}` to `{, ...}` (https://github.com/buzz-language/buzz/issues/253) diff --git a/build.zig b/build.zig index e258f3b6..f430f7ec 100644 --- a/build.zig +++ b/build.zig @@ -64,12 +64,14 @@ const BuzzBuildOptions = struct { gc: BuzzGCOptions, jit: BuzzJITOptions, target: Build.ResolvedTarget, + cycle_limit: ?u128, pub fn step(self: @This(), b: *Build) *Build.Module { var options = b.addOptions(); options.addOption(@TypeOf(self.version), "version", self.version); options.addOption(@TypeOf(self.sha), "sha", self.sha); options.addOption(@TypeOf(self.mimalloc), "mimalloc", self.mimalloc); + options.addOption(@TypeOf(self.cycle_limit), "cycle_limit", self.cycle_limit); self.debug.step(options); self.gc.step(options); @@ -140,6 +142,11 @@ pub fn build(b: *Build) !void { }).stdout, "\n \t", ), + .cycle_limit = b.option( + usize, + "cycle_limit", + "Amount of bytecode (x 1000) the script is allowed to run (WARNING: this disables JIT compilation)", + ) orelse null, .mimalloc = b.option( bool, "mimalloc", @@ -216,7 +223,7 @@ pub fn build(b: *Build) !void { .memory_limit = b.option( usize, "memory_limit", - "Memory limit", + "Memory limit in bytes", ) orelse null, }, .jit = .{ diff --git a/src/Parser.zig b/src/Parser.zig index aa3a1b4c..afb01bf6 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -7081,7 +7081,7 @@ fn importStatement(self: *Self) Error!Ast.Node.Index { } fn zdefStatement(self: *Self) Error!Ast.Node.Index { - if (!BuildOptions.jit) { + if (!BuildOptions.jit and BuildOptions.cycle_limit == null) { self.reportError(.zdef, "zdef can't be used, this instance of buzz was built with JIT compiler disabled"); } diff --git a/src/main.zig b/src/main.zig index 47b448bc..03e3a987 100644 --- a/src/main.zig +++ b/src/main.zig @@ -30,7 +30,7 @@ fn runFile(allocator: Allocator, file_name: []const u8, args: [][:0]u8, flavor: }; var imports = std.StringHashMap(Parser.ScriptImport).init(allocator); var vm = try VM.init(&gc, &import_registry, flavor); - vm.jit = if (BuildOptions.jit) + vm.jit = if (BuildOptions.jit and BuildOptions.cycle_limit == null) JIT.init(&vm) else null; diff --git a/src/repl.zig b/src/repl.zig index 8b7de618..23319e55 100644 --- a/src/repl.zig +++ b/src/repl.zig @@ -47,7 +47,7 @@ pub fn printBanner(out: std.fs.File.Writer, full: bool) void { if (full) { out.print( - "Built with Zig {} {s}\nAllocator: {s}\nJIT: {s}\n", + "Built with Zig {} {s}\nAllocator: {s}, Memory limit: {} {s}\nJIT: {s}, CPU limit: {} {s}\n", .{ builtin.zig_version, switch (builtin.mode) { @@ -59,10 +59,20 @@ pub fn printBanner(out: std.fs.File.Writer, full: bool) void { if (builtin.mode == .Debug) "gpa" else if (BuildOptions.mimalloc) "mimalloc" else "c_allocator", - if (BuildOptions.jit) + if (BuildOptions.memory_limit) |ml| + ml + else + 0, + if (BuildOptions.memory_limit != null) + "bytes" + else + "(unlimited)", + if (BuildOptions.jit and BuildOptions.cycle_limit == null) "on" else "off", + if (BuildOptions.cycle_limit) |cl| cl else 0, + if (BuildOptions.cycle_limit != null) "cycles" else "(unlimited)", }, ) catch unreachable; } @@ -83,7 +93,7 @@ pub fn repl(allocator: std.mem.Allocator) !void { }; var imports = std.StringHashMap(Parser.ScriptImport).init(allocator); var vm = try VM.init(&gc, &import_registry, .Repl); - vm.jit = if (BuildOptions.jit) + vm.jit = if (BuildOptions.jit and BuildOptions.cycle_limit == null) JIT.init(&vm) else null; diff --git a/src/vm.zig b/src/vm.zig index 32e5936b..dece25a0 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -322,6 +322,8 @@ pub const Fiber = struct { pub const VM = struct { const Self = @This(); + var cycles: u128 = 0; + pub const Error = error{ UnwrappedNull, OutOfBound, @@ -330,6 +332,7 @@ pub const VM = struct { FiberOver, BadNumber, ReachedMaximumMemoryUsage, + ReachedMaximumCPUUsage, Custom, // TODO: remove when user can use this set directly in buzz code } || Allocator.Error || std.fmt.BufPrintError; @@ -723,6 +726,21 @@ pub const VM = struct { self.gc.where = current_frame.closure.function.chunk.lines.items[current_frame.ip - 1]; } + if (BuildOptions.cycle_limit) |limit| { + cycles += 1; + + if (cycles > limit * 1000) { + self.throw( + Error.ReachedMaximumCPUUsage, + (self.gc.copyString("Maximum CPU usage reached") catch @panic("Maximum CPU usage reached")).toValue(), + null, + null, + ) catch @panic("Maximum CPU usage reached"); + + return; + } + } + // Tail call @call( .always_tail, @@ -3928,7 +3946,7 @@ pub const VM = struct { } // A JIT compiled function pops its stack on its own - fn callCompiled(self: *Self, closure: ?*ObjClosure, native: NativeFn, arg_count: u8, catch_value: ?Value) !void { + fn callCompiled(self: *Self, closure: *ObjClosure, native: NativeFn, arg_count: u8, catch_value: ?Value) !void { const was_in_native_call = self.currentFrame() != null and self.currentFrame().?.in_native_call; if (self.currentFrame()) |frame| { frame.in_native_call = true; @@ -3937,8 +3955,8 @@ pub const VM = struct { var ctx = NativeCtx{ .vm = self, - .globals = if (closure) |uclosure| uclosure.globals.items.ptr else &[_]Value{}, - .upvalues = if (closure) |uclosure| uclosure.upvalues.items.ptr else &[_]*ObjUpValue{}, + .globals = closure.globals.items.ptr, + .upvalues = closure.upvalues.items.ptr, .base = self.current_fiber.stack_top - arg_count - 1, .stack_top = &self.current_fiber.stack_top, };