console: seed scratch narrows its directory to 0700 and refuses zero bytes when unavailable (Plan 5 Task 3 review)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+41
−3
internal/console/seedscratch.go+6 −1internal/console/seedscratch_test.go+35 −2
CI passed — run details
diff --git a/internal/console/seedscratch.go b/internal/console/seedscratch.go| index eea255b8b..a1d4c33a4 100644 |
| --- a/internal/console/seedscratch.go |
| +++ b/internal/console/seedscratch.go |
| @@ -67,7 +67,7 @@ func (p *seedScratch) dir() string { return p.root } |
| func (p *seedScratch) readCapacity() { |
| n, err := p.free(p.root) |
| if err != nil { |
| - p.capacity = 0 |
| + p.capacity = -1 // unavailable: below zero, so even a zero-byte reservation is refused |
| return |
| } |
| p.capacity = max(n-seedScratchSlack, 0) |
| @@ -122,6 +122,11 @@ func clearSeedScratch(dir string) error { |
| if err := os.MkdirAll(dir, 0o700); err != nil { |
| return err |
| } |
| + // The directory holds copies of customer databases: narrow one that |
| + // already existed with wider permissions. |
| + if err := os.Chmod(dir, 0o700); err != nil { |
| + return err |
| + } |
| ents, err := os.ReadDir(dir) |
| if err != nil { |
| return err |
diff --git a/internal/console/seedscratch_test.go b/internal/console/seedscratch_test.go| index d134e7fc6..57360b813 100644 |
| --- a/internal/console/seedscratch_test.go |
| +++ b/internal/console/seedscratch_test.go |
| @@ -50,16 +50,21 @@ func TestScratchCapacityIsFreeSpaceLessAGiB(t *testing.T) { |
| if ref != nil { |
| t.Fatalf("at capacity: %+v", ref) |
| } |
| + heldReads := reads |
| if _, ref := p.reserve("a/y", 1); ref == nil || ref.Status != http.StatusInsufficientStorage { |
| t.Fatalf("a full pool: %+v, want 507", ref) |
| } |
| + if reads != heldReads { |
| + t.Fatalf("free space re-read while a reservation was held (%d reads, was %d)", reads, heldReads) |
| + } |
| rel() |
| free = 2 << 30 // the disk filled from elsewhere; an empty pool re-reads it |
| + emptyReads := reads |
| if _, ref := p.reserve("a/x", 2<<30); ref == nil || ref.Status != http.StatusInsufficientStorage { |
| t.Fatalf("after a re-read: %+v, want 507", ref) |
| } |
| - if reads < 2 { |
| - t.Fatalf("free space read %d times, want a re-read when the pool emptied", reads) |
| + if reads != emptyReads+1 { |
| + t.Fatalf("free space read %d times after the pool emptied, want exactly one more than %d", reads, emptyReads) |
| } |
| } |
| @@ -83,6 +88,34 @@ func TestSeedScratchWithoutADirectoryIsUnavailable(t *testing.T) { |
| if _, ref := p.reserve("acct/shop", 1); ref == nil || ref.Status != http.StatusInsufficientStorage { |
| t.Fatalf("no directory: %+v, want 507", ref) |
| } |
| + // Not even a zero-byte reservation: a caller would go on to spool into dir(). |
| + if _, ref := p.reserve("acct/shop", 0); ref == nil || ref.Status != http.StatusInsufficientStorage { |
| + t.Fatalf("no directory, zero bytes: %+v, want 507", ref) |
| + } |
| +} |
| + |
| +func TestSeedScratchRefusedDirectoryRefusesZeroBytes(t *testing.T) { |
| + p := startSeedScratch("relative/dir") |
| + if _, ref := p.reserve("acct/shop", 0); ref == nil || ref.Status != http.StatusInsufficientStorage { |
| + t.Fatalf("refused directory, zero bytes: %+v, want 507", ref) |
| + } |
| +} |
| + |
| +func TestSeedScratchClearNarrowsAnExistingDirectory(t *testing.T) { |
| + dir := filepath.Join(t.TempDir(), "seeds") |
| + if err := os.Mkdir(dir, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Chmod(dir, 0o755); err != nil { // whatever the umask was |
| + t.Fatal(err) |
| + } |
| + if err := clearSeedScratch(dir); err != nil { |
| + t.Fatal(err) |
| + } |
| + st, err := os.Stat(dir) |
| + if err != nil || st.Mode().Perm() != 0o700 { |
| + t.Fatalf("mode %v, %v; want 0700", st.Mode().Perm(), err) |
| + } |
| } |
| func TestClearSeedScratch(t *testing.T) { |