Give the local index and its export copy an explicit 0600 mode (closes #168)
check / check (pull_request) Successful in 2m41s

The local index lists every backed-up path and chunk hash, but its file
mode was left to the SQLite driver and the umask, so under a typical 022
umask a fresh index (and its -wal/-shm side files) landed world-readable.
The snapshot export copied the index to snapshot.db with a permissive
create as well.

provideDatabase now calls ensureIndexFileMode before opening the driver:
it creates the index 0600 if missing and chmods an existing one to 0600.
Doing this before the driver opens the file matters because SQLite gives
its -wal and -shm files the mode of the main database file. The export
copy is now created 0600 instead of via the umask-dependent default.

Tests under umask 022 cover a fresh index, an existing 0644 index in a
0755 directory, and the export copy, asserting each ends up 0600.

Model: opus-4-8
This commit is contained in:
2026-09-22 11:00:16 +00:00
parent a6434de57f
commit e4874a5522
4 changed files with 204 additions and 2 deletions
+49
View File
@@ -0,0 +1,49 @@
//nolint:testpackage // exercises the unexported copyFile helper
package snapshot
import (
"os"
"path/filepath"
"syscall"
"testing"
"github.com/spf13/afero"
)
// TestCopyFileExportCopyMode verifies that the exported snapshot database
// copy is created owner-only (0600), even under a lenient 022 umask that
// would otherwise leave a fresh file world-readable.
//
//nolint:paralleltest // syscall.Umask is process-global; parallel tests would clash
func TestCopyFileExportCopyMode(t *testing.T) {
restore := syscall.Umask(0o022)
defer syscall.Umask(restore)
dir := t.TempDir()
src := filepath.Join(dir, "index.sqlite")
err := os.WriteFile(src, []byte("index data"), 0o600)
if err != nil {
t.Fatalf("creating source index: %v", err)
}
dst := filepath.Join(dir, "snapshot.db")
sm := &SnapshotManager{fs: afero.NewOsFs()}
err = sm.copyFile(src, dst)
if err != nil {
t.Fatalf("copyFile: %v", err)
}
info, err := os.Stat(dst)
if err != nil {
t.Fatalf("stat export copy: %v", err)
}
got := info.Mode().Perm()
if got != 0o600 {
t.Fatalf("export copy permissions = %#o, want %#o", got, 0o600)
}
}
+11 -2
View File
@@ -44,6 +44,7 @@ import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
@@ -763,7 +764,13 @@ func (sm *SnapshotManager) compressFile(inputPath, outputPath string) error {
return nil
}
// copyFile copies a file from src to dst
// exportCopyPerm restricts the exported snapshot database copy to the owning
// user; it holds the same private index data as the local index file.
const exportCopyPerm = 0o600
// copyFile copies a file from src to dst. The destination is the exported
// snapshot database, so it is created owner-only rather than with the
// umask-dependent default.
func (sm *SnapshotManager) copyFile(src, dst string) error {
log.Debug("Opening source file for copy", "path", src)
@@ -783,7 +790,9 @@ func (sm *SnapshotManager) copyFile(src, dst string) error {
log.Debug("Creating destination file", "path", dst)
destFile, err := sm.fs.Create(dst)
destFile, err := sm.fs.OpenFile(
dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, exportCopyPerm,
)
if err != nil {
return err
}