diff --git a/CHANGELOG.md b/CHANGELOG.md index 6292b0d..4fe68cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,7 +75,12 @@ picker paths, and the system drive is deliberately not a registered StoragePath. `{{if .System}}` would have failed at render for every share. `ShareRow` is now package-level and the render test constructs the exact type the handler passes. -Tests 915 → 949, all green. Red-proofs recorded in REPORT.md for Scenario B (classification), +**Caught during the live deploy leg and fixed in the same version:** `EnsureImportRoot` ensured only +the leaf, so `MkdirAll`'s intermediates left `/userdata` at `755 root:root` — the one userdata +root on the box outside the 2775/gid-1000 convention. Both the parent and the import dir now carry it +(`TestEnsureImportRoot_ParentCarriesTheConvention`). + +Tests 915 → 950, all green. Red-proofs recorded in REPORT.md for Scenario B (classification), C (determinism) and E (server-side share refusal). No destructive filesystem call was added anywhere in this arc. **MinAgent unchanged.** diff --git a/controller/internal/stacks/import_root_classify_test.go b/controller/internal/stacks/import_root_classify_test.go index 05319d1..8860025 100644 --- a/controller/internal/stacks/import_root_classify_test.go +++ b/controller/internal/stacks/import_root_classify_test.go @@ -323,3 +323,26 @@ func TestPathUnderIsSegmentWise(t *testing.T) { t.Error("a name-prefix sibling must NOT be under the root") } } + +// EnsureImportRoot must apply the convention to the import dir AND its parent userdata dir. +// Observed live on demo-felhom (v0.172.0 first cut): ensuring only the leaf left +// /userdata at 755 root:root, the one userdata root on the box outside the convention. +func TestEnsureImportRoot_ParentCarriesTheConvention(t *testing.T) { + m := newMigManager(t, "") + m.cfg.Paths.SystemDataPath = t.TempDir() // writable stand-in for /mnt/sys_drive + + if err := m.EnsureImportRoot(); err != nil { + // chown to gid 1000 fails for a non-root test user; the modes still land. + t.Logf("EnsureImportRoot returned %v (expected off-root)", err) + } + sysNS := appbackup.NamespaceRoot(m.cfg.Paths.SystemDataPath, false) + for _, p := range []string{appbackup.UserdataDir(sysNS), appbackup.ImportDir(sysNS)} { + fi, err := os.Stat(p) + if err != nil { + t.Fatalf("%s not created: %v", p, err) + } + if fi.Mode().Perm() != 0o775 || fi.Mode()&os.ModeSetgid == 0 { + t.Errorf("%s mode = %v, want setgid + 0775 (2775)", p, fi.Mode()) + } + } +} diff --git a/controller/internal/stacks/skeleton_derive.go b/controller/internal/stacks/skeleton_derive.go index 21fb56c..cb1e3a6 100644 --- a/controller/internal/stacks/skeleton_derive.go +++ b/controller/internal/stacks/skeleton_derive.go @@ -77,5 +77,16 @@ func (m *Manager) EnsureImportRoot() error { if root == "" { return nil } + // The PARENT userdata dir must carry the convention too. EnsureUserdataDir MkdirAll's its + // intermediates at plain 0755 and then chmods only the leaf, so ensuring just the import dir + // leaves /userdata at 755 root:root — the one userdata root on the box that would not + // be 2775/gid-1000. Observed live on demo-felhom before this line existed. + sys := m.cfg.Paths.SystemDataPath + if sys == "" { + return nil + } + if err := appbackup.EnsureUserdataDir(appbackup.UserdataDir(appbackup.NamespaceRoot(sys, false))); err != nil { + return err + } return appbackup.EnsureUserdataDir(root) }