Skip to content

Commit 45ef829

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/lsp/filecache: add integrity check
This change appends a 32-bit CRC checksum to the end of each cache file in Set, and checks it in Get, and dumps the file content if it doesn't match the content. This is intended to help investigate golang/go#59289. Updates golang/go#59289 Change-Id: Ia439d915c37d33b0af5d0da11a8db63762cd80a7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/482818 TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Run-TryBot: Alan Donovan <[email protected]>
1 parent 4bea115 commit 45ef829

File tree

1 file changed

+29
-7
lines changed

1 file changed

+29
-7
lines changed

gopls/internal/lsp/filecache/filecache.go

+29-7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"encoding/binary"
2727
"errors"
2828
"fmt"
29+
"hash/crc32"
2930
"io"
3031
"log"
3132
"os"
@@ -35,6 +36,7 @@ import (
3536
"sync/atomic"
3637
"time"
3738

39+
"golang.org/x/tools/internal/bug"
3840
"golang.org/x/tools/internal/lockedfile"
3941
)
4042

@@ -62,13 +64,20 @@ func Get(kind string, key [32]byte) ([]byte, error) {
6264

6365
// Verify that the Write was complete
6466
// by checking the recorded length.
65-
if len(data) < 8 {
67+
if len(data) < 8+4 {
6668
return nil, ErrNotFound // cache entry is incomplete
6769
}
68-
if length := binary.LittleEndian.Uint64(data); int(length) != len(data)-8 {
70+
length, value, checksum := data[:8], data[8:len(data)-4], data[len(data)-4:]
71+
if binary.LittleEndian.Uint64(length) != uint64(len(value)) {
6972
return nil, ErrNotFound // cache entry is incomplete (or too long!)
7073
}
71-
data = data[8:]
74+
75+
// Check for corruption and print the entire file content as
76+
// this may help us observe the pattern. See issue #59289.
77+
if binary.LittleEndian.Uint32(checksum) != crc32.ChecksumIEEE(value) {
78+
return nil, bug.Errorf("internal error in filecache.Get(%q, %x): invalid checksum at end of %d-byte file %s:\n%q",
79+
kind, key, len(data), name, data)
80+
}
7281

7382
// Update file time for use by LRU eviction.
7483
// (This turns every read into a write operation.
@@ -84,7 +93,7 @@ func Get(kind string, key [32]byte) ([]byte, error) {
8493
return nil, fmt.Errorf("failed to update access time: %w", err)
8594
}
8695

87-
return data, nil
96+
return value, nil
8897
}
8998

9099
// ErrNotFound is the distinguished error
@@ -104,15 +113,28 @@ func Set(kind string, key [32]byte, value []byte) error {
104113
// the expected length first and verify it in Get.
105114
var length [8]byte
106115
binary.LittleEndian.PutUint64(length[:], uint64(len(value)))
107-
header := bytes.NewReader(length[:])
108-
payload := bytes.NewReader(value)
116+
117+
// Occasional file corruption (presence of zero bytes in JSON
118+
// files) has been reported on macOS (see issue #59289),
119+
// assumed due to a nonatomicity problem in the file system.
120+
// Ideally the macOS kernel would be fixed, or lockedfile
121+
// would implement a workaround (since its job is to provide
122+
// reliable atomic file replacement atop kernels that don't),
123+
// but for now we add an extra integrity check: a 32-bit
124+
// checksum at the end.
125+
var checksum [4]byte
126+
binary.LittleEndian.PutUint32(checksum[:], crc32.ChecksumIEEE(value))
109127

110128
// Windows doesn't support atomic rename--we tried MoveFile,
111129
// MoveFileEx, ReplaceFileEx, and SetFileInformationByHandle
112130
// of RenameFileInfo, all to no avail--so instead we use
113131
// advisory file locking, which is only about 2x slower even
114132
// on POSIX platforms with atomic rename.
115-
return lockedfile.Write(name, io.MultiReader(header, payload), 0600)
133+
return lockedfile.Write(name, io.MultiReader(
134+
bytes.NewReader(length[:]),
135+
bytes.NewReader(value),
136+
bytes.NewReader(checksum[:])),
137+
0600)
116138
}
117139

118140
var budget int64 = 1e9 // 1GB

0 commit comments

Comments
 (0)