carlos / platform Public

provision: NestedPaths cleans both row paths before the prefix checks (plan 4 final review)

A hand-edited row with a doubled slash or a dot segment is judged by the
place it names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 4f967e70a040b550e6cc3b3bab8703bf1922dbca parent 8e518dd
2 files changed, +11 −2
  • internal/provision/provision.go +5 −2
  • internal/provision/provision_test.go +6 −0
diff --git a/internal/provision/provision.go b/internal/provision/provision.go
index 38bd3c09e..64fd039a4 100644
--- a/internal/provision/provision.go
+++ b/internal/provision/provision.go
@@ -1300,9 +1300,12 @@ func NestedSocket(host string) string {
// a row can leave management or be hand-edited and still have its files
// under the instance tree, and a release before the layout, an unflag or
// a re-add would mishandle it all the same. Do not merge the two.
+//
+// Both paths are cleaned first, so a hand-edited row with a doubled slash
+// or a dot segment is judged by the place it names.
func NestedPaths(dataDir string, rt registry.Route) bool {
- return strings.HasPrefix(rt.DBPath, filepath.Join(dataDir, "instances")+"/") ||
- strings.HasPrefix(rt.Socket, filepath.Join(SocketDir, "instances")+"/")
+ return strings.HasPrefix(filepath.Clean(rt.DBPath), filepath.Join(dataDir, "instances")+"/") ||
+ strings.HasPrefix(filepath.Clean(rt.Socket), filepath.Join(SocketDir, "instances")+"/")
}
// LogSocket is the agent's per-role log listener for a host (design
diff --git a/internal/provision/provision_test.go b/internal/provision/provision_test.go
index 5503c959e..e7ba07d95 100644
--- a/internal/provision/provision_test.go
+++ b/internal/provision/provision_test.go
@@ -1119,6 +1119,12 @@ func TestNestedPathsIsShapeOnly(t *testing.T) {
{"the instances directory itself", registry.Route{Host: host, DBPath: "/data/instances", Socket: "/run/carlos/instances"}, false},
{"a sibling that shares the prefix", registry.Route{Host: host, DBPath: "/data/instances-old/x.db", Socket: "/run/carlos/instancesx/app.sock"}, false},
{"another data dir", registry.Route{Host: host, DBPath: "/srv/instances/a/a.db"}, false},
+ // A hand-edited row is read as the kernel would resolve it.
+ {"a doubled slash in the db", registry.Route{Host: host, DBPath: "/data//instances/a/a.db"}, true},
+ {"a doubled slash in the socket", registry.Route{Host: host, Socket: "/run/carlos//instances/zz/app.sock"}, true},
+ {"a dot segment in the db", registry.Route{Host: host, DBPath: "/data/./instances/a/a.db"}, true},
+ {"a dot segment in the socket", registry.Route{Host: host, Socket: "/run/carlos/./instances/zz/app.sock"}, true},
+ {"a dot-dot segment that leaves the tree", registry.Route{Host: host, DBPath: "/data/instances/../a.db"}, false},
} {
if got := NestedPaths("/data/", tc.rt); got != tc.want {
t.Errorf("%s: NestedPaths = %v, want %v", tc.name, got, tc.want)