mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-11-27 23:11:20 +08:00
parse: Fix a O(2^n) code path when input is "(" repeated n times.
The parsing function for Form first tries to parse a temp assignment, and backtracks when an assignment cannot be parsed. When the input is "(" repeated n times, each level of Form parsing will first try to parse the rest of the code as an assignment and then backtrack, without making any progress. This results in a call tree with a branching factor of 2, hence the O(2^n) complexity. The fix is to first try to parse a head instead, and only try to parse it as a temp assignment if it does contain "=". This fixes this particular pathological case, although I'm not 100% sure it eliminates all possibilities of O(2^n) time complexity. With the introduction of the "tmp" special command, the current syntax for temporary assignments will be deprecated and eventually go away, which will eliminate all backtracking in the parser. In the meanwhile, this fix may be good enough. This case was discovered with fuzzing support in Go 1.18. Also add the fuzzing test data.
This commit is contained in:
parent
1c1227324e
commit
9cda3f643e
17
pkg/parse/fuzz_test.go
Normal file
17
pkg/parse/fuzz_test.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func FuzzParse(f *testing.F) {
|
||||
f.Add("echo")
|
||||
f.Add("put $x")
|
||||
f.Add("put foo bar | each {|x| echo $x }")
|
||||
f.Fuzz(func(t *testing.T, code string) {
|
||||
Parse(Source{Name: "fuzz", Code: code}, Config{})
|
||||
})
|
||||
}
|
|
@ -14,6 +14,7 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"src.elv.sh/pkg/diag"
|
||||
|
@ -156,12 +157,29 @@ type Form struct {
|
|||
|
||||
func (fn *Form) parse(ps *parser) {
|
||||
parseSpaces(fn, ps)
|
||||
for fn.tryAssignment(ps) {
|
||||
for startsCompound(ps.peek(), CmdExpr) {
|
||||
initial := ps.save()
|
||||
parsedCmd := ps.parse(&Compound{ExprCtx: CmdExpr})
|
||||
|
||||
if s := SourceText(parsedCmd.n); strings.ContainsRune(s, '=') {
|
||||
postCmd := ps.save()
|
||||
ps.restore(initial)
|
||||
parsedAssignment := ps.parse(&Assignment{})
|
||||
if len(ps.errors.Entries) == len(initial.errors.Entries) {
|
||||
parsedAssignment.addTo(&fn.Assignments, fn)
|
||||
parseSpaces(fn, ps)
|
||||
continue
|
||||
} else {
|
||||
ps.restore(postCmd)
|
||||
}
|
||||
}
|
||||
|
||||
parsedCmd.addAs(&fn.Head, fn)
|
||||
parseSpaces(fn, ps)
|
||||
break
|
||||
}
|
||||
|
||||
// Parse head.
|
||||
if !startsCompound(ps.peek(), CmdExpr) {
|
||||
if fn.Head == nil {
|
||||
if len(fn.Assignments) > 0 {
|
||||
// Assignment-only form.
|
||||
return
|
||||
|
@ -169,8 +187,6 @@ func (fn *Form) parse(ps *parser) {
|
|||
// Bad form.
|
||||
ps.error(fmt.Errorf("bad rune at form head: %q", ps.peek()))
|
||||
}
|
||||
ps.parse(&Compound{ExprCtx: CmdExpr}).addAs(&fn.Head, fn)
|
||||
parseSpaces(fn, ps)
|
||||
|
||||
for {
|
||||
r := ps.peek()
|
||||
|
@ -202,27 +218,6 @@ func (fn *Form) parse(ps *parser) {
|
|||
}
|
||||
}
|
||||
|
||||
// tryAssignment tries to parse an assignment. If succeeded, it adds the parsed
|
||||
// assignment to fn.Assignments and returns true. Otherwise it rewinds the
|
||||
// parser and returns false.
|
||||
func (fn *Form) tryAssignment(ps *parser) bool {
|
||||
if !startsIndexing(ps.peek(), LHSExpr) {
|
||||
return false
|
||||
}
|
||||
|
||||
pos := ps.pos
|
||||
errorEntries := ps.errors.Entries
|
||||
parsedAssignment := ps.parse(&Assignment{})
|
||||
// If errors were added, revert
|
||||
if len(ps.errors.Entries) > len(errorEntries) {
|
||||
ps.errors.Entries = errorEntries
|
||||
ps.pos = pos
|
||||
return false
|
||||
}
|
||||
parsedAssignment.addTo(&fn.Assignments, fn)
|
||||
return true
|
||||
}
|
||||
|
||||
func startsForm(r rune) bool {
|
||||
return IsInlineWhitespace(r) || startsCompound(r, CmdExpr)
|
||||
}
|
||||
|
|
|
@ -33,6 +33,20 @@ func (ps *parser) parse(n Node) parsed {
|
|||
return parsed{n}
|
||||
}
|
||||
|
||||
type parserState struct {
|
||||
pos int
|
||||
overEOF int
|
||||
errors Error
|
||||
}
|
||||
|
||||
func (ps *parser) save() parserState {
|
||||
return parserState{ps.pos, ps.overEOF, ps.errors}
|
||||
}
|
||||
|
||||
func (ps *parser) restore(s parserState) {
|
||||
ps.pos, ps.overEOF, ps.errors = s.pos, s.overEOF, s.errors
|
||||
}
|
||||
|
||||
var nodeType = reflect.TypeOf((*Node)(nil)).Elem()
|
||||
|
||||
type parsed struct {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
go test fuzz v1
|
||||
string("(((((((((((((((((((((((((((((((($'")
|
Loading…
Reference in New Issue
Block a user