From 3a7281af688bfb44c5357a05e6d2f6b6b168b2df Mon Sep 17 00:00:00 2001 From: Benoit Giannangeli Date: Fri, 19 Jan 2024 14:53:47 +0100 Subject: [PATCH 1/2] feat(sandboxing): `-Dmemory_limit` --- build.zig | 7 +++++++ src/Codegen.zig | 1 + src/Jit.zig | 7 ++++++- src/Parser.zig | 5 +++++ src/main.zig | 14 ++++++++------ src/memory.zig | 4 ++++ src/obj.zig | 2 +- src/vm.zig | 1 + tests/bench/001-btree.buzz | 2 +- 9 files changed, 34 insertions(+), 9 deletions(-) diff --git a/build.zig b/build.zig index f2e4ce3e..e258f3b6 100644 --- a/build.zig +++ b/build.zig @@ -42,6 +42,7 @@ const BuzzGCOptions = struct { initial_gc: usize, next_gc_ratio: usize, next_full_gc_ratio: usize, + memory_limit: ?usize, pub fn step(self: BuzzGCOptions, options: *Build.Step.Options) void { options.addOption(@TypeOf(self.debug), "gc_debug", self.debug); @@ -51,6 +52,7 @@ const BuzzGCOptions = struct { options.addOption(@TypeOf(self.initial_gc), "initial_gc", self.initial_gc); options.addOption(@TypeOf(self.next_gc_ratio), "next_gc_ratio", self.next_gc_ratio); options.addOption(@TypeOf(self.next_full_gc_ratio), "next_full_gc_ratio", self.next_full_gc_ratio); + options.addOption(@TypeOf(self.memory_limit), "memory_limit", self.memory_limit); } }; @@ -211,6 +213,11 @@ pub fn build(b: *Build) !void { "next_full_gc_ratio", "Ratio applied to get the next full GC threshold", ) orelse 4, + .memory_limit = b.option( + usize, + "memory_limit", + "Memory limit", + ) orelse null, }, .jit = .{ .debug = b.option( diff --git a/src/Codegen.zig b/src/Codegen.zig index c6950c24..d0e2f42b 100644 --- a/src/Codegen.zig +++ b/src/Codegen.zig @@ -21,6 +21,7 @@ pub const Error = error{ CantCompile, UnwrappedNull, OutOfBound, + ReachedMaximumMemoryUsage, } || std.mem.Allocator.Error || std.fmt.BufPrintError; pub const Frame = struct { diff --git a/src/Jit.zig b/src/Jit.zig index e95a0124..05b05c54 100644 --- a/src/Jit.zig +++ b/src/Jit.zig @@ -11,7 +11,12 @@ const ZigType = @import("zigtypes.zig").Type; const ExternApi = @import("jit_extern_api.zig").ExternApi; const api = @import("lib/buzz_api.zig"); -pub const Error = error{ CantCompile, UnwrappedNull, OutOfBound } || std.mem.Allocator.Error || std.fmt.BufPrintError; +pub const Error = error{ + CantCompile, + UnwrappedNull, + OutOfBound, + ReachedMaximumMemoryUsage, +} || std.mem.Allocator.Error || std.fmt.BufPrintError; const OptJump = struct { current_insn: std.ArrayList(m.MIR_insn_t), diff --git a/src/Parser.zig b/src/Parser.zig index 04cf6b88..aa3a1b4c 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -193,6 +193,7 @@ pub const Error = error{ CantCompile, UnwrappedNull, OutOfBound, + ReachedMaximumMemoryUsage, } || std.mem.Allocator.Error || std.fmt.BufPrintError || CompileError; const ParseFn = *const fn (*Self, bool) Error!Ast.Node.Index; @@ -708,6 +709,10 @@ pub fn parse(self: *Self, source: []const u8, file_name: []const u8) !?Ast { } } else { if (self.declaration() catch |err| { + if (err == error.ReachedMaximumMemoryUsage) { + return err; + } + if (BuildOptions.debug) { std.debug.print("Parsing failed with error {}\n", .{err}); } diff --git a/src/main.zig b/src/main.zig index e45460fe..47b448bc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -264,9 +264,10 @@ pub fn main() !void { if (flavor == .Repl) { repl(allocator) catch |err| { - if (BuildOptions.debug) { - std.debug.print("Failed with error {}\n", .{err}); - } + std.io.getStdErr().writer().print( + "Failed with error {s}\n", + .{@errorName(err)}, + ) catch unreachable; std.os.exit(1); }; @@ -277,9 +278,10 @@ pub fn main() !void { positionals.items[1..], flavor, ) catch |err| { - if (BuildOptions.debug) { - std.debug.print("Failed with error {}\n", .{err}); - } + std.io.getStdErr().writer().print( + "Failed with error {s}\n", + .{@errorName(err)}, + ) catch unreachable; std.os.exit(1); }; diff --git a/src/memory.zig b/src/memory.zig index d832e688..c51cbf2b 100644 --- a/src/memory.zig +++ b/src/memory.zig @@ -215,6 +215,10 @@ pub const GarbageCollector = struct { try self.collectGarbage(); } + if (BuildOptions.memory_limit != null and self.bytes_allocated > BuildOptions.memory_limit.?) { + return error.ReachedMaximumMemoryUsage; + } + const allocated = try self.allocator.create(T); if (BuildOptions.gc_debug) { diff --git a/src/obj.zig b/src/obj.zig index c3724c4f..ddd02da1 100644 --- a/src/obj.zig +++ b/src/obj.zig @@ -297,7 +297,7 @@ pub const Obj = struct { } } - pub fn typeOf(self: *Self, gc: *GarbageCollector) error{ OutOfMemory, NoSpaceLeft }!*ObjTypeDef { + pub fn typeOf(self: *Self, gc: *GarbageCollector) error{ OutOfMemory, NoSpaceLeft, ReachedMaximumMemoryUsage }!*ObjTypeDef { return switch (self.obj_type) { .String => try gc.type_registry.getTypeDef(.{ .def_type = .String }), .Pattern => try gc.type_registry.getTypeDef(.{ .def_type = .Pattern }), diff --git a/src/vm.zig b/src/vm.zig index 43159afb..32e5936b 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -329,6 +329,7 @@ pub const VM = struct { NotInFiber, FiberOver, BadNumber, + ReachedMaximumMemoryUsage, Custom, // TODO: remove when user can use this set directly in buzz code } || Allocator.Error || std.fmt.BufPrintError; diff --git a/tests/bench/001-btree.buzz b/tests/bench/001-btree.buzz index be0e24f5..f9ff1de2 100644 --- a/tests/bench/001-btree.buzz +++ b/tests/bench/001-btree.buzz @@ -57,7 +57,7 @@ fun btree(int N) > void { } fun main([str] args) > void !> any { - int N = if (args.len() > 0) parseInt(args[0]) ?? 0 else 0; + int N = if (args.len() > 0) parseInt(args[0]) ?? 3 else 3; btree(N); } \ No newline at end of file From 14e5dce50f641f5ad4d8b77eb07fa59646601ad0 Mon Sep 17 00:00:00 2001 From: Benoit Giannangeli Date: Fri, 19 Jan 2024 15:25:49 +0100 Subject: [PATCH 2/2] 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, };