guard-arm: make the guard directories 0755 whatever the caller's umask
A umask 077 operator shell left /opt/carlos/system/guard and guard/identities 0700 on the dev console box, and console.service (run as carlos, exec'ing the guard binary) restart-looped with exit 203. mkdirAllExact chmods every directory it creates, and the target, to the exact mode, so a re-run also repairs a box left at 0700. Census overlay claim-guard-umask, ruled by platform-1e.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+92
−1
internal/host/guardarm.go+32 −1internal/host/guardarm_mkdir_linux_test.go+47 −0internal/provision/plan14_root_reads_test.go+8 −0internal/provision/testdata/plan14-root-reads-claim-guard-umask.tsv+5 −0
diff --git a/internal/host/guardarm.go b/internal/host/guardarm.go| index 98e4c7693..f0d09bc37 100644 |
| --- a/internal/host/guardarm.go |
| +++ b/internal/host/guardarm.go |
| @@ -111,8 +111,12 @@ func RunSystemGuardArm(args []string) { |
| if err = guardArmControllers(dataDir()); err != nil { |
| log.Fatal(err) |
| } |
| + // The guard directories are traversed by non-root units (the console |
| + // and the edge exec the guard binary through them), so their mode |
| + // cannot depend on the caller's umask: a umask 077 shell once left |
| + // them 0700 and the guarded console failed with exit 203. |
| for _, dir := range []string{d.Dir, filepath.Join(d.Dir, "identities")} { |
| - if err = os.MkdirAll(dir, 0755); err != nil { |
| + if err = mkdirAllExact(dir, 0755); err != nil { |
| log.Fatal(err) |
| } |
| } |
| @@ -269,3 +273,30 @@ func guardArmControllers(dataDir string) error { |
| } |
| return hostfloor.EnsureControllerDir(dataDir, uid, gid) |
| } |
| + |
| +// mkdirAllExact is os.MkdirAll with the caller's umask taken out: every |
| +// directory it creates, and dir itself whether or not it was created, ends |
| +// with exactly mode. Existing parents keep their modes. |
| +func mkdirAllExact(dir string, mode os.FileMode) error { |
| + var created []string |
| + for p := filepath.Clean(dir); ; p = filepath.Dir(p) { |
| + if _, err := os.Lstat(p); err == nil { |
| + break |
| + } else if !os.IsNotExist(err) { |
| + return err |
| + } |
| + created = append(created, p) |
| + if parent := filepath.Dir(p); parent == p { |
| + break |
| + } |
| + } |
| + if err := os.MkdirAll(dir, mode); err != nil { |
| + return err |
| + } |
| + for _, p := range append(created, filepath.Clean(dir)) { |
| + if err := os.Chmod(p, mode); err != nil { |
| + return err |
| + } |
| + } |
| + return nil |
| +} |
diff --git a/internal/host/guardarm_mkdir_linux_test.go b/internal/host/guardarm_mkdir_linux_test.go| new file mode 100644 |
| index 000000000..906be977a |
| --- /dev/null |
| +++ b/internal/host/guardarm_mkdir_linux_test.go |
| @@ -0,0 +1,47 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "syscall" |
| + "testing" |
| +) |
| + |
| +// A umask 077 operator shell must not leave the guard directories closed to |
| +// the non-root units that exec the guard binary through them. |
| +func TestMkdirAllExactIgnoresUmask(t *testing.T) { |
| + old := syscall.Umask(0o077) |
| + defer syscall.Umask(old) |
| + root := t.TempDir() |
| + if err := os.Chmod(root, 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + dir := filepath.Join(root, "system", "guard", "identities") |
| + if err := mkdirAllExact(dir, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, p := range []string{filepath.Join(root, "system"), filepath.Join(root, "system", "guard"), dir} { |
| + st, err := os.Stat(p) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if st.Mode().Perm() != 0o755 { |
| + t.Fatalf("%s is %o, want 755", p, st.Mode().Perm()) |
| + } |
| + } |
| + if st, _ := os.Stat(root); st.Mode().Perm() != 0o700 { |
| + t.Fatalf("an existing parent's mode changed: %o", st.Mode().Perm()) |
| + } |
| + // A directory left 0700 by an earlier run is repaired. |
| + if err := os.Chmod(dir, 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := mkdirAllExact(dir, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if st, _ := os.Stat(dir); st.Mode().Perm() != 0o755 { |
| + t.Fatalf("existing dir not repaired: %o", st.Mode().Perm()) |
| + } |
| +} |
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go| index 13323fdb4..8e68b7dbb 100644 |
| --- a/internal/provision/plan14_root_reads_test.go |
| +++ b/internal/provision/plan14_root_reads_test.go |
| @@ -925,6 +925,14 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-claim-untagged-pointer-fix2.tsv"), &inv, true); err != nil { |
| t.Fatal(err) |
| } |
| + // Census overlay claim-guard-umask (guard-arm's directories ignore the |
| + // caller's umask), loaded last, under census-ruling-claim-guard-umask |
| + // (platform-1e): RunSystemGuardArm re-pinned at its class F,J4, the new |
| + // mkdirAllExact classified G,J4, and the internal/host envelope |
| + // re-pinned. It adds no accepted-legacy row, so the seal stands. |
| + if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-claim-guard-umask.tsv"), &inv, true); err != nil { |
| + t.Fatal(err) |
| + } |
| seal = "53b3cbb6f46d657ce32f49dad84dae79316c80da949ccf37273833eef5c5b2d7" |
| errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...) |
| errs = append(errs, checkRootReadDeclarations(got, inv)...) |
diff --git a/internal/provision/testdata/plan14-root-reads-claim-guard-umask.tsv b/internal/provision/testdata/plan14-root-reads-claim-guard-umask.tsv| new file mode 100644 |
| index 000000000..d6ba8d084 |
| --- /dev/null |
| +++ b/internal/provision/testdata/plan14-root-reads-claim-guard-umask.tsv |
| @@ -0,0 +1,5 @@ |
| +# Census overlay claim-guard-umask (guard-arm's directories ignore the caller's umask; amadan issue drafted 2026-10-19), loaded LAST, after claim-untagged-pointer-fix2. Ruled by platform-1e as census owner: census-ruling-claim-guard-umask. |
| +# RunSystemGuardArm: the guard and identities directories are now made through mkdirAllExact instead of os.MkdirAll; its read sites are the same files at shifted offsets. mkdirAllExact (new): Lstat walks up only to find which components it creates, then chmods exactly those and the target to 0755. It reads no file content and grants nothing. The seal stands: no accepted-legacy row. No 9B8 entry: it writes no record. |
| +@coverage internal/host eb14b3eea2f1fb467aa899a71621e30404cfd1424f030fa320b34a04ebb481c1 |
| +internal/host/guardarm.go RunSystemGuardArm authority F,J4 rev3=census-ruling-claim-guard-umask;identity=carlos system guard-arm, offline CLI uid 0;basis=root guard-arm checks retained census floor before installing guard; no census retains main; policy and identity parse in guardArmInputs, same files read; guard directories made through mkdirAllExact file:OpenFile@565,file:ReadFile@1572,file:ReadFile@5883,file:ReadFile@865,file:ReadFile@945,file:Stat@3534 a6bd0a2da1b5e4d19f3c58da8331dbc5e166da31aba289c77153afe4f28a4cbb |
| +internal/host/guardarm.go mkdirAllExact authority G,J4 rev3=census-ruling-claim-guard-umask;identity=carlos system guard-arm, offline CLI uid 0;basis=Lstat of the target path and its missing parents only, to choose which directories to create and chmod to 0755; no file content is read file:Lstat@149 d1c988f22c5559ba6765ba9072d3fbbf26fd33836ab672157cb4e1bd9b99aaf3 |