R-839: the deploy API refuses an HDD_PATH that is a folder inside a registered drive

Diagnosed: 9202's paperless-ngx app.yaml named <drive>/userdata/paperless-ngx, written by the
2026-09-22 drill harness through POST /api/stacks/<n>/deploy, which accepted it (RefuseAsAppNamespace
matches by prefix). The boot sweep then asked a folder whether it was a mountpoint and HELD the app.
The deploy page offers drive roots only, so a household could not reach this; every other caller
is now refused with a sentence naming the drive (settings.DriveOfSubPath).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 21:52:05 +02:00
parent 0b93e1ad1e
commit 4867ec1804
6 changed files with 101 additions and 0 deletions
+23
View File
@@ -2119,6 +2119,29 @@ func (s *Settings) RefuseAsAppNamespace(path string) (bool, string) {
return false, ""
}
// DriveOfSubPath reports the registered storage path that `path` lies strictly INSIDE (R-839). An app's
// HDD_PATH names a drive root — the namespace root, the userdata dir and the backups tree are all
// derived from it — so a value one or more segments below a registered drive is never valid: the boot
// sweep's mountpoint check then asks about a folder rather than the drive, and HOLDS the app after
// every restart (measured on 9202: `<drive>/userdata/paperless-ngx`, written by a drill harness through
// the deploy API, which accepted it because RefuseAsAppNamespace matches by prefix). Exact matches and
// unregistered paths return ok=false. Pinned by TestR839_DeployRefusesASubPathOfADrive.
func (s *Settings) DriveOfSubPath(path string) (string, bool) {
path = strings.TrimRight(strings.TrimSpace(path), "/")
if path == "" || s == nil {
return "", false
}
s.mu.RLock()
defer s.mu.RUnlock()
for _, sp := range s.StoragePaths {
root := strings.TrimRight(sp.Path, "/")
if root != "" && strings.HasPrefix(path, root+"/") {
return sp.Path, true
}
}
return "", false
}
// Refusal reasons for RefuseAsAppNamespace. Hungarian, adult tone, no emoji — these reach the customer
// through the deploy/migrate error surfaces. They name the storage class and what to do instead, never
// an internal path or field name.