Skip to content

Commit 33fb479

Browse files
Jay Conrodtoothrot
Jay Conrod
authored andcommitted
[release-branch.go1.16] cmd/go/internal/modfetch: detect and recover from missing ziphash file
Previously, if an extracted module directory existed in the module cache, but the corresponding ziphash file did not, if the sum was missing from go.sum, we would not verify the sum. This caused 'go get' not to write missing sums. 'go build' in readonly mode (now the default) checks for missing sums and doesn't attempt to fetch modules that can't be verified against go.sum. With this change, when requesting the module directory with modfetch.DownloadDir, if the ziphash file is missing, the go command will re-hash the zip without downloading or re-extracting it again. Note that the go command creates the ziphash file before the module directory, but another program could remove it separately, and it might not be present after a crash. Fixes #44812 Change-Id: I64551e048a3ba17d069de1ec123d5b8b2757543c Reviewed-on: https://go-review.googlesource.com/c/go/+/298352 Trust: Jay Conrod <[email protected]> Run-TryBot: Jay Conrod <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]> (cherry picked from commit 302a400) Reviewed-on: https://go-review.googlesource.com/c/go/+/298851
1 parent 902d16e commit 33fb479

File tree

4 files changed

+125
-31
lines changed

4 files changed

+125
-31
lines changed

src/cmd/go/internal/modfetch/cache.go

+17
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func DownloadDir(m module.Version) (string, error) {
8484
return "", err
8585
}
8686

87+
// Check whether the directory itself exists.
8788
dir := filepath.Join(cfg.GOMODCACHE, enc+"@"+encVer)
8889
if fi, err := os.Stat(dir); os.IsNotExist(err) {
8990
return dir, err
@@ -92,6 +93,9 @@ func DownloadDir(m module.Version) (string, error) {
9293
} else if !fi.IsDir() {
9394
return dir, &DownloadDirPartialError{dir, errors.New("not a directory")}
9495
}
96+
97+
// Check if a .partial file exists. This is created at the beginning of
98+
// a download and removed after the zip is extracted.
9599
partialPath, err := CachePath(m, "partial")
96100
if err != nil {
97101
return dir, err
@@ -101,6 +105,19 @@ func DownloadDir(m module.Version) (string, error) {
101105
} else if !os.IsNotExist(err) {
102106
return dir, err
103107
}
108+
109+
// Check if a .ziphash file exists. It should be created before the
110+
// zip is extracted, but if it was deleted (by another program?), we need
111+
// to re-calculate it.
112+
ziphashPath, err := CachePath(m, "ziphash")
113+
if err != nil {
114+
return dir, err
115+
}
116+
if _, err := os.Stat(ziphashPath); os.IsNotExist(err) {
117+
return dir, &DownloadDirPartialError{dir, errors.New("ziphash file is missing")}
118+
} else if err != nil {
119+
return dir, err
120+
}
104121
return dir, nil
105122
}
106123

src/cmd/go/internal/modfetch/fetch.go

+48-29
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,16 @@ func DownloadZip(ctx context.Context, mod module.Version) (zipfile string, err e
170170
if err != nil {
171171
return cached{"", err}
172172
}
173+
ziphashfile := zipfile + "hash"
173174

174-
// Skip locking if the zipfile already exists.
175+
// Return without locking if the zip and ziphash files exist.
175176
if _, err := os.Stat(zipfile); err == nil {
176-
return cached{zipfile, nil}
177+
if _, err := os.Stat(ziphashfile); err == nil {
178+
return cached{zipfile, nil}
179+
}
177180
}
178181

179-
// The zip file does not exist. Acquire the lock and create it.
182+
// The zip or ziphash file does not exist. Acquire the lock and create them.
180183
if cfg.CmdName != "mod download" {
181184
fmt.Fprintf(os.Stderr, "go: downloading %s %s\n", mod.Path, mod.Version)
182185
}
@@ -186,14 +189,6 @@ func DownloadZip(ctx context.Context, mod module.Version) (zipfile string, err e
186189
}
187190
defer unlock()
188191

189-
// Double-check that the zipfile was not created while we were waiting for
190-
// the lock.
191-
if _, err := os.Stat(zipfile); err == nil {
192-
return cached{zipfile, nil}
193-
}
194-
if err := os.MkdirAll(filepath.Dir(zipfile), 0777); err != nil {
195-
return cached{"", err}
196-
}
197192
if err := downloadZip(ctx, mod, zipfile); err != nil {
198193
return cached{"", err}
199194
}
@@ -206,6 +201,25 @@ func downloadZip(ctx context.Context, mod module.Version, zipfile string) (err e
206201
ctx, span := trace.StartSpan(ctx, "modfetch.downloadZip "+zipfile)
207202
defer span.Done()
208203

204+
// Double-check that the zipfile was not created while we were waiting for
205+
// the lock in DownloadZip.
206+
ziphashfile := zipfile + "hash"
207+
var zipExists, ziphashExists bool
208+
if _, err := os.Stat(zipfile); err == nil {
209+
zipExists = true
210+
}
211+
if _, err := os.Stat(ziphashfile); err == nil {
212+
ziphashExists = true
213+
}
214+
if zipExists && ziphashExists {
215+
return nil
216+
}
217+
218+
// Create parent directories.
219+
if err := os.MkdirAll(filepath.Dir(zipfile), 0777); err != nil {
220+
return err
221+
}
222+
209223
// Clean up any remaining tempfiles from previous runs.
210224
// This is only safe to do because the lock file ensures that their
211225
// writers are no longer active.
@@ -217,6 +231,12 @@ func downloadZip(ctx context.Context, mod module.Version, zipfile string) (err e
217231
}
218232
}
219233

234+
// If the zip file exists, the ziphash file must have been deleted
235+
// or lost after a file system crash. Re-hash the zip without downloading.
236+
if zipExists {
237+
return hashZip(mod, zipfile, ziphashfile)
238+
}
239+
220240
// From here to the os.Rename call below is functionally almost equivalent to
221241
// renameio.WriteToFile, with one key difference: we want to validate the
222242
// contents of the file (by hashing it) before we commit it. Because the file
@@ -289,15 +309,7 @@ func downloadZip(ctx context.Context, mod module.Version, zipfile string) (err e
289309
}
290310

291311
// Hash the zip file and check the sum before renaming to the final location.
292-
hash, err := dirhash.HashZip(f.Name(), dirhash.DefaultHash)
293-
if err != nil {
294-
return err
295-
}
296-
if err := checkModSum(mod, hash); err != nil {
297-
return err
298-
}
299-
300-
if err := renameio.WriteFile(zipfile+"hash", []byte(hash), 0666); err != nil {
312+
if err := hashZip(mod, f.Name(), ziphashfile); err != nil {
301313
return err
302314
}
303315
if err := os.Rename(f.Name(), zipfile); err != nil {
@@ -309,6 +321,22 @@ func downloadZip(ctx context.Context, mod module.Version, zipfile string) (err e
309321
return nil
310322
}
311323

324+
// hashZip reads the zip file opened in f, then writes the hash to ziphashfile,
325+
// overwriting that file if it exists.
326+
//
327+
// If the hash does not match go.sum (or the sumdb if enabled), hashZip returns
328+
// an error and does not write ziphashfile.
329+
func hashZip(mod module.Version, zipfile, ziphashfile string) error {
330+
hash, err := dirhash.HashZip(zipfile, dirhash.DefaultHash)
331+
if err != nil {
332+
return err
333+
}
334+
if err := checkModSum(mod, hash); err != nil {
335+
return err
336+
}
337+
return renameio.WriteFile(ziphashfile, []byte(hash), 0666)
338+
}
339+
312340
// makeDirsReadOnly makes a best-effort attempt to remove write permissions for dir
313341
// and its transitive contents.
314342
func makeDirsReadOnly(dir string) {
@@ -452,22 +480,13 @@ func HaveSum(mod module.Version) bool {
452480

453481
// checkMod checks the given module's checksum.
454482
func checkMod(mod module.Version) {
455-
if cfg.GOMODCACHE == "" {
456-
// Do not use current directory.
457-
return
458-
}
459-
460483
// Do the file I/O before acquiring the go.sum lock.
461484
ziphash, err := CachePath(mod, "ziphash")
462485
if err != nil {
463486
base.Fatalf("verifying %v", module.VersionError(mod, err))
464487
}
465488
data, err := renameio.ReadFile(ziphash)
466489
if err != nil {
467-
if errors.Is(err, fs.ErrNotExist) {
468-
// This can happen if someone does rm -rf GOPATH/src/cache/download. So it goes.
469-
return
470-
}
471490
base.Fatalf("verifying %v", module.VersionError(mod, err))
472491
}
473492
h := strings.TrimSpace(string(data))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Test that if the module cache contains an extracted source directory but not
2+
# a ziphash, 'go build' complains about a missing sum, and 'go get' adds
3+
# the sum. Verifies #44749.
4+
5+
# With a tidy go.sum, go build succeeds. This also populates the module cache.
6+
cp go.sum.tidy go.sum
7+
go build -n use
8+
env GOPROXY=off
9+
env GOSUMDB=off
10+
11+
# Control case: if we delete the hash for rsc.io/quote v1.5.2,
12+
# 'go build' reports an error. 'go get' adds the sum.
13+
cp go.sum.bug go.sum
14+
! go build -n use
15+
stderr '^use.go:3:8: missing go.sum entry for module providing package rsc.io/quote \(imported by use\); to add:\n\tgo get use$'
16+
go get -d use
17+
cmp go.sum go.sum.tidy
18+
go build -n use
19+
20+
# If we delete the hash *and* the ziphash file, we should see the same behavior.
21+
cp go.sum.bug go.sum
22+
rm $WORK/gopath/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.ziphash
23+
! go build -n use
24+
stderr '^use.go:3:8: missing go.sum entry for module providing package rsc.io/quote \(imported by use\); to add:\n\tgo get use$'
25+
go get -d use
26+
cmp go.sum go.sum.tidy
27+
go build -n use
28+
29+
-- go.mod --
30+
module use
31+
32+
go 1.17
33+
34+
require rsc.io/quote v1.5.2
35+
-- go.sum.tidy --
36+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:pvCbr/wm8HzDD3fVywevekufpn6tCGPY3spdHeZJEsw=
37+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
38+
rsc.io/quote v1.5.2 h1:3fEykkD9k7lYzXqCYrwGAf7iNhbk4yCjHmKBN9td4L0=
39+
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
40+
rsc.io/sampler v1.3.0 h1:HLGR/BgEtI3r0uymSP/nl2uPLsUnNJX8toRyhfpBTII=
41+
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
42+
rsc.io/testonly v1.0.0 h1:K/VWHdO+Jv7woUXG0GzVNx1czBXUt3Ib1deaMn+xk64=
43+
rsc.io/testonly v1.0.0/go.mod h1:OqmGbIFOcF+XrFReLOGZ6BhMM7uMBiQwZsyNmh74SzY=
44+
-- go.sum.bug --
45+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:pvCbr/wm8HzDD3fVywevekufpn6tCGPY3spdHeZJEsw=
46+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
47+
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
48+
rsc.io/sampler v1.3.0 h1:HLGR/BgEtI3r0uymSP/nl2uPLsUnNJX8toRyhfpBTII=
49+
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
50+
rsc.io/testonly v1.0.0 h1:K/VWHdO+Jv7woUXG0GzVNx1czBXUt3Ib1deaMn+xk64=
51+
rsc.io/testonly v1.0.0/go.mod h1:OqmGbIFOcF+XrFReLOGZ6BhMM7uMBiQwZsyNmh74SzY=
52+
-- use.go --
53+
package use
54+
55+
import _ "rsc.io/quote"

src/cmd/go/testdata/script/mod_verify.txt

+5-2
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,13 @@ go mod tidy
4848
grep '^rsc.io/quote v1.1.0/go.mod ' go.sum
4949
grep '^rsc.io/quote v1.1.0 ' go.sum
5050

51-
# sync should ignore missing ziphash; verify should not
51+
# verify should fail on a missing ziphash. tidy should restore it.
5252
rm $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.1.0.ziphash
53-
go mod tidy
5453
! go mod verify
54+
stderr '^rsc.io/quote v1.1.0: missing ziphash: open '$GOPATH'[/\\]pkg[/\\]mod[/\\]cache[/\\]download[/\\]rsc.io[/\\]quote[/\\]@v[/\\]v1.1.0.ziphash'
55+
go mod tidy
56+
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.1.0.ziphash
57+
go mod verify
5558

5659
# Packages below module root should not be mentioned in go.sum.
5760
rm go.sum

0 commit comments

Comments
 (0)