region HA 2b: install-channel slot check by descriptor, RHB-C69, removal refuses without openat2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 files changed,
+150
−20
approved_region_ha_2b_copy_test.go+4 −4docs/superpowers/plans/2026-10-17-region-ha-2b-copy.md+4 −2internal/host/installchannel.go+20 −8internal/host/installchannel_linux.go+42 −1internal/host/installchannel_linux_test.go+36 −0internal/host/installchannel_other.go+2 −0internal/host/installchannel_test.go+37 −1internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv+5 −4
diff --git a/approved_region_ha_2b_copy_test.go b/approved_region_ha_2b_copy_test.go| index cbcc41beb..5b2cf1a14 100644 |
| --- a/approved_region_ha_2b_copy_test.go |
| +++ b/approved_region_ha_2b_copy_test.go |
| @@ -25,20 +25,20 @@ const regionHA2bCopyRecordPath = "docs/superpowers/plans/2026-10-17-region-ha-2b |
| var regionHA2bCopyIDPattern = regexp.MustCompile(`^RHB-C[0-9]+$`) |
| // regionHA2bCopyIDs is the record's inventory, pinned: RHB-C1 to RHB-C64, |
| -// RHB-C67 and RHB-C68. RHB-C65 and RHB-C66 were withdrawn in review and |
| +// RHB-C67 to RHB-C69. RHB-C65 and RHB-C66 were withdrawn in review and |
| // are not reused. |
| var regionHA2bCopyIDs = func() []string { |
| - ids := make([]string, 0, 66) |
| + ids := make([]string, 0, 67) |
| for i := 1; i <= 64; i++ { |
| ids = append(ids, "RHB-C"+strconv.Itoa(i)) |
| } |
| - return append(ids, "RHB-C67", "RHB-C68") |
| + return append(ids, "RHB-C67", "RHB-C68", "RHB-C69") |
| }() |
| // regionHA2bCopyBuilt is every id whose text the code or docs carry. The |
| // task that writes a row's text adds its ids here in the same commit, once |
| // Paul has approved the rows. |
| -var regionHA2bCopyBuilt = []string{"RHB-C1", "RHB-C2", "RHB-C3", "RHB-C4", "RHB-C5", "RHB-C6", "RHB-C7", "RHB-C8", "RHB-C9", "RHB-C10", "RHB-C11", "RHB-C12", "RHB-C13", "RHB-C14", "RHB-C15", "RHB-C16", "RHB-C17", "RHB-C18", "RHB-C64", "RHB-C67", "RHB-C68", "RHB-C19", "RHB-C20", "RHB-C21", "RHB-C22", "RHB-C23", "RHB-C24", "RHB-C25", "RHB-C26", "RHB-C27", "RHB-C28", "RHB-C29", "RHB-C30", "RHB-C31", "RHB-C32", "RHB-C33", "RHB-C34", "RHB-C35", "RHB-C36", "RHB-C37", "RHB-C38", "RHB-C39"} |
| +var regionHA2bCopyBuilt = []string{"RHB-C1", "RHB-C2", "RHB-C3", "RHB-C4", "RHB-C5", "RHB-C6", "RHB-C7", "RHB-C8", "RHB-C9", "RHB-C10", "RHB-C11", "RHB-C12", "RHB-C13", "RHB-C14", "RHB-C15", "RHB-C16", "RHB-C17", "RHB-C18", "RHB-C64", "RHB-C67", "RHB-C68", "RHB-C19", "RHB-C20", "RHB-C21", "RHB-C22", "RHB-C23", "RHB-C24", "RHB-C25", "RHB-C26", "RHB-C27", "RHB-C28", "RHB-C29", "RHB-C30", "RHB-C31", "RHB-C32", "RHB-C33", "RHB-C34", "RHB-C35", "RHB-C36", "RHB-C37", "RHB-C38", "RHB-C39", "RHB-C69"} |
| func mustParseRegionHA2bRecord(t *testing.T) []copyRow { |
| t.Helper() |
diff --git a/docs/superpowers/plans/2026-10-17-region-ha-2b-copy.md b/docs/superpowers/plans/2026-10-17-region-ha-2b-copy.md| index 3f9fd389b..e93b1a610 100644 |
| --- a/docs/superpowers/plans/2026-10-17-region-ha-2b-copy.md |
| +++ b/docs/superpowers/plans/2026-10-17-region-ha-2b-copy.md |
| @@ -30,13 +30,14 @@ The rules: |
| console enrollment pass's. |
| - Ids are not renumbered: a row added in review takes the next free id |
| and goes in its task's table (RHB-C62 to RHB-C64, round 1; RHB-C65 to |
| - RHB-C67, round 3; RHB-C68, round 4). A withdrawn row's id is not |
| + RHB-C67, round 3; RHB-C68, round 4; RHB-C69, build Task 3 round 1, |
| + approved by Paul 2026-10-17). A withdrawn row's id is not |
| reused: RHB-C65 and RHB-C66, the operator alert of round 3, went with |
| the read-back they belonged to in round 4. |
| - No em dashes. A full stop and a new sentence instead. Refusals start |
| lower case, as the CLI's others do. |
| -Paul approved RHB-C1, C5 to C9, C11 to C32, C34 to C64 and C67 to C68 unedited in the copy review on 2026-10-17. RHB-C2 to C4, C10 and C33 are internal error fragments, left out of the review under his rule that reviews hold only text a person reads in normal use; they ship as drafted. |
| +Paul approved RHB-C1, C5 to C9, C11 to C32, C34 to C64 and C67 to C68 unedited in the copy review on 2026-10-17, and RHB-C69, added in build Task 3 round 1, the same day. RHB-C2 to C4, C10 and C33 are internal error fragments, left out of the review under his rule that reviews hold only text a person reads in normal use; they ship as drafted. |
| Placeholders: `<reason>` the text of the error underneath; `<path>` a |
| file; `<region>` a placement code; `<deployment>` a deployment's name; |
| @@ -98,6 +99,7 @@ as stable; `<platform>` such as linux-arm64; `<box>` a box's reference. |
| | RHB-C37 | internal/host/installchannel.go | the <channel> channel names <version> with no release tag, so nothing was installed | approved | |
| | RHB-C38 | internal/host/installchannel.go | the release identity for <tag> could not be read, so nothing was installed: <reason> | approved | |
| | RHB-C39 | internal/host/installchannel.go | <version> (<tag>) is below the host floor <release> or is not the release its identity names, so nothing was installed | approved | |
| +| RHB-C69 | internal/host/installchannel.go | <path> holds a build this command did not install, so nothing was installed | approved | |
| ## carlos system join (Task 7) |
diff --git a/internal/host/installchannel.go b/internal/host/installchannel.go| index 5e6e96ea2..6e7dbf2e1 100644 |
| --- a/internal/host/installchannel.go |
| +++ b/internal/host/installchannel.go |
| @@ -59,6 +59,15 @@ type channelInstall struct { |
| own func(path string) error |
| } |
| +// slotKind is what one protected slot holds (slotState). |
| +type slotKind int |
| + |
| +const ( |
| + slotAbsent slotKind = iota // nothing there |
| + slotHolds // the signed artifact |
| + slotForeign // a symlink, a non-regular file or any other build |
| +) |
| + |
| // channelInstalled is what a run did. |
| type channelInstalled struct { |
| Version, Tag, Channel string |
| @@ -172,17 +181,20 @@ func (in channelInstall) install(ctx context.Context) (channelInstalled, error) |
| } |
| return got, fmt.Errorf("this box already runs %s, so carlos system update installs its releases", applied.Version) |
| } |
| - // With no applied.json, a slot that holds anything but the artifact is a |
| - // binary this command did not put there. A run cut off part way leaves |
| - // only the artifact behind (carlos.prev is written first), so a retry |
| - // passes. The slot's version is not read: that would run a binary |
| - // nothing has verified, as root, so the refusal names the slot. |
| + // With no applied.json, a slot that holds anything but the signed |
| + // artifact is a build this command did not install, and is refused and |
| + // left as it is. A run cut off part way leaves only the artifact behind |
| + // (carlos.prev is written first), so a retry passes. Each slot is opened |
| + // without following a symlink and hashed from the descriptor, against the |
| + // verified pointer's sha256; nothing is executed to learn its version. |
| for _, slot := range []string{systemBinaryPath(a.optDir), systemPrevPath(a.optDir)} { |
| - if _, err := os.Lstat(slot); err == nil && !systemCacheHolds(slot, art.SHA256) { |
| - return got, fmt.Errorf("this box already runs %s, so carlos system update installs its releases", slot) |
| - } else if err != nil && !errors.Is(err, os.ErrNotExist) { |
| + state, err := slotState(slot, art.SHA256) |
| + if err != nil { |
| return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err) |
| } |
| + if state == slotForeign { |
| + return got, fmt.Errorf("%s holds a build this command did not install, so nothing was installed", slot) |
| + } |
| } |
| cache := systemCachePath(a.dataDir, p.Current.Version) |
| defer os.RemoveAll(a.stagingDir()) |
diff --git a/internal/host/installchannel_linux.go b/internal/host/installchannel_linux.go| index 62dfa974f..cabf7fef9 100644 |
| --- a/internal/host/installchannel_linux.go |
| +++ b/internal/host/installchannel_linux.go |
| @@ -3,7 +3,10 @@ |
| package host |
| import ( |
| + "crypto/sha256" |
| + "encoding/hex" |
| "errors" |
| + "io" |
| "os" |
| "path/filepath" |
| @@ -34,6 +37,44 @@ func ownDir(path string, uid, gid int) error { |
| return unix.Fchown(fd, uid, gid) |
| } |
| +// openat2 is unix.Openat2, a seam for the test that a kernel without it |
| +// refuses the removal. There is no path-based fallback. |
| +var openat2 = unix.Openat2 |
| + |
| +// slotState opens one protected slot with O_NOFOLLOW, so a symlink is never |
| +// followed, and hashes what it holds from the descriptor. A slot that is |
| +// absent is slotAbsent; one holding want, slotHolds; a symlink, a |
| +// non-regular file or any other content, slotForeign. Any other failure to |
| +// open or read it is an error. |
| +func slotState(path, want string) (slotKind, error) { |
| + fd, err := unix.Open(path, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0) |
| + switch { |
| + case errors.Is(err, unix.ENOENT): |
| + return slotAbsent, nil |
| + case errors.Is(err, unix.ELOOP): |
| + return slotForeign, nil |
| + case err != nil: |
| + return slotAbsent, err |
| + } |
| + f := os.NewFile(uintptr(fd), path) |
| + defer f.Close() |
| + st, err := f.Stat() |
| + if err != nil { |
| + return slotAbsent, err |
| + } |
| + if !st.Mode().IsRegular() { |
| + return slotForeign, nil |
| + } |
| + h := sha256.New() |
| + if _, err := io.Copy(h, io.LimitReader(f, systemArtifactLimit+1)); err != nil { |
| + return slotAbsent, err |
| + } |
| + if want != "" && hex.EncodeToString(h.Sum(nil)) == want { |
| + return slotHolds, nil |
| + } |
| + return slotForeign, nil |
| +} |
| + |
| // removeCacheEntry removes the agent's cache entry for version, the one |
| // directory install-channel made for the fetch, and what is in it. It never |
| // resolves a path name through a symlink: the router directory is opened |
| @@ -58,7 +99,7 @@ func removeCacheEntry(dataDir, version string) error { |
| // removeBeneath removes name, a directory, and everything under it, by |
| // descriptor. A symlink is unlinked, never entered. |
| func removeBeneath(parent int, name string) error { |
| - fd, err := unix.Openat2(parent, name, &unix.OpenHow{ |
| + fd, err := openat2(parent, name, &unix.OpenHow{ |
| Flags: unix.O_RDONLY | unix.O_DIRECTORY | unix.O_CLOEXEC | unix.O_NOFOLLOW, |
| Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS, |
| }) |
diff --git a/internal/host/installchannel_linux_test.go b/internal/host/installchannel_linux_test.go| new file mode 100644 |
| index 000000000..f01454a72 |
| --- /dev/null |
| +++ b/internal/host/installchannel_linux_test.go |
| @@ -0,0 +1,36 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +// On a kernel without openat2 the removal refuses and leaves the tree: there |
| +// is no path-based fallback. |
| +func TestInstallChannelRemovalRefusesWithoutOpenat2(t *testing.T) { |
| + f := newChannelFixture(t, "v1.2.0") |
| + entry := filepath.Join(f.a.dataDir, "system", "router", "v1.4.0") |
| + if err := os.MkdirAll(entry, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(entry, "carlos"), []byte("x"), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + real := openat2 |
| + defer func() { openat2 = real }() |
| + for _, refuse := range []error{unix.ENOSYS, unix.EPERM, unix.EXDEV} { |
| + openat2 = func(int, string, *unix.OpenHow) (int, error) { return -1, refuse } |
| + if err := removeCacheEntry(f.a.dataDir, "v1.4.0"); !errors.Is(err, refuse) { |
| + t.Fatalf("removal with openat2 failing %v: %v", refuse, err) |
| + } |
| + if _, err := os.Stat(filepath.Join(entry, "carlos")); err != nil { |
| + t.Fatalf("the tree was removed without openat2: %v", err) |
| + } |
| + } |
| +} |
diff --git a/internal/host/installchannel_other.go b/internal/host/installchannel_other.go| index cd262cf1f..4ddf4eaa7 100644 |
| --- a/internal/host/installchannel_other.go |
| +++ b/internal/host/installchannel_other.go |
| @@ -13,3 +13,5 @@ func ownByService(path string) error { return errOwnNotLinux } |
| func ownDir(path string, uid, gid int) error { return errOwnNotLinux } |
| func removeCacheEntry(dataDir, version string) error { return errOwnNotLinux } |
| + |
| +func slotState(path, want string) (slotKind, error) { return slotAbsent, errOwnNotLinux } |
diff --git a/internal/host/installchannel_test.go b/internal/host/installchannel_test.go| index 787d77ac4..7030681d1 100644 |
| --- a/internal/host/installchannel_test.go |
| +++ b/internal/host/installchannel_test.go |
| @@ -396,7 +396,7 @@ func TestInstallChannelRefusesAHandRolledBoxAndCompletesACutOffRun(t *testing.T) |
| t.Fatal(err) |
| } |
| _, err := f.in.run(context.Background()) |
| - if err == nil || err.Error() != "this box already runs "+path+", so carlos system update installs its releases" { |
| + if err == nil || err.Error() != path+" holds a build this command did not install, so nothing was installed" { |
| t.Fatalf("hand-rolled %s: %v", path, err) |
| } |
| if b, _ := os.ReadFile(path); string(b) != "#!/bin/sh\necho hand rolled\n" || f.fetches != 0 { |
| @@ -435,3 +435,39 @@ func TestInstallChannelRefusesAHandRolledBoxAndCompletesACutOffRun(t *testing.T) |
| }) |
| } |
| } |
| + |
| +// A slot that is a symlink is refused, even one to a file that holds the |
| +// artifact, and neither the link nor its target is touched. |
| +func TestInstallChannelRefusesASymlinkedSlot(t *testing.T) { |
| + for _, slot := range []func(string) string{systemBinaryPath, systemPrevPath} { |
| + f := newChannelFixture(t, "v1.2.0") |
| + target := filepath.Join(t.TempDir(), "elsewhere") |
| + if err := os.WriteFile(target, f.body, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + path := slot(f.a.optDir) |
| + if err := os.Symlink(target, path); err != nil { |
| + t.Fatal(err) |
| + } |
| + _, err := f.in.run(context.Background()) |
| + if err == nil || err.Error() != path+" holds a build this command did not install, so nothing was installed" || f.fetches != 0 { |
| + t.Fatalf("symlinked %s: %v after %d fetches", path, err, f.fetches) |
| + } |
| + if l, err := os.Readlink(path); err != nil || l != target { |
| + t.Fatalf("the link was changed: %q %v", l, err) |
| + } |
| + if b, _ := os.ReadFile(target); !bytes.Equal(b, f.body) { |
| + t.Fatalf("the link's target was changed: %q", b) |
| + } |
| + // A slot that cannot be read at all is an error, not a pass. |
| + if err := os.Remove(path); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(path, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := f.in.run(context.Background()); err == nil || !strings.Contains(err.Error(), "holds a build this command did not install") { |
| + t.Fatalf("a directory in a slot: %v", err) |
| + } |
| + } |
| +} |
diff --git a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv| index beb5191b1..0855ccfe7 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv |
| @@ -1,12 +1,13 @@ |
| # Plan 14 census overlay region-ha-2b (region HA spec 2, plan 2b: docs/superpowers/plans/2026-10-17-region-ha-2b-joining.md), loaded last. RULED by the Plan 14 controller 2026-10-16 as census-ruling-region-ha-2b. |
| # Task 2: host.env from the deployment's parameters (internal/hostenv: hostenv.go, the layout, assembly and rendering, with an allowlist of names, shared values written create-only and the group-box marker added on write; cli.go, carlos system host-env write and seed; write_linux.go, the write through directories held by descriptor, and write_other.go, its refusal off Linux; awsssm.go, the SSM adapter) and the SSM SDK in go.mod. Three new root-read units. runHostEnv: seed's --from file. Ruled authority A,J4 on conditions (no box write to the parameters; allowlisted names, safe values, atomic write, checked parent; authority-selecting values cross-checked against the roster at the agent's use). writeHostEnv and checkedDir (Linux): write's fixed path, /etc/carlos/host.env, reached only through /etc and /etc/carlos opened O_NOFOLLOW|O_DIRECTORY and checked by fstat (root's, writable by no group or other), the file written 0600 in that directory, synced, renamed and the directory synced; ruled A,J4 under census-ruling-region-ha-2b, as the checked parent runHostEnv's condition asks for. The legacy seal stands. |
| -# Task 3: carlos system install-channel (internal/host installchannel.go, installchannel_linux.go): a fresh box installs its signed channel release into both protected slots through verifyPointer, hostfloor.CheckImage and stageCandidate, all unchanged, and on every run hands each directory on the agent's cache path (system, system/router and the version's) to the service user through bootstrap's own descriptor-anchored open, refusing a symlinked parent. One new root-read unit, channelInstall.run, whose one read site opens root's apply lock (systemApplier's lockPath). Ruled authority F,J4. stageCandidate, accepted-legacy, is called and not changed. The legacy seal stands. Fix round 1: two more root-read units, channelInstall.install (an lstat of each protected slot, to refuse a slot holding a binary this command did not put there) and removeBeneath (Linux; removes the agent's cache entry only by openat2 and unlinkat on descriptors anchored at the router directory, opened with no symlink followed, never by path name). Both ruled F,J4 on the same footing as channelInstall.run. |
| +# Task 3: carlos system install-channel (internal/host installchannel.go, installchannel_linux.go): a fresh box installs its signed channel release into both protected slots through verifyPointer, hostfloor.CheckImage and stageCandidate, all unchanged, and on every run hands each directory on the agent's cache path (system, system/router and the version's) to the service user through bootstrap's own descriptor-anchored open, refusing a symlinked parent. One new root-read unit, channelInstall.run, whose one read site opens root's apply lock (systemApplier's lockPath). Ruled authority F,J4. stageCandidate, accepted-legacy, is called and not changed. The legacy seal stands. Fix rounds 1 and 2: more root-read units. slotState (Linux): each protected slot opened with O_NOFOLLOW and hashed from the descriptor against the signed artifact's sha256, to refuse a slot holding a build this command did not install. removeBeneath (Linux): removes the agent's cache entry only by openat2 and unlinkat on descriptors anchored at the router directory, opened with no symlink followed, never by path name, and refuses without openat2. var openat2: the unix.Openat2 seam for the test that it refuses. All ruled F,J4 on the same footing as channelInstall.run. |
| @coverage . be7c96bb2ff00b5b018b436f976f0d33bbcc8765c17e357baa6cd0290f59f42d |
| @coverage internal/hostenv 45d9d4b90a5d441020f5ba4573821ca92701246c80b32d6f1823665fb0f65208 |
| internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899 |
| internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| -@coverage internal/host ad94a5be72fa655670bf48a74a80acaad2668df5169a9fcbc9bd2fda9c6785af |
| +@coverage internal/host fca6227146690ccc5c14310d24728c69acd42465d1ec277e08c68e7a7f11997c |
| internal/host/installchannel.go channelInstall.run authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, at a fresh box's start-up): its one read site opens root's apply lock; the install goes through verifyPointer, hostfloor.CheckImage and stageCandidate into both protected slots, and refuses a box whose guard is armed or that already runs another release file:OpenFile@196 f0cdb6dd28e83da282c3b0d7bc2012dcd80db181a8cecbf1a2db6bcc41d1d5e3 |
| -internal/host/installchannel.go channelInstall.install authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, at a fresh box's start-up): its one read site is an lstat of each protected slot, so that a slot holding a binary this command did not put there, on a box with no install record, is refused and left as it is; the slot's own path names it and nothing is executed file:Lstat@2787 17cbca29dcc9bf5df5b901f71aafbe68bde808c0b675bf857b864ebfe1b29058 |
| -internal/host/installchannel_linux.go removeBeneath authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, Linux): removes the agent's cache entry for the version, after a run that made it, only through the router directory held by descriptor (opened with no symlink anywhere on its path) and its version directory opened beneath it with RESOLVE_BENEATH|RESOLVE_NO_SYMLINKS, unlinking by descriptor; a symlink is unlinked, never entered, so nothing outside /data/system/router/<version> is touched file:Openat2@64,file:Readdirnames@386 146d530b92c37b22a70758456529e9ad23bf93abd1269fa234d736e57ef4d2d6 |
| +internal/host/installchannel_linux.go slotState authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, Linux): opens one protected slot with O_NOFOLLOW (a symlink is never followed), fstats the descriptor and hashes from it, to compare with the sha256 of the signed, verified artifact; a slot holding anything else, a symlink or a non-regular file is refused and left as it is, and nothing in it is executed file:Open@66,file:Stat@388 bc533a29f92ae95d31123b551354ab9f8960576e5ffef1bd6a74a06df0b57a24 |
| +internal/host/installchannel_linux.go var openat2 authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, Linux): unix.Openat2 held in a variable so a test can make it fail; removeBeneath is its only user and has no path-based fallback when it fails file:Openat2@14 6081e86f758f4ae3948ec2d4f4c037888e1362f122a410c110e31290ba914816 |
| +internal/host/installchannel_linux.go removeBeneath authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, Linux): removes the agent's cache entry for the version, after a run that made it, only through the router directory held by descriptor (opened with no symlink anywhere on its path); its version directory is opened beneath it with RESOLVE_BENEATH|RESOLVE_NO_SYMLINKS and everything under it is unlinked by descriptor; a symlink is unlinked, never entered; when openat2 is unavailable or refuses, the removal refuses and leaves the tree, with no path-based fallback; its one read site lists the held directory's names file:Readdirnames@381 42732bb7b400abdb7f18ddbf5aacdfadc9496d8941e774dc01a471bf15c35f69 |