From 8b50f5bd16b17aefb93c952493e660c52bc90465 Mon Sep 17 00:00:00 2001 From: Gabriele Cimato Date: Mon, 9 Dec 2024 10:13:21 -0500 Subject: [PATCH] BAAS-34801: add forced mem checks on new arrays and obj props (#127) --- added_values_test.go | 4 +-- array_sparse_test.go | 8 ++--- array_test.go | 8 ++--- builtin_array.go | 13 ++++++++- builtin_arrray_test.go | 55 +++++++++++++++++++++++++++++++++- builtin_map_test.go | 8 ++--- builtin_proxy_test.go | 2 +- builtin_set_test.go | 12 ++++---- compiler_test.go | 4 +-- date_test.go | 2 +- destruct_test.go | 2 +- func_test.go | 12 ++++---- mem_context.go | 36 +++++++++++++++++++++-- memory_test.go | 10 +++---- object.go | 11 +++++++ object_dynamic_test.go | 4 +-- object_gomap_test.go | 4 +-- object_goslice_test.go | 2 +- object_test.go | 65 +++++++++++++++++++++++++++++++++++++++-- proxy_test.go | 4 +-- runtime.go | 10 +++++-- runtime_test.go | 2 +- string_ascii_test.go | 2 +- string_imported_test.go | 2 +- string_test.go | 4 +-- string_unicode_test.go | 2 +- typedarrays_test.go | 6 ++-- value_test.go | 6 ++-- vm_test.go | 8 ++--- 29 files changed, 237 insertions(+), 71 deletions(-) diff --git a/added_values_test.go b/added_values_test.go index 7e0836b5..0ef682d7 100644 --- a/added_values_test.go +++ b/added_values_test.go @@ -48,8 +48,6 @@ func TestIntStringEquality(t *testing.T) { } func TestAddedValuesMemUsage(t *testing.T) { - vm := New() - for _, tc := range []struct { name string val MemUsageReporter @@ -77,7 +75,7 @@ func TestAddedValuesMemUsage(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - mem, err := tc.val.MemUsage(NewMemUsageContext(vm, 100, 100, 100, 100, 0.1, nil)) + mem, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != nil { t.Fatalf("Unexpected error. Actual: %v Expected: nil", err) } diff --git a/array_sparse_test.go b/array_sparse_test.go index 8d77c95b..7c1e8e0b 100644 --- a/array_sparse_test.go +++ b/array_sparse_test.go @@ -274,7 +274,7 @@ func TestSparseArrayObjectMemUsage(t *testing.T) { }{ { name: "mem below threshold", - mu: NewMemUsageContext(vm, 88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), sao: &sparseArrayObject{ items: []sparseArrayItem{ { @@ -289,14 +289,14 @@ func TestSparseArrayObjectMemUsage(t *testing.T) { }, { name: "mem is SizeEmptyStruct for nil sparse array", - mu: NewMemUsageContext(vm, 88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), sao: nil, expected: SizeEmptyStruct, errExpected: nil, }, { name: "mem way above threshold returns first crossing of threshold", - mu: NewMemUsageContext(vm, 88, 100, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 100, 50, 50, 0.1, TestNativeMemUsageChecker{}), sao: &sparseArrayObject{ items: []sparseArrayItem{ { @@ -331,7 +331,7 @@ func TestSparseArrayObjectMemUsage(t *testing.T) { }, { name: "mem above estimate threshold and within memory limit returns correct usage", - mu: NewMemUsageContext(vm, 88, 100, 5, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 100, 5, 50, 0.1, TestNativeMemUsageChecker{}), sao: &sparseArrayObject{ items: []sparseArrayItem{ { diff --git a/array_test.go b/array_test.go index ff3fd327..1da42424 100644 --- a/array_test.go +++ b/array_test.go @@ -144,7 +144,7 @@ func TestArrayObjectMemUsage(t *testing.T) { }{ { name: "mem below threshold given a nil slice of values", - mu: NewMemUsageContext(vm, 88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), ao: &arrayObject{}, // array overhead + array baseObject expectedMem: SizeEmptyStruct + SizeEmptyStruct, @@ -152,7 +152,7 @@ func TestArrayObjectMemUsage(t *testing.T) { }, { name: "mem below threshold given empty slice of values", - mu: NewMemUsageContext(vm, 88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), ao: &arrayObject{values: []Value{}}, // array overhead + array baseObject + values slice overhead expectedMem: SizeEmptyStruct + SizeEmptyStruct + SizeEmptySlice, @@ -160,7 +160,7 @@ func TestArrayObjectMemUsage(t *testing.T) { }, { name: "mem way above threshold returns first crossing of threshold", - mu: NewMemUsageContext(vm, 88, 100, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 100, 50, 50, 0.1, TestNativeMemUsageChecker{}), ao: &arrayObject{ values: []Value{ vm.ToValue("key0"), @@ -179,7 +179,7 @@ func TestArrayObjectMemUsage(t *testing.T) { }, { name: "empty array with negative threshold", - mu: NewMemUsageContext(vm, 88, 100, -1, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 100, -1, 50, 0.1, TestNativeMemUsageChecker{}), ao: &arrayObject{ values: []Value{}, }, diff --git a/builtin_array.go b/builtin_array.go index 79a9cb56..5a561ebf 100644 --- a/builtin_array.go +++ b/builtin_array.go @@ -80,7 +80,18 @@ func relToIdx(rel, l int64) int64 { return max(l+rel, 0) } -func (r *Runtime) newArrayValues(values []Value) *Object { +func (r *Runtime) newArrayValues(values valueStack) *Object { + if r.shouldForceMemCheck { + if memCtx := newMemUsageContextClone(); memCtx != nil { + memUsage, err := valuesMemUsage(values, memCtx) + if err != nil { + panic(err) + } + if memCtx.MemUsageLimitExceeded(memUsage) { + panic(ErrMemLimitExceeded) + } + } + } return setArrayValues(r.newArrayObject(), values).val } diff --git a/builtin_arrray_test.go b/builtin_arrray_test.go index 8ef91612..f2dd2403 100644 --- a/builtin_arrray_test.go +++ b/builtin_arrray_test.go @@ -1,6 +1,9 @@ package goja -import "testing" +import ( + "context" + "testing" +) func TestArrayProtoProp(t *testing.T) { const SCRIPT = ` @@ -338,3 +341,53 @@ func TestArrayProto(t *testing.T) { ` testScriptWithTestLib(SCRIPT, _undefined, t) } + +func TestNewArrayValues(t *testing.T) { + for _, tc := range []struct { + name string + memLimit uint64 + expectedPanic bool + shouldForceMemCheck bool + }{ + { + name: "should not panic when creating a new array under the mem limit", + memLimit: 1000, + expectedPanic: false, + shouldForceMemCheck: false, + }, + { + name: "should panic when creating a new array over the mem limit and mem usage check is forced", + memLimit: 0, + expectedPanic: true, + shouldForceMemCheck: true, + }, + { + name: "should not panic when creating a new array over the mem limit and mem usage check is not forced", + memLimit: 0, + expectedPanic: false, + shouldForceMemCheck: false, + }, + { + name: "should not panic when creating a new array under the mem limit and mem usage check is forced", + memLimit: 1000, + expectedPanic: false, + shouldForceMemCheck: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + defer func() { + r := recover() + if tc.expectedPanic && r == nil { + t.Error("The code is expected to panic, but it didn't") + } + if !tc.expectedPanic && r != nil { + t.Errorf("The code panicked, but it should not have: %v", r) + } + }() + vm := NewWithContext(context.Background(), tc.shouldForceMemCheck) + // Creating a mem usage context so it populates the package variable + NewMemUsageContext(100, tc.memLimit, 100, 100, 0.5, nil) + vm.newArrayValues([]Value{valueInt(0)}) + }) + } +} diff --git a/builtin_map_test.go b/builtin_map_test.go index 6c63bd8e..0e173bcc 100644 --- a/builtin_map_test.go +++ b/builtin_map_test.go @@ -286,7 +286,7 @@ func TestMapObjectMemUsage(t *testing.T) { }{ { name: "mem below threshold", - mu: NewMemUsageContext(vm, 88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), mo: &mapObject{ m: &orderedMap{ hashTable: map[uint64]*mapEntry{ @@ -303,14 +303,14 @@ func TestMapObjectMemUsage(t *testing.T) { }, { name: "mem is SizeEmptyStruct given a nil map object", - mu: NewMemUsageContext(vm, 88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), mo: nil, expectedMem: SizeEmptyStruct, errExpected: nil, }, { name: "mem way above threshold returns first crossing of threshold", - mu: NewMemUsageContext(vm, 88, 100, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 100, 50, 50, 0.1, TestNativeMemUsageChecker{}), mo: &mapObject{ m: &orderedMap{ hashTable: map[uint64]*mapEntry{ @@ -343,7 +343,7 @@ func TestMapObjectMemUsage(t *testing.T) { }, { name: "mem above estimate threshold and within memory limit returns correct mem usage", - mu: NewMemUsageContext(vm, 88, 100, 50, 5, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 100, 50, 5, 0.1, TestNativeMemUsageChecker{}), mo: &mapObject{ m: createOrderedMap(vm, 20), }, diff --git a/builtin_proxy_test.go b/builtin_proxy_test.go index 3c603055..cbc5f8aa 100644 --- a/builtin_proxy_test.go +++ b/builtin_proxy_test.go @@ -1290,7 +1290,7 @@ func TestBuiltinProxyMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != nil { t.Fatalf("Unexpected error. Actual: %v Expected: nil", err) } diff --git a/builtin_set_test.go b/builtin_set_test.go index e00ae265..c5adeda6 100644 --- a/builtin_set_test.go +++ b/builtin_set_test.go @@ -227,7 +227,7 @@ func TestSetObjectMemUsage(t *testing.T) { }{ { name: "mem below threshold", - mu: NewMemUsageContext(vm, 88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), so: &setObject{ m: &orderedMap{ hashTable: map[uint64]*mapEntry{ @@ -244,14 +244,14 @@ func TestSetObjectMemUsage(t *testing.T) { }, { name: "mem is SizeEmptyStruct given a nil map object", - mu: NewMemUsageContext(vm, 88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), so: nil, expectedMem: SizeEmptyStruct, errExpected: nil, }, { name: "mem way above threshold returns first crossing of threshold", - mu: NewMemUsageContext(vm, 88, 100, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 100, 50, 50, 0.1, TestNativeMemUsageChecker{}), so: &setObject{ m: &orderedMap{ hashTable: map[uint64]*mapEntry{ @@ -284,7 +284,7 @@ func TestSetObjectMemUsage(t *testing.T) { }, { name: "mem above estimate threshold and within memory limit returns correct mem usage", - mu: NewMemUsageContext(vm, 88, 100, 50, 5, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 100, 50, 5, 0.1, TestNativeMemUsageChecker{}), so: &setObject{ m: createOrderedMap(vm, 20), }, @@ -298,7 +298,7 @@ func TestSetObjectMemUsage(t *testing.T) { }, { name: "mem above estimate threshold and within memory limit and nil values returns correct mem usage", - mu: NewMemUsageContext(vm, 88, 100, 50, 1, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 100, 50, 1, 0.1, TestNativeMemUsageChecker{}), so: &setObject{ m: createOrderedMapWithNilValues(3), }, @@ -308,7 +308,7 @@ func TestSetObjectMemUsage(t *testing.T) { }, { name: "mem is SizeEmptyStruct given a nil orderedMap object", - mu: NewMemUsageContext(vm, 88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 5000, 50, 50, 0.1, TestNativeMemUsageChecker{}), so: &setObject{}, expectedMem: SizeEmptyStruct, errExpected: nil, diff --git a/compiler_test.go b/compiler_test.go index 478279fa..a0c2fb8a 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -5657,7 +5657,7 @@ func TestProgramMemUsage(t *testing.T) { }{ { name: "mem below threshold", - mu: NewMemUsageContext(New(), 88, 50, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 50, 50, 50, 0.1, TestNativeMemUsageChecker{}), p: &Program{ values: []Value{ New().newDateObject(time.Now(), true, nil), @@ -5669,7 +5669,7 @@ func TestProgramMemUsage(t *testing.T) { }, { name: "mem way above threshold returns first crossing of threshold", - mu: NewMemUsageContext(New(), 88, 50, 50, 50, 0.1, TestNativeMemUsageChecker{}), + mu: NewMemUsageContext(88, 50, 50, 50, 0.1, TestNativeMemUsageChecker{}), p: &Program{ values: []Value{ New().newDateObject(time.Now(), true, nil), diff --git a/date_test.go b/date_test.go index 70feb588..57ce2f60 100644 --- a/date_test.go +++ b/date_test.go @@ -349,7 +349,7 @@ func TestDateMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/destruct_test.go b/destruct_test.go index 724b7bf0..ff8bc06b 100644 --- a/destruct_test.go +++ b/destruct_test.go @@ -27,7 +27,7 @@ func TestDestructMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/func_test.go b/func_test.go index 02bd3d10..a41951a1 100644 --- a/func_test.go +++ b/func_test.go @@ -184,7 +184,7 @@ func TestNativeFuncObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -234,7 +234,7 @@ func TestFuncObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -282,7 +282,7 @@ func TestBaseJsFuncObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -361,7 +361,7 @@ func TestClassFuncObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -421,7 +421,7 @@ func TestMethodFuncObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -490,7 +490,7 @@ func TestArrowFuncObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/mem_context.go b/mem_context.go index 4e9489cc..2b68ae69 100644 --- a/mem_context.go +++ b/mem_context.go @@ -3,6 +3,7 @@ package goja import ( "errors" "math" + "sync" ) type visitTracker struct { @@ -66,15 +67,25 @@ type MemUsageContext struct { memoryLimit uint64 } +// memContextMu is used to protect the latestMemUsageContext variable. +var memContextMu sync.RWMutex + +// latestMemUsageContext is used to store the latest MemUsageContext instance created. +// This is needed to allow arbitrary mem usage checks with the same exact configuration +// as the latest mem usage context. This is an escape hatch since a new mem usage +// context is usually only created from the client using goja. +var latestMemUsageContext *MemUsageContext + func NewMemUsageContext( - vm *Runtime, maxDepth int, memLimit uint64, arrayLenThreshold, objPropsLenThreshold int, sampleRate float64, nativeChecker NativeMemUsageChecker, ) *MemUsageContext { - return &MemUsageContext{ + memContextMu.Lock() + defer memContextMu.Unlock() + latestMemUsageContext = &MemUsageContext{ visitTracker: visitTracker{objsVisited: make(map[objectImpl]struct{}), stashesVisited: make(map[*stash]struct{})}, depthTracker: &depthTracker{curDepth: 0, maxDepth: maxDepth}, NativeMemUsageChecker: nativeChecker, @@ -91,6 +102,24 @@ func NewMemUsageContext( return computeSampleStep(totalItems, sampleRate) }, } + return latestMemUsageContext +} + +func newMemUsageContextClone() *MemUsageContext { + memContextMu.RLock() + defer memContextMu.RUnlock() + if latestMemUsageContext != nil { + return &MemUsageContext{ + visitTracker: visitTracker{objsVisited: make(map[objectImpl]struct{}), stashesVisited: make(map[*stash]struct{})}, + depthTracker: &depthTracker{curDepth: 0, maxDepth: latestMemUsageContext.maxDepth}, + NativeMemUsageChecker: latestMemUsageContext.NativeMemUsageChecker, + memoryLimit: latestMemUsageContext.memoryLimit, + ArrayLenExceedsThreshold: latestMemUsageContext.ArrayLenExceedsThreshold, + ObjectPropsLenExceedsThreshold: latestMemUsageContext.ObjectPropsLenExceedsThreshold, + ComputeSampleStep: latestMemUsageContext.ComputeSampleStep, + } + } + return nil } // MemUsageLimitExceeded ensures a limit function is defined and checks against the limit. If limit is breached @@ -123,7 +152,8 @@ func computeSampleStep(totalItems int, sampleRate float64) int { } var ( - ErrMaxDepth = errors.New("reached max depth") + ErrMaxDepth = errors.New("reached max depth") + ErrMemLimitExceeded = errors.New("execution memory limit exceeded") ) type MemUsageReporter interface { diff --git a/memory_test.go b/memory_test.go index b9314da3..6aba1fca 100644 --- a/memory_test.go +++ b/memory_test.go @@ -414,7 +414,7 @@ func TestMemCheck(t *testing.T) { vm.Set("checkMem", func(call FunctionCall) Value { mem, err := vm.MemUsage( - NewMemUsageContext(vm, 100, memUsageLimit, arrLenThreshold, objPropsLenThreshold, 0.1, TestNativeMemUsageChecker{}), + NewMemUsageContext(100, memUsageLimit, arrLenThreshold, objPropsLenThreshold, 0.1, TestNativeMemUsageChecker{}), ) if err != nil { t.Fatal(err) @@ -485,7 +485,7 @@ func TestMemMaxDepth(t *testing.T) { // All global variables are contained in the Runtime's globalObject field, which causes // them to be one level deeper _, err = vm.MemUsage( - NewMemUsageContext(vm, tc.expectedDepth, memUsageLimit, arrLenThreshold, objPropsLenThreshold, 0.1, TestNativeMemUsageChecker{}), + NewMemUsageContext(tc.expectedDepth, memUsageLimit, arrLenThreshold, objPropsLenThreshold, 0.1, TestNativeMemUsageChecker{}), ) if err != ErrMaxDepth { t.Fatalf("expected mem check to hit depth limit error, but got nil %v", err) @@ -493,7 +493,7 @@ func TestMemMaxDepth(t *testing.T) { _, err = vm.MemUsage( // need to add 2 to the expectedDepth since Object is lazy loaded it adds onto the expected depth - NewMemUsageContext(vm, tc.expectedDepth+2, memUsageLimit, arrLenThreshold, objPropsLenThreshold, 0.1, TestNativeMemUsageChecker{}), + NewMemUsageContext(tc.expectedDepth+2, memUsageLimit, arrLenThreshold, objPropsLenThreshold, 0.1, TestNativeMemUsageChecker{}), ) if err != nil { t.Fatalf("expected to NOT hit mem check hit depth limit error, but got %v", err) @@ -607,7 +607,7 @@ func TestMemArraysWithLenThreshold(t *testing.T) { vm.Set("checkMem", func(call FunctionCall) Value { mem, err := vm.MemUsage( - NewMemUsageContext(vm, 100, tc.memLimit, tc.threshold, objPropsLenThreshold, 0.1, TestNativeMemUsageChecker{}), + NewMemUsageContext(100, tc.memLimit, tc.threshold, objPropsLenThreshold, 0.1, TestNativeMemUsageChecker{}), ) if err != nil { t.Fatal(err) @@ -698,7 +698,7 @@ func TestMemObjectsWithPropsLenThreshold(t *testing.T) { vm.Set("checkMem", func(call FunctionCall) Value { mem, err := vm.MemUsage( - NewMemUsageContext(vm, 100, tc.memLimit, arrLenThreshold, tc.threshold, 0.1, TestNativeMemUsageChecker{}), + NewMemUsageContext(100, tc.memLimit, arrLenThreshold, tc.threshold, 0.1, TestNativeMemUsageChecker{}), ) if err != nil { t.Fatal(err) diff --git a/object.go b/object.go index 928b8c15..4013f686 100644 --- a/object.go +++ b/object.go @@ -548,6 +548,17 @@ func (o *baseObject) setOwnStr(name unistring.String, val Value, throw bool) boo o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) return false } else { + if o.val != nil && o.val.runtime != nil && o.val.runtime.shouldForceMemCheck { + if memCtx := newMemUsageContextClone(); memCtx != nil { + memUsage, err := val.MemUsage(memCtx) + if err != nil { + panic(err) + } + if memCtx.MemUsageLimitExceeded(memUsage) { + panic(ErrMemLimitExceeded) + } + } + } o.values[name] = val names := copyNamesIfNeeded(o.propNames, 1) o.propNames = append(names, name) diff --git a/object_dynamic_test.go b/object_dynamic_test.go index 3d6c2196..5f2ad57b 100644 --- a/object_dynamic_test.go +++ b/object_dynamic_test.go @@ -460,7 +460,7 @@ func TestBaseDynamicObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -503,7 +503,7 @@ func TestDynamicArrayMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/object_gomap_test.go b/object_gomap_test.go index e09eca8c..b24c226a 100644 --- a/object_gomap_test.go +++ b/object_gomap_test.go @@ -331,7 +331,7 @@ func TestGoMapUnicode(t *testing.T) { func TestGoMapMemUsage(t *testing.T) { vm := New() - vmCtx := NewMemUsageContext(vm, 100, 100, 100, 100, 0.1, nil) + vmCtx := NewMemUsageContext(100, 100, 100, 100, 0.1, nil) nestedMap := map[string]interface{}{ "subTest1": valueInt(99), @@ -503,7 +503,7 @@ func TestGoMapMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(vm, 100, tc.memLimit, 100, tc.estimateThreshold, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, tc.memLimit, 100, tc.estimateThreshold, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/object_goslice_test.go b/object_goslice_test.go index ccabb572..d2b48bcf 100644 --- a/object_goslice_test.go +++ b/object_goslice_test.go @@ -442,7 +442,7 @@ func TestGoSliceMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(vm, 100, tc.memLimit, tc.estimateThreshold, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, tc.memLimit, tc.estimateThreshold, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/object_test.go b/object_test.go index b5ced16b..4842b391 100644 --- a/object_test.go +++ b/object_test.go @@ -1,6 +1,7 @@ package goja import ( + "context" "fmt" "reflect" "strings" @@ -758,7 +759,7 @@ func TestBaseObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, tc.memLimit, 100, tc.threshold, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, tc.memLimit, 100, tc.threshold, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -816,7 +817,7 @@ func TestPrimitiveValueObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -829,3 +830,63 @@ func TestPrimitiveValueObjectMemUsage(t *testing.T) { }) } } + +func TestSetOwnStr(t *testing.T) { + for _, tc := range []struct { + name string + memLimit uint64 + expectedPanic bool + shouldForceMemCheck bool + }{ + { + name: "should not panic when setting a value in an object", + memLimit: 1000, + expectedPanic: false, + shouldForceMemCheck: false, + }, + { + name: "should panic when setting a value in an object over the mem limit and mem usage check is forced", + memLimit: 0, + expectedPanic: true, + shouldForceMemCheck: true, + }, + { + name: "should not panic when setting a value in an object over the mem limit and mem usage check is not forced", + memLimit: 0, + expectedPanic: false, + shouldForceMemCheck: false, + }, + { + name: "should not panic when setting a value in an object under the mem limit and mem usage check is forced", + memLimit: 1000, + expectedPanic: false, + shouldForceMemCheck: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + defer func() { + r := recover() + if tc.expectedPanic && r == nil { + t.Error("The code is expected to panic, but it didn't") + } + if !tc.expectedPanic && r != nil { + t.Errorf("The code panicked, but it should not have: %v", r) + } + }() + v := &Object{ + runtime: NewWithContext(context.Background(), tc.shouldForceMemCheck), + } + o := &baseObject{ + val: v, + extensible: true, + } + o.init() + v.self = o + + // Creating a mem usage context so it populates the package variable + NewMemUsageContext(100, tc.memLimit, 100, 100, 0.5, nil) + + o.setOwnStr("test", valueInt(123), false) + }) + } +} diff --git a/proxy_test.go b/proxy_test.go index d86a2d5f..ee474253 100644 --- a/proxy_test.go +++ b/proxy_test.go @@ -68,7 +68,7 @@ func TestProxyMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -116,7 +116,7 @@ func TestJSProxyHandlerMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/runtime.go b/runtime.go index c54d9736..7ee7b599 100644 --- a/runtime.go +++ b/runtime.go @@ -210,6 +210,8 @@ type Runtime struct { tickMetricTrackingEnabled bool tickMetrics map[string]uint64 + + shouldForceMemCheck bool } func (self *Runtime) Ticks() uint64 { @@ -1399,9 +1401,11 @@ func New() *Runtime { } // NewWithContext creates an instance of a Javascript runtime that can be used to run code. Multiple instances may be created and -// used simultaneously, however it is not possible to pass JS values across runtimes. -func NewWithContext(ctx context.Context) *Runtime { - r := &Runtime{ctx: ctx} +// used simultaneously, however it is not possible to pass JS values across runtimes. The 'shouldForceMemCheck' parameter is a +// safety net to trigger mem usage check on hot paths where memory usage is expected to be high but we can't otherwise catch it +// with the poller. +func NewWithContext(ctx context.Context, shouldForceMemCheck bool) *Runtime { + r := &Runtime{ctx: ctx, shouldForceMemCheck: shouldForceMemCheck} r.init() return r } diff --git a/runtime_test.go b/runtime_test.go index 4df4c308..b6aa517c 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -3182,7 +3182,7 @@ func TestRuntimeMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, tc.memLimit, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, tc.memLimit, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/string_ascii_test.go b/string_ascii_test.go index 821f8989..f3fc8965 100644 --- a/string_ascii_test.go +++ b/string_ascii_test.go @@ -27,7 +27,7 @@ func TestStringAsciiMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/string_imported_test.go b/string_imported_test.go index 736e9654..6dc5d019 100644 --- a/string_imported_test.go +++ b/string_imported_test.go @@ -27,7 +27,7 @@ func TestStringImportedMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/string_test.go b/string_test.go index 5426533b..5b8f1cd5 100644 --- a/string_test.go +++ b/string_test.go @@ -166,8 +166,6 @@ func BenchmarkASCIIConcat(b *testing.B) { } func TestStringObjectMemUsage(t *testing.T) { - vm := New() - for _, tc := range []struct { name string val *stringObject @@ -186,7 +184,7 @@ func TestStringObjectMemUsage(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - mem, err := tc.val.MemUsage(NewMemUsageContext(vm, 100, 100, 100, 100, 0.1, nil)) + mem, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != nil { t.Fatalf("Unexpected error. Actual: %v Expected: nil", err) } diff --git a/string_unicode_test.go b/string_unicode_test.go index 86d3c6e7..9291f3e8 100644 --- a/string_unicode_test.go +++ b/string_unicode_test.go @@ -27,7 +27,7 @@ func TestStringUnicodeMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/typedarrays_test.go b/typedarrays_test.go index a5746083..846de9e5 100644 --- a/typedarrays_test.go +++ b/typedarrays_test.go @@ -386,7 +386,7 @@ func TestArrayBufferObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -471,7 +471,7 @@ func TestTypedArrayObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -538,7 +538,7 @@ func TestDataViewObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/value_test.go b/value_test.go index d3b2fa66..25deed00 100644 --- a/value_test.go +++ b/value_test.go @@ -110,7 +110,7 @@ func TestValueMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -167,7 +167,7 @@ func TestValuePropertyMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -278,7 +278,7 @@ func TestObjectMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } diff --git a/vm_test.go b/vm_test.go index c75847c0..2ae53e5b 100644 --- a/vm_test.go +++ b/vm_test.go @@ -425,7 +425,7 @@ func TestValueStackMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, tc.memLimit, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, tc.memLimit, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -485,7 +485,7 @@ func TestVMContextMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -547,7 +547,7 @@ func TestStashMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := tc.val.MemUsage(NewMemUsageContext(New(), 100, 100, 100, 100, 0.1, nil)) + total, err := tc.val.MemUsage(NewMemUsageContext(100, 100, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) } @@ -601,7 +601,7 @@ func TestTmpValuesMemUsage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - total, err := valuesMemUsage(tc.val, NewMemUsageContext(New(), 100, tc.memLimit, 100, 100, 0.1, nil)) + total, err := valuesMemUsage(tc.val, NewMemUsageContext(100, tc.memLimit, 100, 100, 0.1, nil)) if err != tc.errExpected { t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected) }