fix: run DWH verification with read-only registry access

This commit is contained in:
User
2026-08-21 02:14:50 +02:00
parent 419c3440d7
commit 134dc1977c
6 changed files with 291 additions and 14 deletions
+27 -1
View File
@@ -48,6 +48,18 @@ var socketOwner = func(path string) (uint32, error) {
return stat.Uid, nil
}
var socketParentOwner = func(path string) (uint32, error) {
info, err := os.Lstat(path)
if err != nil {
return 0, err
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return 0, fmt.Errorf("socket parent owner is unavailable")
}
return stat.Uid, nil
}
// New returns the HTTP verifier. It has no network listener and is safe to use
// with an httptest server only for synthetic test registries.
func New(store *registry.Store, logger *log.Logger, now func() time.Time) http.Handler {
@@ -101,7 +113,7 @@ func ListenAndServe(ctx context.Context, config Config) error {
if err := validateSocketParent(config.Socket); err != nil {
return err
}
store, err := registry.Open(config.RegistryRoot)
store, err := registry.OpenReadOnly(config.RegistryRoot)
if err != nil {
return err
}
@@ -164,6 +176,20 @@ func validateSocketParent(socket string) error {
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("socket parent is not a directory")
}
if info.Mode().Perm()&0o022 != 0 {
return errors.New("socket parent is group or world writable")
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return errors.New("socket parent owner is unavailable")
}
owner, err := socketParentOwner(parent)
if err != nil {
return err
}
if stat.Uid != uint32(os.Geteuid()) || owner != stat.Uid {
return errors.New("socket parent owner mismatch")
}
resolved, err := filepath.EvalSymlinks(parent)
if err != nil || resolved != parent {
return errors.New("socket parent is not canonical")
@@ -212,6 +212,7 @@ func TestVerifyMapsMultipleLegacyRecordsToServiceUnavailable(t *testing.T) {
func TestListenAndServeFailsClosedForInvalidPathsAndCollisions(t *testing.T) {
root := t.TempDir()
initializeRegistry(t, root)
parent := t.TempDir()
for _, tc := range []struct {
name, registryRoot, socket string
@@ -252,8 +253,47 @@ func TestListenAndServeFailsClosedForInvalidPathsAndCollisions(t *testing.T) {
}
}
func TestListenAndServeRejectsUnsafeSocketParents(t *testing.T) {
root := t.TempDir()
initializeRegistry(t, root)
for _, mode := range []os.FileMode{0o770, 0o702} {
t.Run("mode "+mode.String(), func(t *testing.T) {
parent := t.TempDir()
if err := os.Chmod(parent, mode); err != nil {
t.Fatalf("Chmod(%v) error = %v", mode, err)
}
socket := filepath.Join(parent, "verify.sock")
err := ListenAndServe(context.Background(), Config{RegistryRoot: root, Socket: socket, Logger: log.New(&bytes.Buffer{}, "", 0), Now: time.Now})
if err == nil {
t.Fatalf("ListenAndServe(parent mode %v) error = nil, want refusal", mode)
}
if _, err := os.Lstat(socket); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("unsafe parent created socket: %v", err)
}
})
}
}
func TestListenAndServeRejectsForeignOwnedSocketParent(t *testing.T) {
root := t.TempDir()
initializeRegistry(t, root)
parent := t.TempDir()
socket := filepath.Join(parent, "verify.sock")
previous := socketParentOwner
t.Cleanup(func() { socketParentOwner = previous })
socketParentOwner = func(string) (uint32, error) { return uint32(os.Geteuid()) + 1, nil }
err := ListenAndServe(context.Background(), Config{RegistryRoot: root, Socket: socket, Logger: log.New(&bytes.Buffer{}, "", 0), Now: time.Now})
if err == nil {
t.Fatal("ListenAndServe() error = nil, want foreign-parent refusal")
}
if _, err := os.Lstat(socket); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("foreign parent created socket: %v", err)
}
}
func TestListenAndServeReclaimsOnlyOwnedStaleSocketAndCleansUpOnCancellation(t *testing.T) {
root := t.TempDir()
initializeRegistry(t, root)
socket := filepath.Join(t.TempDir(), "verify.sock")
stale, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"})
if err != nil {
@@ -290,6 +330,7 @@ func TestListenAndServeReclaimsOnlyOwnedStaleSocketAndCleansUpOnCancellation(t *
func TestListenAndServeRefusesStaleSocketWithForeignOwner(t *testing.T) {
root := t.TempDir()
initializeRegistry(t, root)
socket := filepath.Join(t.TempDir(), "verify.sock")
stale, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"})
if err != nil {
@@ -313,6 +354,7 @@ func TestListenAndServeRefusesStaleSocketWithForeignOwner(t *testing.T) {
func TestListenAndServeRefusesLiveSocket(t *testing.T) {
root := t.TempDir()
initializeRegistry(t, root)
socket := filepath.Join(t.TempDir(), "verify.sock")
live, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"})
if err != nil {
@@ -330,6 +372,7 @@ func TestListenAndServeRefusesLiveSocket(t *testing.T) {
func TestListenAndServeCleanupRefusesChangedSocketPath(t *testing.T) {
root := t.TempDir()
initializeRegistry(t, root)
socket := filepath.Join(t.TempDir(), "verify.sock")
ctx, cancel := context.WithCancel(context.Background())
errs := make(chan error, 1)
@@ -363,6 +406,17 @@ func TestListenAndServeCleanupRefusesChangedSocketPath(t *testing.T) {
}
}
func initializeRegistry(t *testing.T, root string) {
t.Helper()
store, err := registry.Open(root)
if err != nil {
t.Fatalf("Open() error = %v", err)
}
if err := store.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
}
func waitForReadySocket(t *testing.T, socket string) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)