Browse Source

Implemented SetMaxCallStackSize(). Closes #263

Dmitry Panov 4 years ago
parent
commit
65d24f7432
3 changed files with 55 additions and 8 deletions
  1. 24 4
      runtime.go
  2. 14 0
      runtime_test.go
  3. 17 4
      vm.go

+ 24 - 4
runtime.go

@@ -247,11 +247,20 @@ type Exception struct {
 	stack []StackFrame
 }
 
+type uncatchableException struct {
+	stack *[]StackFrame
+	err   error
+}
+
 type InterruptedError struct {
 	Exception
 	iface interface{}
 }
 
+type StackOverflowError struct {
+	Exception
+}
+
 func (e *InterruptedError) Value() interface{} {
 	return e.iface
 }
@@ -1191,8 +1200,8 @@ func (r *Runtime) RunScript(name, src string) (Value, error) {
 func (r *Runtime) RunProgram(p *Program) (result Value, err error) {
 	defer func() {
 		if x := recover(); x != nil {
-			if intr, ok := x.(*InterruptedError); ok {
-				err = intr
+			if ex, ok := x.(*uncatchableException); ok {
+				err = ex.err
 			} else {
 				panic(x)
 			}
@@ -1230,6 +1239,8 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) {
 // CaptureCallStack appends the current call stack frames to the stack slice (which may be nil) up to the specified depth.
 // The most recent frame will be the first one.
 // If depth <= 0 or more than the number of available frames, returns the entire stack.
+// This method is not safe for concurrent use and should only be called by a Go function that is
+// called from a running script.
 func (r *Runtime) CaptureCallStack(depth int, stack []StackFrame) []StackFrame {
 	l := len(r.vm.callStack)
 	var offset int
@@ -2026,6 +2037,15 @@ func (r *Runtime) SetParserOptions(opts ...parser.Option) {
 	r.parserOptions = opts
 }
 
+// SetMaxCallStackSize sets the maximum function call depth. When exceeded, a *StackOverflowError is thrown and
+// returned by RunProgram or by a Callable call. This is useful to prevent memory exhaustion caused by an
+// infinite recursion. The default value is math.MaxInt32.
+// This method (as the rest of the Set* methods) is not safe for concurrent use and may only be called
+// from the vm goroutine or when the vm is not running.
+func (r *Runtime) SetMaxCallStackSize(size int) {
+	r.vm.maxCallStackSize = size
+}
+
 // New is an equivalent of the 'new' operator allowing to call it directly from Go.
 func (r *Runtime) New(construct Value, args ...Value) (o *Object, err error) {
 	err = r.try(func() {
@@ -2044,8 +2064,8 @@ func AssertFunction(v Value) (Callable, bool) {
 			return func(this Value, args ...Value) (ret Value, err error) {
 				defer func() {
 					if x := recover(); x != nil {
-						if ex, ok := x.(*InterruptedError); ok {
-							err = ex
+						if ex, ok := x.(*uncatchableException); ok {
+							err = ex.err
 						} else {
 							panic(x)
 						}

+ 14 - 0
runtime_test.go

@@ -1971,6 +1971,20 @@ func TestDeclareGlobalFunc(t *testing.T) {
 	testScript1(TESTLIB+SCRIPT, _undefined, t)
 }
 
+func TestStackOverflowError(t *testing.T) {
+	vm := New()
+	vm.SetMaxCallStackSize(3)
+	_, err := vm.RunString(`
+	function f() {
+		f();
+	}
+	f();
+	`)
+	if _, ok := err.(*StackOverflowError); !ok {
+		t.Fatal(err)
+	}
+}
+
 /*
 func TestArrayConcatSparse(t *testing.T) {
 function foo(a,b,c)

+ 17 - 4
vm.go

@@ -147,6 +147,8 @@ type vm struct {
 	newTarget Value
 	result    Value
 
+	maxCallStackSize int
+
 	stashAllocs int
 	halt        bool
 
@@ -379,6 +381,7 @@ func (vm *vm) newStash() {
 func (vm *vm) init() {
 	vm.sb = -1
 	vm.stash = &vm.r.global.stash
+	vm.maxCallStackSize = math.MaxInt32
 }
 
 func (vm *vm) run() {
@@ -405,7 +408,10 @@ func (vm *vm) run() {
 		atomic.StoreUint32(&vm.interrupted, 0)
 		vm.interruptVal = nil
 		vm.interruptLock.Unlock()
-		panic(v)
+		panic(&uncatchableException{
+			stack: &v.stack,
+			err:   v,
+		})
 	}
 }
 
@@ -469,11 +475,11 @@ func (vm *vm) try(f func()) (ex *Exception) {
 				ex = &Exception{
 					val: x1,
 				}
-			case *InterruptedError:
-				x1.stack = vm.captureStack(x1.stack, ctxOffset)
-				panic(x1)
 			case *Exception:
 				ex = x1
+			case *uncatchableException:
+				*x1.stack = vm.captureStack(*x1.stack, ctxOffset)
+				panic(x1)
 			case typeError:
 				ex = &Exception{
 					val: vm.r.NewTypeError(string(x1)),
@@ -534,6 +540,13 @@ func (vm *vm) saveCtx(ctx *context) {
 }
 
 func (vm *vm) pushCtx() {
+	if len(vm.callStack) > vm.maxCallStackSize {
+		ex := &StackOverflowError{}
+		panic(&uncatchableException{
+			stack: &ex.stack,
+			err:   ex,
+		})
+	}
 	vm.callStack = append(vm.callStack, context{})
 	ctx := &vm.callStack[len(vm.callStack)-1]
 	vm.saveCtx(ctx)