diff --git a/CHANGELOG.md b/CHANGELOG.md index 7898961..d5b29e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,10 @@ still refuses and the app page still says so. household's folders come through `${USERDATA_PATH}`. Every catalog bind already uses `${HDD_PATH}/appdata/…`. Known limit, stated: one app can still name ANOTHER app's `appdata/` (paperless-ngx's folder is `paperless`, so a per-app rule needs a name map) — filed. Red-proved (`P2/redproof-security-review-round3.txt`). +- **Found live on demo-hp 9201 with rc.4:** the first sync refused every app NOT installed (sabnzbd, sonarr, …): on + the box the placeholder roots do not exist, so the symlink resolve could not resolve the ROOT and nothing matched. A + root is now resolved through its nearest existing ancestor too, and the placeholder check (no app env) resolves no + symlinks at all. `TestAdmit_RootsThatDoNotExistYet`, red-proved (`P2/redproof-live-root-resolve.txt`). ## v0.327.1 — files. reaches FileBrowser again: a file route by name, not Docker labels (2026-10-17) diff --git a/controller/internal/privpolicy/privpolicy.go b/controller/internal/privpolicy/privpolicy.go index adf7976..9939ed3 100644 --- a/controller/internal/privpolicy/privpolicy.go +++ b/controller/internal/privpolicy/privpolicy.go @@ -548,8 +548,9 @@ func (c *checker) underRoot(p string) bool { return false } for _, r := range c.roots { - rr, rerr := c.p.Resolve(r) - if rerr == nil && within(rp, rr) { + // A root is resolved the same way (nearest existing ancestor): a drive folder not created yet must not make + // every bind under it "outside" (found live on 9201 with rc.4: every not-installed app refused). + if rr, ok := c.resolveExisting(r); ok && within(rp, rr) { return true } } diff --git a/controller/internal/privpolicy/privpolicy_test.go b/controller/internal/privpolicy/privpolicy_test.go index 955e02b..6db5885 100644 --- a/controller/internal/privpolicy/privpolicy_test.go +++ b/controller/internal/privpolicy/privpolicy_test.go @@ -417,3 +417,24 @@ func TestAdmit_HDDPathRootIsAppdataOnly(t *testing.T) { } } } + +// Found live on 9201 (rc.4): with Resolve set, roots that do not exist yet (a drive folder not created, or the +// placeholder env) made every bind "outside". A root is resolved through its nearest existing ancestor too. +func TestAdmit_RootsThatDoNotExistYet(t *testing.T) { + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "mnt", "felhom-drives"), 0o755); err != nil { + t.Fatal(err) + } + resolve := func(s string) (string, error) { + r, err := filepath.EvalSymlinks(filepath.Join(root, s)) + if err != nil { + return "", err + } + rel, _ := filepath.Rel(root, r) + return "/" + rel, nil + } + c := "services:\n app:\n image: x/y:1\n volumes:\n - ${USERDATA_PATH}/media:/m\n - ${HDD_PATH}/appdata/app:/c\n" + if err := Admit("app", []byte(c), PlaceholderEnv(), Policy{Resolve: resolve}); err != nil { + t.Fatalf("binds under roots that do not exist yet were refused: %v", err) + } +} diff --git a/controller/internal/stacks/allowlist.go b/controller/internal/stacks/allowlist.go index e9360aa..8fe6483 100644 --- a/controller/internal/stacks/allowlist.go +++ b/controller/internal/stacks/allowlist.go @@ -50,11 +50,14 @@ func (m *Manager) policyEnv(env map[string]string) map[string]string { // app's refusal line. func (m *Manager) admitCompose(name string, data []byte, env map[string]string) error { e := privpolicy.PlaceholderEnv() + pol := privpolicy.Policy{SystemDataPath: m.sysDataPath} if env != nil { e = m.policyEnv(env) + // Resolve (real paths only — the placeholder env names no real folder): a bind source that is a symlink out + // of its data root is outside; it is judged where it points. + pol.Resolve = filepath.EvalSymlinks } - // Resolve: a bind source that is a symlink out of its data root is outside (it is judged where it points). - err := privpolicy.Admit(name, data, e, privpolicy.Policy{SystemDataPath: m.sysDataPath, Resolve: filepath.EvalSymlinks}) + err := privpolicy.Admit(name, data, e, pol) m.noteAdmission(name, data, err) return err }