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 f2e4ce3e..f430f7ec 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); } }; @@ -62,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); @@ -138,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", @@ -211,6 +220,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 in bytes", + ) 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..afb01bf6 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}); } @@ -7076,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 e45460fe..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; @@ -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/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 43159afb..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, @@ -329,6 +331,8 @@ pub const VM = struct { NotInFiber, FiberOver, BadNumber, + ReachedMaximumMemoryUsage, + ReachedMaximumCPUUsage, Custom, // TODO: remove when user can use this set directly in buzz code } || Allocator.Error || std.fmt.BufPrintError; @@ -722,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, @@ -3927,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; @@ -3936,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, }; 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