From d622770ceb19693ffd81ddb44868717dbe9a07b9 Mon Sep 17 00:00:00 2001 From: kimjooyoon Date: Tue, 27 Oct 2026 03:04:35 +0900 Subject: [PATCH 1/2] Issue #554: reject overflowing upvalue counts during compilation --- compile.go | 3 ++ compile_upvalues_test.go | 68 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 compile_upvalues_test.go diff --git a/compile.go b/compile.go index f9fbf576..622c5f83 100644 --- a/compile.go +++ b/compile.go @@ -1336,6 +1336,9 @@ func compileFunctionExpr(context *funcContext, funcexpr *ast.FunctionExpr, ec *e context.Proto.Code = context.Code.List() context.Proto.DbgSourcePositions = context.Code.PosList() context.Proto.DbgUpvalues = context.Upvalues.Names() + if len(context.Proto.DbgUpvalues) > math.MaxUint8 { + raiseCompileError(context, context.Proto.LineDefined, "too many upvalues") + } context.Proto.NumUpvalues = uint8(len(context.Proto.DbgUpvalues)) for _, clv := range context.Proto.Constants { sv := "" diff --git a/compile_upvalues_test.go b/compile_upvalues_test.go new file mode 100644 index 00000000..8e7ac513 --- /dev/null +++ b/compile_upvalues_test.go @@ -0,0 +1,68 @@ +package lua + +import ( + "fmt" + "strings" + "testing" +) + +func TestCompileUpvalueLimit(t *testing.T) { + tests := []struct { + upvalues int + wantError bool + }{ + {254, false}, + {255, false}, + {256, true}, + {300, true}, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("%d", tt.upvalues), func(t *testing.T) { + // Split locals between two enclosing functions so the leaf closure + // reaches the upvalue limit without reaching the local-variable limit. + var source strings.Builder + source.WriteString("local function outer()\n") + for i := 0; i < tt.upvalues/2; i++ { + fmt.Fprintf(&source, "local v%d = 1\n", i) + } + source.WriteString("return function()\n") + for i := tt.upvalues / 2; i < tt.upvalues; i++ { + fmt.Fprintf(&source, "local v%d = 1\n", i) + } + source.WriteString("return function()\nreturn ") + for i := 0; i < tt.upvalues; i++ { + if i > 0 { + source.WriteString(" + ") + } + fmt.Fprintf(&source, "v%d", i) + } + source.WriteString("\nend\nend\nend\nreturn outer()()()\n") + + L := NewState() + defer L.Close() + fn, err := L.LoadString(source.String()) + if tt.wantError { + if err == nil { + t.Fatal("LoadString accepted too many upvalues") + } + apiErr, ok := err.(*ApiError) + if !ok || apiErr.Type != ApiErrorSyntax { + t.Fatalf("LoadString error = %v (%T), want ApiErrorSyntax", err, err) + } + if !strings.Contains(err.Error(), "too many upvalues") { + t.Fatalf("LoadString error = %q, want too many upvalues", err) + } + return + } + if err != nil { + t.Fatalf("LoadString failed: %v", err) + } + if err := L.CallByParam(P{Fn: fn, NRet: 1, Protect: true}); err != nil { + t.Fatalf("closure execution failed: %v", err) + } + if got := L.Get(-1); got != LNumber(tt.upvalues) { + t.Fatalf("closure returned %v, want %d", got, tt.upvalues) + } + }) + } +} From 75d727ec4a16052c23a6a220d87983e73da4e835 Mon Sep 17 00:00:00 2001 From: kimjooyoon Date: Tue, 27 Oct 2026 03:20:39 +0900 Subject: [PATCH 2/2] Issue #555: report the source line of local and register overflow --- compile.go | 44 ++++++------ compile_locations_test.go | 138 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 19 deletions(-) create mode 100644 compile_locations_test.go diff --git a/compile.go b/compile.go index 622c5f83..623a5b67 100644 --- a/compile.go +++ b/compile.go @@ -541,10 +541,10 @@ func (fc *funcContext) BlockLocalVarsCount() int { return count } -func (fc *funcContext) RegisterLocalVar(name string) int { +func (fc *funcContext) RegisterLocalVar(name string, line int) int { ret := fc.Block.LocalVars.Register(name) fc.Proto.DbgLocals = append(fc.Proto.DbgLocals, &DbgLocalInfo{Name: name, StartPc: fc.Code.LastPC() + 1}) - fc.SetRegTop(fc.RegTop() + 1) + fc.SetRegTop(fc.RegTop()+1, line) return ret } @@ -592,7 +592,7 @@ func (fc *funcContext) LeaveBlock() int { fc.ResolveCurrentBlockGotosWithParentBlock() } fc.Block = fc.Block.Parent - fc.SetRegTop(fc.Block.LocalVars.LastIndex()) + fc.SetRegTop(fc.Block.LocalVars.LastIndex(), fc.Proto.LineDefined) return closed } @@ -602,9 +602,9 @@ func (fc *funcContext) EndScope() { } } -func (fc *funcContext) SetRegTop(top int) { +func (fc *funcContext) SetRegTop(top, line int) { if top > maxRegisters { - raiseCompileError(fc, fc.Proto.LineDefined, "too many local variables") + raiseCompileError(fc, line, "too many local variables") } fc.regTop = top } @@ -857,7 +857,7 @@ func compileLocalAssignStmt(context *funcContext, stmt *ast.LocalAssignStmt) { / reg := context.RegTop() if len(stmt.Names) == 1 && len(stmt.Exprs) == 1 { if _, ok := stmt.Exprs[0].(*ast.FunctionExpr); ok { - context.RegisterLocalVar(stmt.Names[0]) + context.RegisterLocalVar(stmt.Names[0], sline(stmt)) compileRegAssignment(context, stmt.Names, stmt.Exprs, reg, len(stmt.Names), sline(stmt)) return } @@ -865,7 +865,7 @@ func compileLocalAssignStmt(context *funcContext, stmt *ast.LocalAssignStmt) { / compileRegAssignment(context, stmt.Names, stmt.Exprs, reg, len(stmt.Names), sline(stmt)) for _, name := range stmt.Names { - context.RegisterLocalVar(name) + context.RegisterLocalVar(name, sline(stmt)) } } // }}} @@ -1054,17 +1054,17 @@ func compileNumberForStmt(context *funcContext, stmt *ast.NumberForStmt) { // {{ context.EnterBlock(endlabel, stmt) reg := context.RegTop() - rindex := context.RegisterLocalVar("(for index)") + rindex := context.RegisterLocalVar("(for index)", sline(stmt)) ecupdate(ec, ecLocal, rindex, 0) compileExpr(context, reg, stmt.Init, ec) reg = context.RegTop() - rlimit := context.RegisterLocalVar("(for limit)") + rlimit := context.RegisterLocalVar("(for limit)", sline(stmt)) ecupdate(ec, ecLocal, rlimit, 0) compileExpr(context, reg, stmt.Limit, ec) reg = context.RegTop() - rstep := context.RegisterLocalVar("(for step)") + rstep := context.RegisterLocalVar("(for step)", sline(stmt)) if stmt.Step == nil { stmt.Step = &ast.NumberExpr{Value: "1"} stmt.Step.SetLine(sline(stmt.Init)) @@ -1074,7 +1074,7 @@ func compileNumberForStmt(context *funcContext, stmt *ast.NumberForStmt) { // {{ code.AddASbx(OP_FORPREP, rindex, 0, sline(stmt)) - context.RegisterLocalVar(stmt.Name) + context.RegisterLocalVar(stmt.Name, sline(stmt)) bodypc := code.LastPC() compileChunk(context, stmt.Stmts, false) @@ -1097,16 +1097,16 @@ func compileGenericForStmt(context *funcContext, stmt *ast.GenericForStmt) { // nnames := len(stmt.Names) context.EnterBlock(endlabel, stmt) - rgen := context.RegisterLocalVar("(for generator)") - context.RegisterLocalVar("(for state)") - context.RegisterLocalVar("(for control)") + rgen := context.RegisterLocalVar("(for generator)", sline(stmt)) + context.RegisterLocalVar("(for state)", sline(stmt)) + context.RegisterLocalVar("(for control)", sline(stmt)) compileRegAssignment(context, stmt.Names, stmt.Exprs, context.RegTop()-3, 3, sline(stmt)) code.AddASbx(OP_JMP, 0, fllabel, sline(stmt)) for _, name := range stmt.Names { - context.RegisterLocalVar(name) + context.RegisterLocalVar(name, sline(stmt)) } context.SetLabelPc(bodylabel, code.LastPC()) @@ -1313,16 +1313,16 @@ func compileFunctionExpr(context *funcContext, funcexpr *ast.FunctionExpr, ec *e context.Proto.NumParameters = uint8(len(funcexpr.ParList.Names)) if ec.ctype == ecMethod { context.Proto.NumParameters += 1 - context.RegisterLocalVar("self") + context.RegisterLocalVar("self", sline(funcexpr)) } for _, name := range funcexpr.ParList.Names { - context.RegisterLocalVar(name) + context.RegisterLocalVar(name, sline(funcexpr)) } if funcexpr.ParList.HasVargs { if CompatVarArg { context.Proto.IsVarArg = VarArgHasArg | VarArgNeedsArg if context.Parent != nil { - context.RegisterLocalVar("arg") + context.RegisterLocalVar("arg", sline(funcexpr)) } } context.Proto.IsVarArg |= VarArgIsVarArg @@ -1773,13 +1773,16 @@ func getExprName(context *funcContext, expr ast.Expr) string { // {{{ func patchCode(context *funcContext) { // {{{ maxreg := 1 + overflowLine := context.Proto.LineDefined if np := int(context.Proto.NumParameters); np > 1 { maxreg = np } moven := 0 code := context.Code.List() + lines := context.Code.PosList() for pc := 0; pc < len(code); pc++ { inst := code[pc] + previousMax := maxreg curop := opGetOpCode(inst) switch curop { case OP_CLOSURE: @@ -1830,6 +1833,9 @@ func patchCode(context *funcContext) { // {{{ maxreg = reg } } + if previousMax < maxRegisters && maxreg >= maxRegisters { + overflowLine = lines[pc] + } // bulk move optimization(reducing op dipatch costs) if curop == OP_MOVE { @@ -1844,7 +1850,7 @@ func patchCode(context *funcContext) { // {{{ } maxreg++ if maxreg > maxRegisters { - raiseCompileError(context, context.Proto.LineDefined, "register overflow(too many local variables)") + raiseCompileError(context, overflowLine, "register overflow(too many local variables)") } context.Proto.NumUsedRegisters = uint8(maxreg) } // }}} diff --git a/compile_locations_test.go b/compile_locations_test.go new file mode 100644 index 00000000..81929034 --- /dev/null +++ b/compile_locations_test.go @@ -0,0 +1,138 @@ +package lua + +import ( + "fmt" + "strings" + "testing" + + "github.com/yuin/gopher-lua/parse" +) + +func TestCompileRegisterLimitLocations(t *testing.T) { + locals := func(count int, initializer string) string { + var source strings.Builder + for i := 0; i < count; i++ { + fmt.Fprintf(&source, "local v%d%s\n", i, initializer) + } + return source.String() + } + call := func(arguments int) string { + return "consume(" + strings.TrimSuffix(strings.Repeat("1,", arguments), ",") + ")\n" + } + const mainPrefix = "-- main chunk\n" + const nestedPrefix = "-- nested function\n\n\n\nlocal function oversized()\n" + const localError = "too many local variables" + const registerError = "register overflow(too many local variables)" + locals195 := mainPrefix + locals(195, " = 1") + locals200 := mainPrefix + locals(200, " = 1") + multilineCall := "consume(\n" + strings.Repeat("1,\n", 19) + "1\n)\n" + + tests := []struct { + name string + source string + wantLine int + wantMessage string + }{ + { + name: "initialized_locals_in_main", + source: mainPrefix + locals(210, " = 1"), + wantLine: 202, + wantMessage: localError, + }, + { + name: "initialized_locals_in_nested_function", + source: nestedPrefix + locals(210, " = 1") + "end\n", + wantLine: 206, + wantMessage: localError, + }, + { + name: "uninitialized_locals_in_main", + source: mainPrefix + locals(210, ""), + wantLine: 202, + wantMessage: localError, + }, + { + name: "uninitialized_locals_in_nested_function", + source: nestedPrefix + locals(210, "") + "end\n", + wantLine: 206, + wantMessage: localError, + }, + { + name: "multiline_initializer_uses_declaration_start", + source: locals200 + "local overflow =\n1\n", + wantLine: 202, + wantMessage: localError, + }, + { + name: "local_function_declaration", + source: locals200 + "local function overflow()\nend\n", + wantLine: 202, + wantMessage: localError, + }, + { + name: "call_arguments_on_one_line", + source: locals195 + call(20), + wantLine: 197, + wantMessage: registerError, + }, + { + name: "multiline_call_uses_first_overflowing_argument", + source: locals195 + multilineCall, + // The function uses register 195; the fifth argument uses register + // 200, the first beyond the 200-register limit, on source line 202. + wantLine: 202, + wantMessage: registerError, + }, + { + name: "later_larger_call_keeps_first_overflow_location", + source: locals195 + call(20) + "\n" + call(40), + wantLine: 197, + wantMessage: registerError, + }, + { + name: "accepted_200_locals", + source: locals200 + "return v199\n", + }, + { + name: "accepted_call_below_register_limit", + source: locals195 + call(4), + }, + { + name: "accepted_locals_reused_after_scope_exit", + source: "do\n" + locals(200, " = 1") + "end\n" + + "do\n" + locals(200, " = 1") + "end\nreturn 1\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + name := tt.name + ".lua" + chunk, err := parse.Parse(strings.NewReader(tt.source), name) + if err != nil { + t.Fatalf("Parse failed before compilation: %v", err) + } + proto, err := Compile(chunk, name) + if tt.wantMessage == "" { + if err != nil { + t.Fatalf("Compile rejected an accepted register count: %v", err) + } + if proto == nil { + t.Fatal("Compile returned no function prototype") + } + return + } + if err == nil { + t.Fatal("Compile accepted an overflowing register count") + } + compileErr, ok := err.(*CompileError) + if !ok { + t.Fatalf("Compile error = %v (%T), want *CompileError", err, err) + } + if compileErr.Message != tt.wantMessage { + t.Errorf("Compile error message = %q, want %q", compileErr.Message, tt.wantMessage) + } + if compileErr.Line != tt.wantLine { + t.Errorf("Compile error line = %d, want %d", compileErr.Line, tt.wantLine) + } + }) + } +}