carlos / platform Public

edge: fix race condition in purge listener socket creation

Use syscall.Umask to prevent a world-connectable race window when
creating the unix socket. The socket file is now created at mode 0600
from the moment it exists, with concurrent ListenPurge calls serialized
around the umask change to prevent interference (carlios agent spawns
multiple instances concurrently).

Fixes security vulnerability identified in code review.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Paul Campbell 17c5afe83d6773b9b658a01ff39131b7ee3f1802 parent 16d329c
1 file changed, +21 −0
  • internal/edge/purge.go +21 −0
diff --git a/internal/edge/purge.go b/internal/edge/purge.go
index 8ee360962..2f931ef79 100644
--- a/internal/edge/purge.go
+++ b/internal/edge/purge.go
@@ -6,6 +6,8 @@ import (
"net"
"net/http"
"os"
+ "sync"
+ "syscall"
)
// PurgeListener is a per-instance, local-only channel for invalidating
@@ -25,6 +27,13 @@ type purgeRequest struct {
Query string `json:"query"`
}
+// listenMu guards the umask-wrapped net.Listen call in ListenPurge. Since
+// syscall.Umask is process-wide and not per-goroutine, concurrent ListenPurge
+// calls (e.g., in carlos agent spawning multiple instances) must serialize
+// around the umask change to avoid race windows where the socket is created
+// world-connectable before being chmod'd to 0600.
+var listenMu sync.Mutex
+
// ListenPurge binds socketPath (removing any stale file left behind by a
// crashed previous instance) and serves purge requests scoped to host —
// host is fixed here, never taken from the request, so a compromised or
@@ -33,10 +42,22 @@ func ListenPurge(socketPath, host string, cache *Cache) (*PurgeListener, error)
if err := os.Remove(socketPath); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
}
+
+ // Create the socket with umask 0177 to prevent a world-connectable race window.
+ // syscall.Umask is process-wide, so we serialize around it with listenMu
+ // to protect against concurrent ListenPurge calls (e.g., in carlos agent
+ // spawning multiple instances).
+ listenMu.Lock()
+ prevUmask := syscall.Umask(0177)
ln, err := net.Listen("unix", socketPath)
+ syscall.Umask(prevUmask)
+ listenMu.Unlock()
+
if err != nil {
return nil, err
}
+
+ // Defensive chmod as backstop (no longer load-bearing, but kept for defense-in-depth).
if err := os.Chmod(socketPath, 0600); err != nil {
ln.Close()
return nil, err