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>
2 files changed,
+11
−2
internal/provision/provision.go+5 −2internal/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) |