Skip to content

Commit 07a58bc

Browse files
committed
gopls/internal/golang: refine crash golang/go#70553
This CL adds defensive assertions to ExtractToNewFile, which experiences a crash due to out-of-bounds indexing. The logic looks sound, but clearly something is off. This CL also fixes a mistake in the logic added to parsego.Parse to work around golang/go#70162 (missing File.FileStart in go/parser), but I don't think that bug explains golang/go#70553 because it is concerned only with deeply broken files. Updates golang/go#70553 Updates golang/go#70162 Change-Id: Ie4f6fbbde2046023d7a3b865e5810bbed3be3118 Reviewed-on: https://go-review.googlesource.com/c/tools/+/631675 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent c622026 commit 07a58bc

File tree

3 files changed

+65
-27
lines changed

3 files changed

+65
-27
lines changed

Diff for: gopls/internal/cache/parsego/file.go

+17
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"sync"
1313

1414
"golang.org/x/tools/gopls/internal/protocol"
15+
"golang.org/x/tools/gopls/internal/util/bug"
1516
"golang.org/x/tools/gopls/internal/util/safetoken"
1617
)
1718

@@ -116,6 +117,22 @@ func (pgf *File) RangePos(r protocol.Range) (token.Pos, token.Pos, error) {
116117
return pgf.Tok.Pos(start), pgf.Tok.Pos(end), nil
117118
}
118119

120+
// CheckNode asserts that the Node's positions are valid w.r.t. pgf.Tok.
121+
func (pgf *File) CheckNode(node ast.Node) {
122+
// Avoid safetoken.Offsets, and put each assertion on its own source line.
123+
pgf.CheckPos(node.Pos())
124+
pgf.CheckPos(node.End())
125+
}
126+
127+
// CheckPos asserts that the position is valid w.r.t. pgf.Tok.
128+
func (pgf *File) CheckPos(pos token.Pos) {
129+
if !pos.IsValid() {
130+
bug.Report("invalid token.Pos")
131+
} else if _, err := safetoken.Offset(pgf.Tok, pos); err != nil {
132+
bug.Report("token.Pos out of range")
133+
}
134+
}
135+
119136
// Resolve lazily resolves ast.Ident.Objects in the enclosed syntax tree.
120137
//
121138
// Resolve must be called before accessing any of:

Diff for: gopls/internal/cache/parsego/parse.go

+14-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"golang.org/x/tools/gopls/internal/label"
2626
"golang.org/x/tools/gopls/internal/protocol"
2727
"golang.org/x/tools/gopls/internal/util/astutil"
28+
"golang.org/x/tools/gopls/internal/util/bug"
2829
"golang.org/x/tools/gopls/internal/util/safetoken"
2930
"golang.org/x/tools/internal/diff"
3031
"golang.org/x/tools/internal/event"
@@ -77,12 +78,21 @@ func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, s
7778
tokenFile := func(file *ast.File) *token.File {
7879
tok := fset.File(file.FileStart)
7980
if tok == nil {
81+
// Invalid File.FileStart (also File.{Package,Name.Pos}).
82+
if file.Package.IsValid() {
83+
bug.Report("ast.File has valid Package but no FileStart")
84+
}
85+
if file.Name.Pos().IsValid() {
86+
bug.Report("ast.File has valid Name.Pos but no FileStart")
87+
}
8088
tok = fset.AddFile(uri.Path(), -1, len(src))
8189
tok.SetLinesForContent(src)
82-
if file.FileStart.IsValid() {
83-
file.FileStart = token.Pos(tok.Base())
84-
file.FileEnd = token.Pos(tok.Base() + tok.Size())
85-
}
90+
// If the File contained any valid token.Pos values,
91+
// they would all be invalid wrt the new token.File,
92+
// but we have established that it lacks FileStart,
93+
// Package, and Name.Pos.
94+
file.FileStart = token.Pos(tok.Base())
95+
file.FileEnd = token.Pos(tok.Base() + tok.Size())
8696
}
8797
return tok
8898
}

Diff for: gopls/internal/golang/extracttofile.go

+34-23
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,19 @@ func ExtractToNewFile(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
9797
if !ok {
9898
return nil, bug.Errorf("invalid selection")
9999
}
100+
pgf.CheckPos(start) // #70553
101+
// Inv: start is valid wrt pgf.Tok.
100102

101103
// select trailing empty lines
102104
offset, err := safetoken.Offset(pgf.Tok, end)
103105
if err != nil {
104106
return nil, err
105107
}
106108
rest := pgf.Src[offset:]
107-
end += token.Pos(len(rest) - len(bytes.TrimLeft(rest, " \t\n")))
109+
spaces := len(rest) - len(bytes.TrimLeft(rest, " \t\n"))
110+
end += token.Pos(spaces)
111+
pgf.CheckPos(end) // #70553
112+
// Inv: end is valid wrt pgf.Tok.
108113

109114
replaceRange, err := pgf.PosRange(start, end)
110115
if err != nil {
@@ -172,6 +177,7 @@ func ExtractToNewFile(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
172177
}
173178

174179
fileStart := pgf.File.FileStart
180+
pgf.CheckPos(fileStart) // #70553
175181
buf.Write(pgf.Src[start-fileStart : end-fileStart])
176182

177183
newFileContent, err := format.Source(buf.Bytes())
@@ -221,31 +227,42 @@ func selectedToplevelDecls(pgf *parsego.File, start, end token.Pos) (token.Pos,
221227
firstName := ""
222228
for _, decl := range pgf.File.Decls {
223229
if posRangeIntersects(start, end, decl.Pos(), decl.End()) {
224-
var id *ast.Ident
225-
switch v := decl.(type) {
230+
var (
231+
comment *ast.CommentGroup // (include comment preceding decl)
232+
id *ast.Ident
233+
)
234+
switch decl := decl.(type) {
226235
case *ast.BadDecl:
227236
return 0, 0, "", false
237+
228238
case *ast.FuncDecl:
229239
// if only selecting keyword "func" or function name, extend selection to the
230240
// whole function
231-
if posRangeContains(v.Pos(), v.Name.End(), start, end) {
232-
start, end = v.Pos(), v.End()
241+
if posRangeContains(decl.Pos(), decl.Name.End(), start, end) {
242+
pgf.CheckNode(decl) // #70553
243+
start, end = decl.Pos(), decl.End()
244+
// Inv: start, end are valid wrt pgf.Tok.
233245
}
234-
id = v.Name
246+
comment = decl.Doc
247+
id = decl.Name
248+
235249
case *ast.GenDecl:
236250
// selection cannot intersect an import declaration
237-
if v.Tok == token.IMPORT {
251+
if decl.Tok == token.IMPORT {
238252
return 0, 0, "", false
239253
}
240254
// if only selecting keyword "type", "const", or "var", extend selection to the
241255
// whole declaration
242-
if v.Tok == token.TYPE && posRangeContains(v.Pos(), v.Pos()+token.Pos(len("type")), start, end) ||
243-
v.Tok == token.CONST && posRangeContains(v.Pos(), v.Pos()+token.Pos(len("const")), start, end) ||
244-
v.Tok == token.VAR && posRangeContains(v.Pos(), v.Pos()+token.Pos(len("var")), start, end) {
245-
start, end = v.Pos(), v.End()
256+
if decl.Tok == token.TYPE && posRangeContains(decl.Pos(), decl.Pos()+token.Pos(len("type")), start, end) ||
257+
decl.Tok == token.CONST && posRangeContains(decl.Pos(), decl.Pos()+token.Pos(len("const")), start, end) ||
258+
decl.Tok == token.VAR && posRangeContains(decl.Pos(), decl.Pos()+token.Pos(len("var")), start, end) {
259+
pgf.CheckNode(decl) // #70553
260+
start, end = decl.Pos(), decl.End()
261+
// Inv: start, end are valid wrt pgf.Tok.
246262
}
247-
if len(v.Specs) > 0 {
248-
switch spec := v.Specs[0].(type) {
263+
comment = decl.Doc
264+
if len(decl.Specs) > 0 {
265+
switch spec := decl.Specs[0].(type) {
249266
case *ast.TypeSpec:
250267
id = spec.Name
251268
case *ast.ValueSpec:
@@ -261,16 +278,10 @@ func selectedToplevelDecls(pgf *parsego.File, start, end token.Pos) (token.Pos,
261278
// may be "_"
262279
firstName = id.Name
263280
}
264-
// extends selection to docs comments
265-
var c *ast.CommentGroup
266-
switch decl := decl.(type) {
267-
case *ast.GenDecl:
268-
c = decl.Doc
269-
case *ast.FuncDecl:
270-
c = decl.Doc
271-
}
272-
if c != nil && c.Pos() < start {
273-
start = c.Pos()
281+
if comment != nil && comment.Pos() < start {
282+
pgf.CheckNode(comment) // #70553
283+
start = comment.Pos()
284+
// Inv: start is valid wrt pgf.Tok.
274285
}
275286
}
276287
}

0 commit comments

Comments
 (0)