Skip to content

Commit 302a400

Browse files
author
Jay Conrod
committed
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 #44749 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]>
1 parent 2e794c2 commit 302a400

File tree

4 files changed

+125
-31
lines changed

4 files changed

+125
-31
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func DownloadDir(m module.Version) (string, error) {
8080
return "", err
8181
}
8282

83+
// Check whether the directory itself exists.
8384
dir := filepath.Join(cfg.GOMODCACHE, enc+"@"+encVer)
8485
if fi, err := os.Stat(dir); os.IsNotExist(err) {
8586
return dir, err
@@ -88,6 +89,9 @@ func DownloadDir(m module.Version) (string, error) {
8889
} else if !fi.IsDir() {
8990
return dir, &DownloadDirPartialError{dir, errors.New("not a directory")}
9091
}
92+
93+
// Check if a .partial file exists. This is created at the beginning of
94+
// a download and removed after the zip is extracted.
9195
partialPath, err := CachePath(m, "partial")
9296
if err != nil {
9397
return dir, err
@@ -97,6 +101,19 @@ func DownloadDir(m module.Version) (string, error) {
97101
} else if !os.IsNotExist(err) {
98102
return dir, err
99103
}
104+
105+
// Check if a .ziphash file exists. It should be created before the
106+
// zip is extracted, but if it was deleted (by another program?), we need
107+
// to re-calculate it.
108+
ziphashPath, err := CachePath(m, "ziphash")
109+
if err != nil {
110+
return dir, err
111+
}
112+
if _, err := os.Stat(ziphashPath); os.IsNotExist(err) {
113+
return dir, &DownloadDirPartialError{dir, errors.New("ziphash file is missing")}
114+
} else if err != nil {
115+
return dir, err
116+
}
100117
return dir, nil
101118
}
102119

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

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,16 @@ func DownloadZip(ctx context.Context, mod module.Version) (zipfile string, err e
168168
if err != nil {
169169
return cached{"", err}
170170
}
171+
ziphashfile := zipfile + "hash"
171172

172-
// Skip locking if the zipfile already exists.
173+
// Return without locking if the zip and ziphash files exist.
173174
if _, err := os.Stat(zipfile); err == nil {
174-
return cached{zipfile, nil}
175+
if _, err := os.Stat(ziphashfile); err == nil {
176+
return cached{zipfile, nil}
177+
}
175178
}
176179

177-
// The zip file does not exist. Acquire the lock and create it.
180+
// The zip or ziphash file does not exist. Acquire the lock and create them.
178181
if cfg.CmdName != "mod download" {
179182
fmt.Fprintf(os.Stderr, "go: downloading %s %s\n", mod.Path, mod.Version)
180183
}
@@ -184,14 +187,6 @@ func DownloadZip(ctx context.Context, mod module.Version) (zipfile string, err e
184187
}
185188
defer unlock()
186189

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

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

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

289309
// Hash the zip file and check the sum before renaming to the final location.
290-
hash, err := dirhash.HashZip(f.Name(), dirhash.DefaultHash)
291-
if err != nil {
292-
return err
293-
}
294-
if err := checkModSum(mod, hash); err != nil {
295-
return err
296-
}
297-
298-
if err := renameio.WriteFile(zipfile+"hash", []byte(hash), 0666); err != nil {
310+
if err := hashZip(mod, f.Name(), ziphashfile); err != nil {
299311
return err
300312
}
301313
if err := os.Rename(f.Name(), zipfile); err != nil {
@@ -307,6 +319,22 @@ func downloadZip(ctx context.Context, mod module.Version, zipfile string) (err e
307319
return nil
308320
}
309321

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

451479
// checkMod checks the given module's checksum.
452480
func checkMod(mod module.Version) {
453-
if cfg.GOMODCACHE == "" {
454-
// Do not use current directory.
455-
return
456-
}
457-
458481
// Do the file I/O before acquiring the go.sum lock.
459482
ziphash, err := CachePath(mod, "ziphash")
460483
if err != nil {
461484
base.Fatalf("verifying %v", module.VersionError(mod, err))
462485
}
463486
data, err := renameio.ReadFile(ziphash)
464487
if err != nil {
465-
if errors.Is(err, fs.ErrNotExist) {
466-
// This can happen if someone does rm -rf GOPATH/src/cache/download. So it goes.
467-
return
468-
}
469488
base.Fatalf("verifying %v", module.VersionError(mod, err))
470489
}
471490
h := strings.TrimSpace(string(data))
Lines changed: 55 additions & 0 deletions
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

Lines changed: 5 additions & 2 deletions
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)