carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 7b70b316dd070f4c31a45c94320f46d743e13767 parent f9ffec4
2 files changed, +41 −3
  • internal/console/seedscratch.go +6 −1
  • internal/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) {