Skip to content

Commit 72c3df8

Browse files
adonovangopherbot
authored andcommitted
[gopls-release-branch.0.10] gopls/internal/hooks: fixes to diff disaster logic
Previously, the disaster logic in the new diff implementation would "encrypt" the before/after files using a monoalphabetic substitution, which has been insecure since the 9th century. Instead, save plain text, in file with mode 0600, and invite the user to audit the file before sharing it with us. Also, separate the two files using a NUL byte, not a newline, which is highly ambiguous. Also, in the JSON diff stats writer: - print a warning if we can't create the log file. (The previous code was subtle--it stored a nil *os.File in an io.Writer, which caused Writes to fail with an error, in effect, silently.) - Don't hold the mutex around the write operation. - Fix minor off-by-one error (re: 15) - Crash if JSON encoding fails; it "can't happen". Change-Id: I9b6a4145451afd77594f0ef9868143634a9c4561 Reviewed-on: https://go-review.googlesource.com/c/tools/+/445580 Run-TryBot: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]> (cherry picked from commit 2af106e) Reviewed-on: https://go-review.googlesource.com/c/tools/+/445816 Auto-Submit: Alan Donovan <[email protected]>
1 parent 2c50d0c commit 72c3df8

File tree

2 files changed

+54
-116
lines changed

2 files changed

+54
-116
lines changed

gopls/internal/hooks/diff.go

Lines changed: 47 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,15 @@
55
package hooks
66

77
import (
8-
"crypto/rand"
98
"encoding/json"
109
"fmt"
11-
"io"
10+
"io/ioutil"
1211
"log"
13-
"math/big"
1412
"os"
1513
"path/filepath"
1614
"runtime"
17-
"strings"
1815
"sync"
1916
"time"
20-
"unicode"
2117

2218
"github.com/sergi/go-diff/diffmatchpatch"
2319
"golang.org/x/tools/internal/bug"
@@ -36,116 +32,82 @@ type diffstat struct {
3632
}
3733

3834
var (
39-
mu sync.Mutex // serializes writes and protects ignored
40-
difffd io.Writer
41-
ignored int // lots of the diff calls have 0 diffs
42-
)
35+
ignoredMu sync.Mutex
36+
ignored int // counter of diff requests on equal strings
4337

44-
var fileonce sync.Once
38+
diffStatsOnce sync.Once
39+
diffStats *os.File // never closed
40+
)
4541

42+
// save writes a JSON record of statistics about diff requests to a temporary file.
4643
func (s *diffstat) save() {
47-
// save log records in a file in os.TempDir().
48-
// diff is frequently called with identical strings, so
49-
// these are somewhat compressed out
50-
fileonce.Do(func() {
51-
fname := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-diff-%x", os.Getpid()))
52-
fd, err := os.Create(fname)
44+
diffStatsOnce.Do(func() {
45+
f, err := ioutil.TempFile("", "gopls-diff-stats-*")
5346
if err != nil {
54-
// now what?
47+
log.Printf("can't create diff stats temp file: %v", err) // e.g. disk full
48+
return
5549
}
56-
difffd = fd
50+
diffStats = f
5751
})
52+
if diffStats == nil {
53+
return
54+
}
5855

59-
mu.Lock()
60-
defer mu.Unlock()
56+
// diff is frequently called with equal strings,
57+
// so we count repeated instances but only print every 15th.
58+
ignoredMu.Lock()
6159
if s.Oldedits == 0 && s.Newedits == 0 {
60+
ignored++
6261
if ignored < 15 {
63-
// keep track of repeated instances of no diffs
64-
// but only print every 15th
65-
ignored++
62+
ignoredMu.Unlock()
6663
return
6764
}
68-
s.Ignored = ignored + 1
69-
} else {
70-
s.Ignored = ignored
7165
}
66+
s.Ignored = ignored
7267
ignored = 0
73-
// it would be really nice to see why diff was called
74-
_, f, l, ok := runtime.Caller(2)
75-
if ok {
76-
var fname string
77-
fname = filepath.Base(f) // diff is only called from a few places
78-
s.Stack = fmt.Sprintf("%s:%d", fname, l)
68+
ignoredMu.Unlock()
69+
70+
// Record the name of the file in which diff was called.
71+
// There aren't many calls, so only the base name is needed.
72+
if _, file, line, ok := runtime.Caller(2); ok {
73+
s.Stack = fmt.Sprintf("%s:%d", filepath.Base(file), line)
7974
}
8075
x, err := json.Marshal(s)
8176
if err != nil {
82-
log.Print(err) // failure to print statistics should not stop gopls
77+
log.Fatalf("internal error marshalling JSON: %v", err)
8378
}
84-
fmt.Fprintf(difffd, "%s\n", x)
79+
fmt.Fprintf(diffStats, "%s\n", x)
8580
}
8681

87-
// save encrypted versions of the broken input and return the file name
88-
// (the saved strings will have the same diff behavior as the user's strings)
82+
// disaster is called when the diff algorithm panics or produces a
83+
// diff that cannot be applied. It saves the broken input in a
84+
// new temporary file and logs the file name, which is returned.
8985
func disaster(before, after string) string {
90-
// encrypt before and after for privacy. (randomized monoalphabetic cipher)
91-
// got will contain the substitution cipher
92-
// for the runes in before and after
93-
got := map[rune]rune{}
94-
for _, r := range before {
95-
got[r] = ' ' // value doesn't matter
96-
}
97-
for _, r := range after {
98-
got[r] = ' '
99-
}
100-
repl := initrepl(len(got))
101-
i := 0
102-
for k := range got { // randomized
103-
got[k] = repl[i]
104-
i++
105-
}
106-
// use got to encrypt before and after
107-
subst := func(r rune) rune { return got[r] }
108-
first := strings.Map(subst, before)
109-
second := strings.Map(subst, after)
110-
111-
// one failure per session is enough, and more private.
112-
// this saves the last one.
113-
fname := fmt.Sprintf("%s/gopls-failed-%x", os.TempDir(), os.Getpid())
114-
fd, err := os.Create(fname)
115-
defer fd.Close()
116-
_, err = fmt.Fprintf(fd, "%s\n%s\n", first, second)
117-
if err != nil {
118-
// what do we tell the user?
86+
// We use the pid to salt the name, not os.TempFile,
87+
// so that each process creates at most one file.
88+
// One is sufficient for a bug report.
89+
filename := fmt.Sprintf("%s/gopls-diff-bug-%x", os.TempDir(), os.Getpid())
90+
91+
// We use NUL as a separator: it should never appear in Go source.
92+
data := before + "\x00" + after
93+
94+
if err := ioutil.WriteFile(filename, []byte(data), 0600); err != nil {
95+
log.Printf("failed to write diff bug report: %v", err)
11996
return ""
12097
}
121-
// ask the user to send us the file, somehow
122-
return fname
123-
}
12498

125-
func initrepl(n int) []rune {
126-
repl := make([]rune, 0, n)
127-
for r := rune(0); len(repl) < n; r++ {
128-
if unicode.IsLetter(r) || unicode.IsNumber(r) {
129-
repl = append(repl, r)
130-
}
131-
}
132-
// randomize repl
133-
rdr := rand.Reader
134-
lim := big.NewInt(int64(len(repl)))
135-
for i := 1; i < n; i++ {
136-
v, _ := rand.Int(rdr, lim)
137-
k := v.Int64()
138-
repl[i], repl[k] = repl[k], repl[i]
139-
}
140-
return repl
99+
// TODO(adonovan): is there a better way to surface this?
100+
log.Printf("Bug detected in diff algorithm! Please send file %s to the maintainers of gopls if you are comfortable sharing its contents.", filename)
101+
102+
return filename
141103
}
142104

143105
// BothDiffs edits calls both the new and old diffs, checks that the new diffs
144106
// change before into after, and attempts to preserve some statistics.
145107
func BothDiffs(before, after string) (edits []diff.Edit) {
146108
// The new diff code contains a lot of internal checks that panic when they
147109
// fail. This code catches the panics, or other failures, tries to save
148-
// the failing example (and ut wiykd ask the user to send it back to us, and
110+
// the failing example (and it would ask the user to send it back to us, and
149111
// changes options.newDiff to 'old', if only we could figure out how.)
150112
stat := diffstat{Before: len(before), After: len(after)}
151113
now := time.Now()

gopls/internal/hooks/diff_test.go

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
package hooks
66

77
import (
8-
"fmt"
98
"io/ioutil"
109
"os"
1110
"testing"
12-
"unicode/utf8"
1311

1412
"golang.org/x/tools/internal/diff/difftest"
1513
)
@@ -18,40 +16,18 @@ func TestDiff(t *testing.T) {
1816
difftest.DiffTest(t, ComputeEdits)
1917
}
2018

21-
func TestRepl(t *testing.T) {
22-
t.Skip("just for checking repl by looking at it")
23-
repl := initrepl(800)
24-
t.Errorf("%q", string(repl))
25-
t.Errorf("%d", len(repl))
26-
}
27-
2819
func TestDisaster(t *testing.T) {
29-
a := "This is a string,(\u0995) just for basic functionality"
30-
b := "Ths is another string, (\u0996) to see if disaster will store stuff correctly"
20+
a := "This is a string,(\u0995) just for basic\nfunctionality"
21+
b := "This is another string, (\u0996) to see if disaster will store stuff correctly"
3122
fname := disaster(a, b)
3223
buf, err := ioutil.ReadFile(fname)
3324
if err != nil {
34-
t.Errorf("error %v reading %s", err, fname)
35-
}
36-
var x, y string
37-
n, err := fmt.Sscanf(string(buf), "%s\n%s\n", &x, &y)
38-
if n != 2 {
39-
t.Errorf("got %d, expected 2", n)
40-
t.Logf("read %q", string(buf))
41-
}
42-
if a == x || b == y {
43-
t.Error("failed to encrypt")
44-
}
45-
err = os.Remove(fname)
46-
if err != nil {
47-
t.Errorf("%v removing %s", err, fname)
25+
t.Fatal(err)
4826
}
49-
alen, blen := utf8.RuneCount([]byte(a)), utf8.RuneCount([]byte(b))
50-
xlen, ylen := utf8.RuneCount([]byte(x)), utf8.RuneCount([]byte(y))
51-
if alen != xlen {
52-
t.Errorf("a; got %d, expected %d", xlen, alen)
27+
if string(buf) != a+"\x00"+b {
28+
t.Error("failed to record original strings")
5329
}
54-
if blen != ylen {
55-
t.Errorf("b: got %d expected %d", ylen, blen)
30+
if err := os.Remove(fname); err != nil {
31+
t.Error(err)
5632
}
5733
}

0 commit comments

Comments
 (0)