Skip to content

Commit 7102826

Browse files
committed
Feature: Track and limit heap memory allocations
Implemented a memory allocation tracking feature that allows measure and limit heap allocations, useful for preventing out-of-bounds memory usage in sandboxed environments. - SetMemoryLimit(bytes): Configure maximum heap allocation limit - GetAllocatedBytes(): Query current tracked memory usage - ResetMemoryUsage(): Reset allocation counter Tracked allocation paths: - Table allocations (array and hash parts, growth/resizing) - String operations (concat, rep, format, gsub, etc.) - Function/closure allocations - VM registry growth
1 parent ccacf66 commit 7102826

12 files changed

Lines changed: 1391 additions & 327 deletions

File tree

.github/workflows/test.yaml

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,19 @@
1-
on: [push, pull_request]
1+
on: [push, pull_request, workflow_dispatch]
22
name: test
33
jobs:
44
test:
55
strategy:
66
fail-fast: false
77
matrix:
8-
go-version: [1.18.x, 1.19.x]
8+
go-version: [1.24.x, 1.25.x]
99
platform: [ubuntu-latest, macos-latest, windows-latest]
1010
runs-on: ${{ matrix.platform }}
1111
steps:
1212
- name: Install Go
13-
uses: actions/setup-go@v1
13+
uses: actions/setup-go@v5
1414
with:
1515
go-version: ${{ matrix.go-version }}
1616
- name: Checkout code
17-
uses: actions/checkout@v1
17+
uses: actions/checkout@v5
1818
- name: Run tests
19-
run: ./_tools/go-inline *.go && go fmt . && go test -v ./... -covermode=count -coverprofile=coverage.out -coverpkg=$(go list ./... | sed 's/\n/,/g')
20-
- name: Send coverage
21-
if: "matrix.platform == 'ubuntu-latest'"
22-
env:
23-
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24-
run: |
25-
GO111MODULE=off go get github.com/mattn/goveralls
26-
./_tools/go-inline *.go && go fmt .
27-
$(go env GOPATH)/bin/goveralls -coverprofile=coverage.out -service=github
19+
run: make test

Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
.PHONY: build test glua
22

33
build:
4-
./_tools/go-inline *.go && go fmt . && go build
4+
./_tools/go-inline *.go
5+
go fmt .
6+
go vet .
7+
go build
58

69
glua: *.go pm/*.go cmd/glua/glua.go
710
./_tools/go-inline *.go && go fmt . && go build cmd/glua/glua.go
811

912
test:
10-
./_tools/go-inline *.go && go fmt . && go test
13+
./_tools/go-inline *.go
14+
go fmt .
15+
go vet .
16+
go test

_state.go

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"sync"
1212
"sync/atomic"
1313
"time"
14+
"unsafe"
1415

1516
"github.com/yuin/gopher-lua/parse"
1617
)
@@ -394,6 +395,15 @@ func (rg *registry) resize(requiredSize int) { // +inline-start
394395
} // +inline-end
395396

396397
func (rg *registry) forceResize(newSize int) {
398+
// Track memory growth for registry resize
399+
oldSize := len(rg.array)
400+
if newSize > oldSize {
401+
additionalBytes := int64(newSize-oldSize) * 16 // LValue is 16 bytes
402+
if ls, ok := rg.handler.(*LState); ok {
403+
ls.TrackAlloc(additionalBytes)
404+
}
405+
}
406+
397407
newSlice := make([]LValue, newSize)
398408
copy(newSlice, rg.array[:rg.top]) // should we copy the area beyond top? there shouldn't be any valid values there so it shouldn't be necessary.
399409
rg.array = newSlice
@@ -589,6 +599,42 @@ func newLState(options Options) *LState {
589599
return ls
590600
}
591601

602+
/* Memory tracking {{{ */
603+
604+
// TrackAlloc adds bytes to the memory allocation counter and checks against the limit.
605+
// Raises a Lua error if the allocation would exceed the memory limit.
606+
func (ls *LState) TrackAlloc(bytes int64) {
607+
ls.allocatedBytes += bytes
608+
609+
// Only check limit if one is set
610+
if ls.maxBytes > 0 && ls.allocatedBytes > ls.maxBytes {
611+
ls.RaiseError("memory limit exceeded: %d bytes allocated, limit is %d bytes",
612+
ls.allocatedBytes, ls.maxBytes)
613+
}
614+
}
615+
616+
// ResetMemoryUsage resets the allocated bytes counter to zero.
617+
func (ls *LState) ResetMemoryUsage() {
618+
ls.allocatedBytes = 0
619+
}
620+
621+
// SetMemoryLimit sets the maximum memory limit in bytes. Set to 0 to disable limiting.
622+
func (ls *LState) SetMemoryLimit(maxBytes int64) {
623+
ls.maxBytes = maxBytes
624+
}
625+
626+
// GetAllocatedBytes returns the current number of tracked allocated bytes.
627+
func (ls *LState) GetAllocatedBytes() int64 {
628+
return ls.allocatedBytes
629+
}
630+
631+
// GetMemoryLimit returns the current memory limit in bytes (0 if no limit).
632+
func (ls *LState) GetMemoryLimit() int64 {
633+
return ls.maxBytes
634+
}
635+
636+
/* }}} */
637+
592638
func (ls *LState) printReg() {
593639
println("-------------------------")
594640
println("thread:", ls)
@@ -995,7 +1041,7 @@ func (ls *LState) initCallFrame(cf *callFrame) { // +inline-start
9951041
if CompatVarArg {
9961042
ls.reg.SetTop(cf.LocalBase + nargs + np + 1)
9971043
if (proto.IsVarArg & VarArgNeedsArg) != 0 {
998-
argtb := newLTable(nvarargs, 0)
1044+
argtb := ls.newLTable(nvarargs, 0)
9991045
for i := 0; i < nvarargs; i++ {
10001046
argtb.RawSetInt(i+1, ls.reg.Get(cf.LocalBase+np+i))
10011047
}
@@ -1333,7 +1379,6 @@ func (ls *LState) Get(idx int) LValue {
13331379
return LNil
13341380
}
13351381
}
1336-
return LNil
13371382
}
13381383

13391384
func (ls *LState) Push(value LValue) {
@@ -1389,11 +1434,11 @@ func (ls *LState) Remove(index int) {
13891434
/* object allocation {{{ */
13901435

13911436
func (ls *LState) NewTable() *LTable {
1392-
return newLTable(defaultArrayCap, defaultHashCap)
1437+
return ls.newLTable(defaultArrayCap, defaultHashCap)
13931438
}
13941439

13951440
func (ls *LState) CreateTable(acap, hcap int) *LTable {
1396-
return newLTable(acap, hcap)
1441+
return ls.newLTable(acap, hcap)
13971442
}
13981443

13991444
// NewThread returns a new LState that shares with the original state all global objects.
@@ -1412,22 +1457,25 @@ func (ls *LState) NewThread() (*LState, context.CancelFunc) {
14121457
}
14131458

14141459
func (ls *LState) NewFunctionFromProto(proto *FunctionProto) *LFunction {
1415-
return newLFunctionL(proto, ls.Env, int(proto.NumUpvalues))
1460+
return ls.newLFunctionL(proto, ls.Env, int(proto.NumUpvalues))
14161461
}
14171462

14181463
func (ls *LState) NewUserData() *LUserData {
1464+
size := int64(unsafe.Sizeof(LUserData{}))
1465+
ls.TrackAlloc(size)
1466+
14191467
return &LUserData{
14201468
Env: ls.currentEnv(),
14211469
Metatable: LNil,
14221470
}
14231471
}
14241472

14251473
func (ls *LState) NewFunction(fn LGFunction) *LFunction {
1426-
return newLFunctionG(fn, ls.currentEnv(), 0)
1474+
return ls.newLFunctionG(fn, ls.currentEnv(), 0)
14271475
}
14281476

14291477
func (ls *LState) NewClosure(fn LGFunction, upvalues ...LValue) *LFunction {
1430-
cl := newLFunctionG(fn, ls.currentEnv(), len(upvalues))
1478+
cl := ls.newLFunctionG(fn, ls.currentEnv(), len(upvalues))
14311479
for i, lv := range upvalues {
14321480
cl.Upvalues[i] = &Upvalue{}
14331481
cl.Upvalues[i].Close()
@@ -1806,7 +1854,7 @@ func (ls *LState) Load(reader io.Reader, name string) (*LFunction, error) {
18061854
if err != nil {
18071855
return nil, newApiErrorE(ApiErrorSyntax, err)
18081856
}
1809-
return newLFunctionL(proto, ls.currentEnv(), 0), nil
1857+
return ls.newLFunctionL(proto, ls.currentEnv(), 0), nil
18101858
}
18111859

18121860
func (ls *LState) Call(nargs, nret int) {
@@ -1882,7 +1930,7 @@ func (ls *LState) PCall(nargs, nret int, errfunc *LFunction) (err error) {
18821930
}
18831931

18841932
func (ls *LState) GPCall(fn LGFunction, data LValue) error {
1885-
ls.Push(newLFunctionG(fn, ls.currentEnv(), 0))
1933+
ls.Push(ls.newLFunctionG(fn, ls.currentEnv(), 0))
18861934
ls.Push(data)
18871935
return ls.PCall(1, MultRet, nil)
18881936
}

0 commit comments

Comments
 (0)