Skip to content

Commit d69af72

Browse files
rolandshoemakerbradfitz
authored andcommitted
[release-branch.go1.18] go/parser: limit recursion depth
Limit nested parsing to 100,000, which prevents stack exhaustion when parsing deeply nested statements, types, and expressions. Also limit the scope depth to 1,000 during object resolution. Thanks to Juho Nurminen of Mattermost for reporting this issue. Fixes golang#53708 Updates golang#53616 Fixes CVE-2022-1962 Change-Id: I4d7b86c1d75d0bf3c7af1fdea91582aa74272c64 Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1491025 Reviewed-by: Russ Cox <[email protected]> Reviewed-by: Damien Neil <[email protected]> (cherry picked from commit 6a856f08d58e4b6705c0c337d461c540c1235c83) Reviewed-on: https://go-review.googlesource.com/c/go/+/417056 TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Michael Knyszek <[email protected]> Reviewed-by: Heschi Kreinick <[email protected]>
1 parent f53d677 commit d69af72

File tree

4 files changed

+234
-8
lines changed

4 files changed

+234
-8
lines changed

src/go/parser/interface.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,11 @@ func ParseFile(fset *token.FileSet, filename string, src any, mode Mode) (f *ast
9797
defer func() {
9898
if e := recover(); e != nil {
9999
// resume same panic if it's not a bailout
100-
if _, ok := e.(bailout); !ok {
100+
bail, ok := e.(bailout)
101+
if !ok {
101102
panic(e)
103+
} else if bail.msg != "" {
104+
p.errors.Add(p.file.Position(bail.pos), bail.msg)
102105
}
103106
}
104107

@@ -203,8 +206,11 @@ func ParseExprFrom(fset *token.FileSet, filename string, src any, mode Mode) (ex
203206
defer func() {
204207
if e := recover(); e != nil {
205208
// resume same panic if it's not a bailout
206-
if _, ok := e.(bailout); !ok {
209+
bail, ok := e.(bailout)
210+
if !ok {
207211
panic(e)
212+
} else if bail.msg != "" {
213+
p.errors.Add(p.file.Position(bail.pos), bail.msg)
208214
}
209215
}
210216
p.errors.Sort()

src/go/parser/parser.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ type parser struct {
6060
inRhs bool // if set, the parser is parsing a rhs expression
6161

6262
imports []*ast.ImportSpec // list of imports
63+
64+
// nestLev is used to track and limit the recursion depth
65+
// during parsing.
66+
nestLev int
6367
}
6468

6569
func (p *parser) init(fset *token.FileSet, filename string, src []byte, mode Mode) {
@@ -109,6 +113,24 @@ func un(p *parser) {
109113
p.printTrace(")")
110114
}
111115

116+
// maxNestLev is the deepest we're willing to recurse during parsing
117+
const maxNestLev int = 1e5
118+
119+
func incNestLev(p *parser) *parser {
120+
p.nestLev++
121+
if p.nestLev > maxNestLev {
122+
p.error(p.pos, "exceeded max nesting depth")
123+
panic(bailout{})
124+
}
125+
return p
126+
}
127+
128+
// decNestLev is used to track nesting depth during parsing to prevent stack exhaustion.
129+
// It is used along with incNestLev in a similar fashion to how un and trace are used.
130+
func decNestLev(p *parser) {
131+
p.nestLev--
132+
}
133+
112134
// Advance to the next token.
113135
func (p *parser) next0() {
114136
// Because of one-token look-ahead, print the previous token
@@ -221,8 +243,12 @@ func (p *parser) next() {
221243
}
222244
}
223245

224-
// A bailout panic is raised to indicate early termination.
225-
type bailout struct{}
246+
// A bailout panic is raised to indicate early termination. pos and msg are
247+
// only populated when bailing out of object resolution.
248+
type bailout struct {
249+
pos token.Pos
250+
msg string
251+
}
226252

227253
func (p *parser) error(pos token.Pos, msg string) {
228254
if p.trace {
@@ -1252,6 +1278,8 @@ func (p *parser) parseTypeInstance(typ ast.Expr) ast.Expr {
12521278
}
12531279

12541280
func (p *parser) tryIdentOrType() ast.Expr {
1281+
defer decNestLev(incNestLev(p))
1282+
12551283
switch p.tok {
12561284
case token.IDENT:
12571285
typ := p.parseTypeName(nil)
@@ -1664,7 +1692,13 @@ func (p *parser) parsePrimaryExpr(x ast.Expr) ast.Expr {
16641692
if x == nil {
16651693
x = p.parseOperand()
16661694
}
1667-
for {
1695+
// We track the nesting here rather than at the entry for the function,
1696+
// since it can iteratively produce a nested output, and we want to
1697+
// limit how deep a structure we generate.
1698+
var n int
1699+
defer func() { p.nestLev -= n }()
1700+
for n = 1; ; n++ {
1701+
incNestLev(p)
16681702
switch p.tok {
16691703
case token.PERIOD:
16701704
p.next()
@@ -1724,6 +1758,8 @@ func (p *parser) parsePrimaryExpr(x ast.Expr) ast.Expr {
17241758
}
17251759

17261760
func (p *parser) parseUnaryExpr() ast.Expr {
1761+
defer decNestLev(incNestLev(p))
1762+
17271763
if p.trace {
17281764
defer un(trace(p, "UnaryExpr"))
17291765
}
@@ -1813,7 +1849,13 @@ func (p *parser) parseBinaryExpr(x ast.Expr, prec1 int, check bool) ast.Expr {
18131849
if x == nil {
18141850
x = p.parseUnaryExpr()
18151851
}
1816-
for {
1852+
// We track the nesting here rather than at the entry for the function,
1853+
// since it can iteratively produce a nested output, and we want to
1854+
// limit how deep a structure we generate.
1855+
var n int
1856+
defer func() { p.nestLev -= n }()
1857+
for n = 1; ; n++ {
1858+
incNestLev(p)
18171859
op, oprec := p.tokPrec()
18181860
if oprec < prec1 {
18191861
return x
@@ -2123,6 +2165,8 @@ func (p *parser) parseIfHeader() (init ast.Stmt, cond ast.Expr) {
21232165
}
21242166

21252167
func (p *parser) parseIfStmt() *ast.IfStmt {
2168+
defer decNestLev(incNestLev(p))
2169+
21262170
if p.trace {
21272171
defer un(trace(p, "IfStmt"))
21282172
}
@@ -2426,6 +2470,8 @@ func (p *parser) parseForStmt() ast.Stmt {
24262470
}
24272471

24282472
func (p *parser) parseStmt() (s ast.Stmt) {
2473+
defer decNestLev(incNestLev(p))
2474+
24292475
if p.trace {
24302476
defer un(trace(p, "Statement"))
24312477
}

src/go/parser/parser_test.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"go/ast"
1111
"go/token"
1212
"io/fs"
13+
"runtime"
1314
"strings"
1415
"testing"
1516
)
@@ -577,3 +578,171 @@ type x int // comment
577578
t.Errorf("got %q, want %q", comment, "// comment")
578579
}
579580
}
581+
582+
var parseDepthTests = []struct {
583+
name string
584+
format string
585+
// multipler is used when a single statement may result in more than one
586+
// change in the depth level, for instance "1+(..." produces a BinaryExpr
587+
// followed by a UnaryExpr, which increments the depth twice. The test
588+
// case comment explains which nodes are triggering the multiple depth
589+
// changes.
590+
parseMultiplier int
591+
// scope is true if we should also test the statement for the resolver scope
592+
// depth limit.
593+
scope bool
594+
// scopeMultiplier does the same as parseMultiplier, but for the scope
595+
// depths.
596+
scopeMultiplier int
597+
}{
598+
// The format expands the part inside « » many times.
599+
// A second set of brackets nested inside the first stops the repetition,
600+
// so that for example «(«1»)» expands to (((...((((1))))...))).
601+
{name: "array", format: "package main; var x «[1]»int"},
602+
{name: "slice", format: "package main; var x «[]»int"},
603+
{name: "struct", format: "package main; var x «struct { X «int» }»", scope: true},
604+
{name: "pointer", format: "package main; var x «*»int"},
605+
{name: "func", format: "package main; var x «func()»int", scope: true},
606+
{name: "chan", format: "package main; var x «chan »int"},
607+
{name: "chan2", format: "package main; var x «<-chan »int"},
608+
{name: "interface", format: "package main; var x «interface { M() «int» }»", scope: true, scopeMultiplier: 2}, // Scopes: InterfaceType, FuncType
609+
{name: "map", format: "package main; var x «map[int]»int"},
610+
{name: "slicelit", format: "package main; var x = «[]any{«»}»", parseMultiplier: 2}, // Parser nodes: UnaryExpr, CompositeLit
611+
{name: "arraylit", format: "package main; var x = «[1]any{«nil»}»", parseMultiplier: 2}, // Parser nodes: UnaryExpr, CompositeLit
612+
{name: "structlit", format: "package main; var x = «struct{x any}{«nil»}»", parseMultiplier: 2}, // Parser nodes: UnaryExpr, CompositeLit
613+
{name: "maplit", format: "package main; var x = «map[int]any{1:«nil»}»", parseMultiplier: 2}, // Parser nodes: CompositeLit, KeyValueExpr
614+
{name: "dot", format: "package main; var x = «x.»x"},
615+
{name: "index", format: "package main; var x = x«[1]»"},
616+
{name: "slice", format: "package main; var x = x«[1:2]»"},
617+
{name: "slice3", format: "package main; var x = x«[1:2:3]»"},
618+
{name: "dottype", format: "package main; var x = x«.(any)»"},
619+
{name: "callseq", format: "package main; var x = x«()»"},
620+
{name: "methseq", format: "package main; var x = x«.m()»", parseMultiplier: 2}, // Parser nodes: SelectorExpr, CallExpr
621+
{name: "binary", format: "package main; var x = «1+»1"},
622+
{name: "binaryparen", format: "package main; var x = «1+(«1»)»", parseMultiplier: 2}, // Parser nodes: BinaryExpr, ParenExpr
623+
{name: "unary", format: "package main; var x = «^»1"},
624+
{name: "addr", format: "package main; var x = «& »x"},
625+
{name: "star", format: "package main; var x = «*»x"},
626+
{name: "recv", format: "package main; var x = «<-»x"},
627+
{name: "call", format: "package main; var x = «f(«1»)»", parseMultiplier: 2}, // Parser nodes: Ident, CallExpr
628+
{name: "conv", format: "package main; var x = «(*T)(«1»)»", parseMultiplier: 2}, // Parser nodes: ParenExpr, CallExpr
629+
{name: "label", format: "package main; func main() { «Label:» }"},
630+
{name: "if", format: "package main; func main() { «if true { «» }»}", parseMultiplier: 2, scope: true, scopeMultiplier: 2}, // Parser nodes: IfStmt, BlockStmt. Scopes: IfStmt, BlockStmt
631+
{name: "ifelse", format: "package main; func main() { «if true {} else » {} }", scope: true},
632+
{name: "switch", format: "package main; func main() { «switch { default: «» }»}", scope: true, scopeMultiplier: 2}, // Scopes: TypeSwitchStmt, CaseClause
633+
{name: "typeswitch", format: "package main; func main() { «switch x.(type) { default: «» }» }", scope: true, scopeMultiplier: 2}, // Scopes: TypeSwitchStmt, CaseClause
634+
{name: "for0", format: "package main; func main() { «for { «» }» }", scope: true, scopeMultiplier: 2}, // Scopes: ForStmt, BlockStmt
635+
{name: "for1", format: "package main; func main() { «for x { «» }» }", scope: true, scopeMultiplier: 2}, // Scopes: ForStmt, BlockStmt
636+
{name: "for3", format: "package main; func main() { «for f(); g(); h() { «» }» }", scope: true, scopeMultiplier: 2}, // Scopes: ForStmt, BlockStmt
637+
{name: "forrange0", format: "package main; func main() { «for range x { «» }» }", scope: true, scopeMultiplier: 2}, // Scopes: RangeStmt, BlockStmt
638+
{name: "forrange1", format: "package main; func main() { «for x = range z { «» }» }", scope: true, scopeMultiplier: 2}, // Scopes: RangeStmt, BlockStmt
639+
{name: "forrange2", format: "package main; func main() { «for x, y = range z { «» }» }", scope: true, scopeMultiplier: 2}, // Scopes: RangeStmt, BlockStmt
640+
{name: "go", format: "package main; func main() { «go func() { «» }()» }", parseMultiplier: 2, scope: true}, // Parser nodes: GoStmt, FuncLit
641+
{name: "defer", format: "package main; func main() { «defer func() { «» }()» }", parseMultiplier: 2, scope: true}, // Parser nodes: DeferStmt, FuncLit
642+
{name: "select", format: "package main; func main() { «select { default: «» }» }", scope: true},
643+
}
644+
645+
// split splits pre«mid»post into pre, mid, post.
646+
// If the string does not have that form, split returns x, "", "".
647+
func split(x string) (pre, mid, post string) {
648+
start, end := strings.Index(x, "«"), strings.LastIndex(x, "»")
649+
if start < 0 || end < 0 {
650+
return x, "", ""
651+
}
652+
return x[:start], x[start+len("«") : end], x[end+len("»"):]
653+
}
654+
655+
func TestParseDepthLimit(t *testing.T) {
656+
if runtime.GOARCH == "wasm" {
657+
t.Skip("causes call stack exhaustion on js/wasm")
658+
}
659+
for _, tt := range parseDepthTests {
660+
for _, size := range []string{"small", "big"} {
661+
t.Run(tt.name+"/"+size, func(t *testing.T) {
662+
n := maxNestLev + 1
663+
if tt.parseMultiplier > 0 {
664+
n /= tt.parseMultiplier
665+
}
666+
if size == "small" {
667+
// Decrease the number of statements by 10, in order to check
668+
// that we do not fail when under the limit. 10 is used to
669+
// provide some wiggle room for cases where the surrounding
670+
// scaffolding syntax adds some noise to the depth that changes
671+
// on a per testcase basis.
672+
n -= 10
673+
}
674+
675+
pre, mid, post := split(tt.format)
676+
if strings.Contains(mid, "«") {
677+
left, base, right := split(mid)
678+
mid = strings.Repeat(left, n) + base + strings.Repeat(right, n)
679+
} else {
680+
mid = strings.Repeat(mid, n)
681+
}
682+
input := pre + mid + post
683+
684+
fset := token.NewFileSet()
685+
_, err := ParseFile(fset, "", input, ParseComments|SkipObjectResolution)
686+
if size == "small" {
687+
if err != nil {
688+
t.Errorf("ParseFile(...): %v (want success)", err)
689+
}
690+
} else {
691+
expected := "exceeded max nesting depth"
692+
if err == nil || !strings.HasSuffix(err.Error(), expected) {
693+
t.Errorf("ParseFile(...) = _, %v, want %q", err, expected)
694+
}
695+
}
696+
})
697+
}
698+
}
699+
}
700+
701+
func TestScopeDepthLimit(t *testing.T) {
702+
if runtime.GOARCH == "wasm" {
703+
t.Skip("causes call stack exhaustion on js/wasm")
704+
}
705+
for _, tt := range parseDepthTests {
706+
if !tt.scope {
707+
continue
708+
}
709+
for _, size := range []string{"small", "big"} {
710+
t.Run(tt.name+"/"+size, func(t *testing.T) {
711+
n := maxScopeDepth + 1
712+
if tt.scopeMultiplier > 0 {
713+
n /= tt.scopeMultiplier
714+
}
715+
if size == "small" {
716+
// Decrease the number of statements by 10, in order to check
717+
// that we do not fail when under the limit. 10 is used to
718+
// provide some wiggle room for cases where the surrounding
719+
// scaffolding syntax adds some noise to the depth that changes
720+
// on a per testcase basis.
721+
n -= 10
722+
}
723+
724+
pre, mid, post := split(tt.format)
725+
if strings.Contains(mid, "«") {
726+
left, base, right := split(mid)
727+
mid = strings.Repeat(left, n) + base + strings.Repeat(right, n)
728+
} else {
729+
mid = strings.Repeat(mid, n)
730+
}
731+
input := pre + mid + post
732+
733+
fset := token.NewFileSet()
734+
_, err := ParseFile(fset, "", input, DeclarationErrors)
735+
if size == "small" {
736+
if err != nil {
737+
t.Errorf("ParseFile(...): %v (want success)", err)
738+
}
739+
} else {
740+
expected := "exceeded max scope depth during object resolution"
741+
if err == nil || !strings.HasSuffix(err.Error(), expected) {
742+
t.Errorf("ParseFile(...) = _, %v, want %q", err, expected)
743+
}
744+
}
745+
})
746+
}
747+
}
748+
}

src/go/parser/resolver.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ func resolveFile(file *ast.File, handle *token.File, declErr func(token.Pos, str
5454
file.Unresolved = r.unresolved[0:i]
5555
}
5656

57+
const maxScopeDepth int = 1e3
58+
5759
type resolver struct {
5860
handle *token.File
5961
declErr func(token.Pos, string)
@@ -85,16 +87,19 @@ func (r *resolver) sprintf(format string, args ...any) string {
8587
}
8688

8789
func (r *resolver) openScope(pos token.Pos) {
90+
r.depth++
91+
if r.depth > maxScopeDepth {
92+
panic(bailout{pos: pos, msg: "exceeded max scope depth during object resolution"})
93+
}
8894
if debugResolve {
8995
r.trace("opening scope @%v", pos)
90-
r.depth++
9196
}
9297
r.topScope = ast.NewScope(r.topScope)
9398
}
9499

95100
func (r *resolver) closeScope() {
101+
r.depth--
96102
if debugResolve {
97-
r.depth--
98103
r.trace("closing scope")
99104
}
100105
r.topScope = r.topScope.Outer

0 commit comments

Comments
 (0)