claim-prev-rehearsal fix round 1: mandatory carlos.prev record, unfinished-update refusal, census branch, hardened policy read
All in the install-channel --policy path; without --policy nothing changes. - Astra: carlos.prev's version record is mandatory (completePrev); a run whose record cannot be written fails and keeps no identity, and the already-installed answer requires the record or completes it. - I1: an open update journal or update-begun marker refuses (guardArmIdle). - I2: the --policy applier reads the root participation census, so a census box takes the update's root moves floor branch. - M3: the policy file is opened O_NOFOLLOW|O_NONBLOCK, fstat'd regular, capped at 64 KiB. - M4: one wording for the policy refusal; the replaced line only when carlos.prev held another release. Census overlay claim-prev-rehearsal-fix1 PROPOSED.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 files changed,
+541
−45
docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md+21 −0internal/host/installchannel.go+113 −18internal/host/installchannel_linux.go+32 −0internal/host/installchannel_other.go+2 −0internal/host/installchannel_policy_rootrunner_linux_test.go+77 −0internal/host/installchannel_policy_test.go+280 −27internal/provision/plan14_root_reads_test.go+8 −0internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal-fix1.tsv+8 −0
diff --git a/docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md b/docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md| index dd329d5dd..225453681 100644 |
| --- a/docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md |
| +++ b/docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md |
| @@ -31,3 +31,24 @@ No new bucket writer. install-channel writes only local files (`carlos.prev`, it |
| ## Test files |
| The rewritten root-runner witness (`activation_console_rootrunner_linux_test.go`) and the new `installchannel_policy_test.go` are tests and are outside the census. |
| + |
| +## Fix round 1 (PROPOSED) |
| + |
| +Overlay `internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal-fix1.tsv`, loaded after the ruled overlay, every row `rev3=PROPOSED-claim-prev-rehearsal-fix1`. Everything is in the `--policy` path; without `--policy` nothing changes. |
| + |
| +What changed: |
| + |
| +- carlos.prev's version record is mandatory in the `--policy` path (Astra). `completePrev` runs under the prev lock: it removes the old verdicts and record, copies carlos when refreshing, then writes the record and reads it back. Every failure is fatal, then the floor's judgement follows through `notePrev` without a replace. The `--policy` "already installed" answer requires the record, and completes it when it is missing or stale. |
| +- An open update journal, or an update-begun marker beside the guard, refuses through `guardArmIdle` (I1). |
| +- The `--policy` applier reads the root participation census (`rootMovesFloorIdentity`), as the update's does, so a census box takes the root moves floor branch (I2). |
| +- The policy file is opened with O_NOFOLLOW and O_NONBLOCK, checked with fstat to be a regular file, and capped at 64 KiB (M3). |
| + |
| +| Unit | Change | Class | Executing identity | |
| +|---|---|---|---| |
| +| `internal/host/installchannel.go` `channelInstall.run` | digest unchanged; basis corrected (M5): it now also runs with `--policy` at an updated box's root shell | F,J4, kept exactly | unchanged | |
| +| `internal/host/installchannel.go` `channelInstall.refreshPrev` | re-pinned; basis now says it reaches `notePrev`, and through it `judgePrevWith`/`judgeSeedsPrev` (or the root moves floor judge on a census box), which can open the registry as root when a legacy local floor exists | F,J4 | uid 0 at the box's attended root shell | |
| +| `internal/host/installchannel_linux.go` `openChannelPolicy` | new; read sites `file:Open`, `file:Stat`, `file:ReadAll` | F,J4 | uid 0 at the box's attended root shell | |
| +| `internal/host/installchannel.go` `readChannelPolicy` | `@remove`: its read moved into `openChannelPolicy` | | | |
| +| `completePrev`, `prevRecorded`, `newChannelInstall`, `install` | no read site of their own | envelope | the same root shell | |
| + |
| +The internal/host envelope is re-pinned. The seal stands. No 9B8 entry: no new bucket writer. (On a census box, verifyPointer remembers the channel identity in root's guarded identities, as the update already does; that is a local root-state write, not a bucket write.) |
diff --git a/internal/host/installchannel.go b/internal/host/installchannel.go| index 5d661b1f6..c7860a94f 100644 |
| --- a/internal/host/installchannel.go |
| +++ b/internal/host/installchannel.go |
| @@ -114,23 +114,16 @@ func RunSystemInstallChannel(args []string) { |
| fmt.Fprintln(os.Stderr, fmt.Sprintf("%s=%q is not a channel, so nothing was installed", systemChannelEnv, invalid)) |
| os.Exit(1) |
| } |
| - in := channelInstall{ |
| - a: &systemApplier{ops: productionSysOps(), openStore: openSystemStore, channel: channel, optDir: release.OptDir(), |
| - dataDir: dataDir(), etcDir: "/etc", build: version.Build, platform: runtime.GOOS + "-" + runtime.GOARCH}, |
| - bucket: os.Getenv("CARLOS_DEPLOYMENT_BUCKET"), |
| - fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error { |
| - return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha) |
| - }, |
| - own: ownByService, |
| - } |
| + var policy []byte |
| if *policyPath != "" { |
| raw, err := readChannelPolicy(*policyPath) |
| if err != nil { |
| fmt.Fprintln(os.Stderr, err) |
| os.Exit(1) |
| } |
| - in.policy = raw |
| + policy = raw |
| } |
| + in := newChannelInstall(channel, policy) |
| got, err := in.run(context.Background()) |
| if err != nil { |
| fmt.Fprintln(os.Stderr, err) |
| @@ -139,17 +132,49 @@ func RunSystemInstallChannel(args []string) { |
| fmt.Println(installedLine(got)) |
| } |
| +// channelPolicyLimit caps --policy's file: sign-policy writes about 500 |
| +// bytes. |
| +const channelPolicyLimit = 64 << 10 |
| + |
| +// errChannelPolicy is --policy's one refusal of the policy itself, said |
| +// alike before and after its signature is checked. |
| +var errChannelPolicy = errors.New("the --policy file is not an armed policy signed for this deployment, so nothing was installed") |
| + |
| +// newChannelInstall is the production run. The --policy path refreshes |
| +// carlos.prev as an update does, so its applier reads the root |
| +// participation census as the update's does (systemapply.go): on a census |
| +// box it takes the root moves floor branch, where a floor step that fails |
| +// fails (fix round 1, I2). Without --policy the applier is as it was. |
| +func newChannelInstall(channel string, policy []byte) channelInstall { |
| + in := channelInstall{ |
| + a: &systemApplier{ops: productionSysOps(), openStore: openSystemStore, channel: channel, optDir: release.OptDir(), |
| + dataDir: dataDir(), etcDir: "/etc", build: version.Build, platform: runtime.GOOS + "-" + runtime.GOARCH}, |
| + bucket: os.Getenv("CARLOS_DEPLOYMENT_BUCKET"), |
| + fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error { |
| + return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha) |
| + }, |
| + own: ownByService, |
| + policy: policy, |
| + } |
| + if policy != nil { |
| + in.a.rootMoves = rootMovesFloorIdentity |
| + } |
| + return in |
| +} |
| + |
| // readChannelPolicy reads --policy's file and refuses, before anything else |
| -// is read, a file that is not an armed policy at all. Its signature is |
| +// is read, a file that is not an armed policy at all. It is opened without |
| +// following a symlink or waiting on a FIFO, and must be a regular file of |
| +// at most channelPolicyLimit bytes (openChannelPolicy). Its signature is |
| // checked once the release key is known (channelPolicy). |
| func readChannelPolicy(path string) ([]byte, error) { |
| - raw, err := os.ReadFile(path) |
| + raw, err := openChannelPolicy(path, channelPolicyLimit) |
| if err != nil { |
| return nil, fmt.Errorf("the policy %s could not be read, so nothing was installed: %w", path, err) |
| } |
| var p hostfloor.Policy |
| if json.Unmarshal(raw, &p) != nil || p.Mode != "armed" { |
| - return nil, fmt.Errorf("%s is not an armed policy signed for this deployment, so nothing was installed", path) |
| + return nil, errChannelPolicy |
| } |
| return raw, nil |
| } |
| @@ -215,7 +240,7 @@ func (in channelInstall) install(ctx context.Context) (channelInstalled, error) |
| if in.policy != nil { |
| pol, f, err := channelPolicy(in.policy, pub, in.bucket) |
| if err != nil { |
| - return got, errors.New("the --policy file is not an armed policy signed for this deployment, so nothing was installed") |
| + return got, errChannelPolicy |
| } |
| policy, policyFloor = &pol, f |
| } |
| @@ -241,6 +266,14 @@ func (in channelInstall) install(ctx context.Context) (channelInstalled, error) |
| if err != nil { |
| return got, err |
| } |
| + // With --policy, a box whose update has not finished (an open journal, |
| + // or an update-begun marker beside the guard) is refused, as guard-arm |
| + // refuses it: recovery owns carlos.prev until then (fix round 1, I1). |
| + if policy != nil { |
| + if err := guardArmIdle(systemGuardDir(a.optDir), systemRootDir(a.optDir)); err != nil { |
| + return got, fmt.Errorf("an update on this box has not finished, so nothing was installed: %w", err) |
| + } |
| + } |
| already := systemCacheHolds(systemBinaryPath(a.optDir), art.SHA256) && systemCacheHolds(systemPrevPath(a.optDir), art.SHA256) && |
| systemCacheHolds(channelIdentityPath(a.optDir), hostfloor.Digest(wire)) |
| if applied != nil { |
| @@ -248,6 +281,14 @@ func (in channelInstall) install(ctx context.Context) (channelInstalled, error) |
| // either this install, complete, or a box an update has since |
| // managed: its releases come through carlos system update. |
| if already { |
| + // With --policy, "already installed" also needs carlos.prev's |
| + // version record to name this release; one a cut-off run left |
| + // missing or stale is completed first, or the run fails. |
| + if policy != nil && !prevRecorded(a.optDir, p.Current.Version) { |
| + if err := in.completePrev(ctx, p.Current.Version, false); err != nil { |
| + return got, err |
| + } |
| + } |
| got.Already = true |
| return got, nil |
| } |
| @@ -298,6 +339,13 @@ func (in channelInstall) install(ctx context.Context) (channelInstalled, error) |
| return got, fmt.Errorf("%s does not hold %s after the copy", slot, p.Current.Version) |
| } |
| } |
| + // With --policy, carlos.prev's version record is part of the install |
| + // (completePrev); without it the install is exactly as it was. |
| + if policy != nil { |
| + if err := in.completePrev(ctx, p.Current.Version, false); err != nil { |
| + return got, err |
| + } |
| + } |
| if err := hostfloor.Durable(channelIdentityPath(a.optDir), wire, 0o644); err != nil { |
| return got, err |
| } |
| @@ -385,6 +433,49 @@ func policyFloorPublished(ctx context.Context, s release.Store, policy hostfloor |
| return nil |
| } |
| +// channelPrevVersionWrite is writePrevVersion, a seam for the tests that |
| +// the record's write failing fails the --policy install. |
| +var channelPrevVersionWrite = writePrevVersion |
| + |
| +// prevRecorded reports whether carlos.prev's version record names version. |
| +func prevRecorded(optDir, version string) bool { |
| + v, ok := readLine(prevVersionPath(optDir)) |
| + return ok && v == version |
| +} |
| + |
| +// completePrev is --policy's refresh of carlos.prev's records, the steps |
| +// an update's notePrev takes but with every one mandatory, armed floor or |
| +// not (prev-rehearsal fix round 1): under the prev lock, the verdicts and |
| +// the version record of what carlos.prev held are removed, carlos is |
| +// copied over it when copyCurrent is set, and the version record is |
| +// written and read back. Any failure fails the run, which then keeps no |
| +// identity and reports nothing done; a retry completes it. The floor's |
| +// judgement follows as an update makes it (notePrev without a replace): |
| +// a judgement that cannot be made leaves no verdict, which refuses. |
| +func (in channelInstall) completePrev(ctx context.Context, version string, copyCurrent bool) error { |
| + a := in.a |
| + err := withPrevLock(a.optDir, func() error { |
| + for _, path := range []string{prevAllowedPath(a.optDir), seedsPrevAllowedPath(a.optDir), prevVersionPath(a.optDir)} { |
| + if err := removeDurable(path); err != nil { |
| + return err |
| + } |
| + } |
| + if copyCurrent { |
| + if err := copyFileDurable(systemBinaryPath(a.optDir), systemPrevPath(a.optDir), 0o755); err != nil { |
| + return err |
| + } |
| + } |
| + return channelPrevVersionWrite(a.optDir, version) |
| + }) |
| + if err == nil && !prevRecorded(a.optDir, version) { |
| + err = fmt.Errorf("%s does not name %s", prevVersionPath(a.optDir), version) |
| + } |
| + if err != nil { |
| + return fmt.Errorf("the record of the release in carlos.prev could not be written, so the install is not complete. run it again: %w", err) |
| + } |
| + return a.notePrev(ctx, false, "") |
| +} |
| + |
| // refreshPrev is --policy's one admission of an updated box (applied.json |
| // names the channel release, and the slots and the identity are not yet |
| // install-channel's): carlos must already hold the channel release's |
| @@ -392,7 +483,8 @@ func policyFloorPublished(ctx context.Context, s release.Store, policy hostfloor |
| // (slotState). Its carlos.prev, the release the update replaced, is then |
| // refreshed from carlos the way an update refreshes it (notePrev: the |
| // sidecar and verdicts of the old carlos.prev removed first, the copy, the |
| -// version sidecar, the floor's judgement), and the identity is kept beside |
| +// version sidecar, the floor's judgement), every step mandatory here |
| +// (completePrev), and only then is the identity kept beside |
| // them. Nothing is fetched, and applied.json and seen.json are the |
| // update's and stay as they are. The caller has already refused an armed |
| // guard, another release and a release below the policy's floor. |
| @@ -427,10 +519,13 @@ func (in channelInstall) refreshPrev(ctx context.Context, p *release.SystemPoint |
| } |
| } |
| // Always, even over a carlos.prev a cut-off run already copied, so the |
| - // version sidecar is this release's. |
| - if err := a.notePrev(ctx, true, p.Current.Version); err != nil { |
| + // version record is this release's. The record is mandatory here. |
| + if err := in.completePrev(ctx, p.Current.Version, true); err != nil { |
| return got, err |
| } |
| + // The replaced line names a release that was there; a retry that found |
| + // carlos.prev already this release, or none, says only what is installed. |
| + replaced := held == slotForeign |
| for _, slot := range []string{prev, current} { |
| if state, err := slotState(slot, art.SHA256); err != nil || state != slotHolds { |
| return got, fmt.Errorf("%s does not hold %s after the copy", slot, p.Current.Version) |
| @@ -439,7 +534,7 @@ func (in channelInstall) refreshPrev(ctx context.Context, p *release.SystemPoint |
| if err := hostfloor.Durable(channelIdentityPath(a.optDir), wire, 0o644); err != nil { |
| return got, err |
| } |
| - got.Replaced = true |
| + got.Replaced = replaced |
| return got, nil |
| } |
diff --git a/internal/host/installchannel_linux.go b/internal/host/installchannel_linux.go| index cabf7fef9..a4d468b6d 100644 |
| --- a/internal/host/installchannel_linux.go |
| +++ b/internal/host/installchannel_linux.go |
| @@ -6,6 +6,7 @@ import ( |
| "crypto/sha256" |
| "encoding/hex" |
| "errors" |
| + "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| @@ -37,6 +38,37 @@ func ownDir(path string, uid, gid int) error { |
| return unix.Fchown(fd, uid, gid) |
| } |
| +// openChannelPolicy reads --policy's file: opened with O_NOFOLLOW (a |
| +// symlink is refused, never followed) and O_NONBLOCK (a FIFO never blocks |
| +// the open), fstat'd to be a regular file of at most limit bytes, and only |
| +// then read, from the descriptor, up to limit. |
| +func openChannelPolicy(path string, limit int64) ([]byte, error) { |
| + fd, err := unix.Open(path, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0) |
| + if err != nil { |
| + return nil, err |
| + } |
| + f := os.NewFile(uintptr(fd), path) |
| + defer f.Close() |
| + st, err := f.Stat() |
| + if err != nil { |
| + return nil, err |
| + } |
| + if !st.Mode().IsRegular() { |
| + return nil, errors.New("not a regular file") |
| + } |
| + if st.Size() > limit { |
| + return nil, fmt.Errorf("larger than %d bytes", limit) |
| + } |
| + raw, err := io.ReadAll(io.LimitReader(f, limit+1)) |
| + if err != nil { |
| + return nil, err |
| + } |
| + if int64(len(raw)) > limit { |
| + return nil, fmt.Errorf("larger than %d bytes", limit) |
| + } |
| + return raw, nil |
| +} |
| + |
| // 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 |
diff --git a/internal/host/installchannel_other.go b/internal/host/installchannel_other.go| index 4ddf4eaa7..46797b801 100644 |
| --- a/internal/host/installchannel_other.go |
| +++ b/internal/host/installchannel_other.go |
| @@ -15,3 +15,5 @@ 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 } |
| + |
| +func openChannelPolicy(path string, limit int64) ([]byte, error) { return nil, errOwnNotLinux } |
diff --git a/internal/host/installchannel_policy_rootrunner_linux_test.go b/internal/host/installchannel_policy_rootrunner_linux_test.go| new file mode 100644 |
| index 000000000..b19a094d0 |
| --- /dev/null |
| +++ b/internal/host/installchannel_policy_rootrunner_linux_test.go |
| @@ -0,0 +1,77 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "context" |
| + "encoding/base64" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostfloor" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// Fix round 1, I2, as real root: a box with a root participation census |
| +// (leo's kind) and an unarmed guard, updated from v1.3.0 to v1.4.0 by the |
| +// real apply on the census branch, then armed for guard-arm by |
| +// install-channel --policy on that same branch. The release before is |
| +// replaced, carlos.prev's record names the release, the identity is kept, |
| +// and the root moves floor's judgement ran over the new carlos.prev. |
| +func TestRootInstallChannelPolicyCensusBox(t *testing.T) { |
| + scratch := generationRootRunnerDir(t) |
| + t.Setenv("TMPDIR", scratch) |
| + f := &channelFixture{applyFixture: newApplyFixture(t, "stable", "v1.3.0")} |
| + for _, dir := range []string{f.a.optDir, f.a.dataDir, f.a.etcDir} { |
| + for path := dir; path != scratch && path != "/" && path != filepath.Dir(scratch); path = filepath.Dir(path) { |
| + if err := os.Chmod(path, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| + t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(f.pub)) |
| + t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "bucket") |
| + id := rootFloorIdentity{Deployment: "bucket", Box: "box", Enrollment: "en"} |
| + f.a.rootMoves = func() (rootFloorIdentity, bool, error) { return id, true, nil } |
| + old := []byte("#!/bin/sh\necho carlos v1.3.0\n") |
| + rootFloorSignRunnerBytes(t, f.a.optDir, systemBinaryPath(f.a.optDir), "v1.3.0", old, f.priv) |
| + f.body = []byte("#!/bin/sh\necho carlos v1.4.0\n") |
| + applyChannelUpdate(t, f, "v1.4.0", f.body, true) |
| + if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, old) { |
| + t.Fatalf("the census update left %q and %q", cur, prev) |
| + } |
| + f.in = channelInstall{a: f.a, bucket: "bucket", fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error { |
| + f.fetches++ |
| + return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha) |
| + }, own: func(string) error { return nil }} |
| + f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) |
| + got, err := f.in.run(context.Background()) |
| + if err != nil || !got.Replaced { |
| + t.Fatalf("install-channel --policy on a census box: %+v %v", got, err) |
| + } |
| + if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) || !prevRecorded(f.a.optDir, "v1.4.0") || f.fetches != 0 { |
| + t.Fatalf("slots %q %q", cur, prev) |
| + } |
| + key, _ := release.IdentityKey("v1.4.0") |
| + published, _, err := f.store.Get(context.Background(), key) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if held, err := os.ReadFile(channelIdentityPath(f.a.optDir)); err != nil || !bytes.Equal(held, published) { |
| + t.Fatal("identity", err) |
| + } |
| + floors, err := openRootMovesFloors(f.a.optDir, id, false) |
| + if err != nil { |
| + t.Fatal("the census box's root floor state", err) |
| + } |
| + defer floors.close() |
| + if verdict, err := floors.public.Read("prev.json"); err != nil || len(verdict) == 0 { |
| + t.Fatal("the root moves floor did not judge the new carlos.prev", err) |
| + } |
| + if again, err := f.in.run(context.Background()); err != nil || !again.Already { |
| + t.Fatalf("rerun %+v %v", again, err) |
| + } |
| + t.Log("PASS install-channel --policy on a census box: root moves branch, mandatory record, identity kept") |
| +} |
diff --git a/internal/host/installchannel_policy_test.go b/internal/host/installchannel_policy_test.go| index 6929f7f58..607e67814 100644 |
| --- a/internal/host/installchannel_policy_test.go |
| +++ b/internal/host/installchannel_policy_test.go |
| @@ -9,6 +9,7 @@ import ( |
| "os" |
| "path/filepath" |
| "strings" |
| + "syscall" |
| "testing" |
| "github.com/carlosframework/platform/internal/hostfloor" |
| @@ -33,51 +34,70 @@ func updatedChannelBox(t *testing.T) (f *channelFixture, old []byte) { |
| t.Fatal(err) |
| } |
| f.body = []byte("#!/bin/sh\necho carlos v1.4.0\n") |
| - f.publish(t, "v1.4.0", f.body) |
| + applyChannelUpdate(t, f, "v1.4.0", f.body, true) |
| + if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, old) { |
| + t.Fatalf("the update left %q and %q", cur, prev) |
| + } |
| + if _, err := os.Stat(channelIdentityPath(f.a.optDir)); !os.IsNotExist(err) { |
| + t.Fatalf("an update wrote a channel identity: %v", err) |
| + } |
| + f.in = channelInstall{a: f.a, bucket: "bucket", fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error { |
| + f.fetches++ |
| + return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha) |
| + }, own: func(path string) error { |
| + f.owned = append(f.owned, path) |
| + return nil |
| + }} |
| + return f, old |
| +} |
| + |
| +// applyChannelUpdate moves f's box to version through the platform's update |
| +// path: the promote (and, with identity, the release identity), the agent's |
| +// fetch and request (systemLeg), then the real apply. |
| +func applyChannelUpdate(t *testing.T, f *channelFixture, version string, body []byte, identity bool) { |
| + t.Helper() |
| ctx := context.Background() |
| + if identity { |
| + f.publish(t, version, body) |
| + } else { |
| + rel := release.SystemRelease{Version: version, ReleaseTag: version, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{ |
| + "linux-arm64": {Key: release.SystemArtifactKey(version, "linux-arm64"), SHA256: hostfloor.Digest(body)}}} |
| + if _, err := release.PromoteSystem(ctx, f.store, release.RootSigner(f.priv), "router", "stable", rel, release.SystemPromoteOptions{}); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| p, _, err := release.GetSystemPointer(ctx, f.store, f.pub, release.SystemComponentRouter, "stable") |
| if err != nil { |
| t.Fatal(err) |
| } |
| - // The agent's half of the update (systemLeg.fetch and writeRequest). |
| leg := &systemLeg{dataDir: f.a.dataDir, fetchTo: func(ctx context.Context, key, dst, sha string) error { |
| return release.FetchToFile(ctx, f.store, key, dst, systemArtifactLimit, sha) |
| }} |
| - pending := &systemPending{version: "v1.4.0", promotedAt: p.PromotedAt, artifact: p.Current.Artifacts["linux-arm64"]} |
| + pending := &systemPending{version: version, promotedAt: p.PromotedAt, artifact: p.Current.Artifacts["linux-arm64"]} |
| if err := leg.fetch(ctx, pending); err != nil { |
| t.Fatal(err) |
| } |
| if err := leg.writeRequest(pending, f.now); err != nil { |
| t.Fatal(err) |
| } |
| - staged := filepath.Join(f.a.stagingDir(), "carlos") |
| - f.answer["plan:"+staged] = f.answer["plan:"+systemBinaryPath(f.a.optDir)] |
| - f.answer["units:"+staged] = f.answer["units:"+systemBinaryPath(f.a.optDir)] |
| - prev := systemPrevPath(f.a.optDir) |
| - f.answer["plan:"+prev] = f.answer["plan:"+systemBinaryPath(f.a.optDir)] |
| - f.answer["units:"+prev] = f.answer["units:"+systemBinaryPath(f.a.optDir)] |
| - // Every binary answers `version` from its own bytes. |
| - delete(f.answer, "version:"+systemBinaryPath(f.a.optDir)) |
| + // The candidate ships the instance layout's mount, so the layout guard |
| + // does not open the registry (a post-layout release, as every one the |
| + // trial installs is). |
| + plan, units := `{"enabled":["carlos-edge.service","carlos-adopt.timer","carlos-adopt.path","carlos-stage.path","carlos-restart.path","carlos-system-update.path","carlos-system-recover.service"],"inert":["carlos-restore-verify.timer"]}`, "carlos-edge.service carlos-adopt.timer carlos-adopt.path carlos-stage.path carlos-restart.path carlos-system-update.path carlos-system-update.service carlos-system-recover.service carlos-instance-control.service data-instances.mount" |
| + for _, path := range []string{filepath.Join(f.a.stagingDir(), "carlos"), systemPrevPath(f.a.optDir), systemBinaryPath(f.a.optDir)} { |
| + f.answer["plan:"+path], f.answer["units:"+path] = plan, units |
| + // Every binary answers `version` from its own bytes. |
| + delete(f.answer, "version:"+path) |
| + } |
| if err := f.a.Run(ctx, false); err != nil { |
| t.Fatal(err) |
| } |
| - if r := f.result(); r == nil || r.Outcome != systemOutcomeApplied { |
| - t.Fatalf("the update did not apply: %+v", r) |
| + if r := f.result(); r == nil || r.Outcome != systemOutcomeApplied || r.Version != version { |
| + t.Fatalf("the update to %s did not apply: %+v", version, r) |
| } |
| - if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, old) { |
| - t.Fatalf("the update left %q and %q", cur, prev) |
| - } |
| - if _, err := os.Stat(channelIdentityPath(f.a.optDir)); !os.IsNotExist(err) { |
| - t.Fatalf("an update wrote a channel identity: %v", err) |
| + if cur, _ := os.ReadFile(systemBinaryPath(f.a.optDir)); !bytes.Equal(cur, body) { |
| + t.Fatalf("the update left %q", cur) |
| } |
| - f.in = channelInstall{a: f.a, bucket: "bucket", fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error { |
| - f.fetches++ |
| - return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha) |
| - }, own: func(path string) error { |
| - f.owned = append(f.owned, path) |
| - return nil |
| - }} |
| - return f, old |
| } |
| // signedPolicy is what claims run sign-policy writes for floor. |
| @@ -298,6 +318,45 @@ func TestInstallChannelPolicyRefusals(t *testing.T) { |
| t.Fatal(err) |
| } |
| }, "this box's install record could not be read, so nothing was installed: unexpected end of JSON input"}, |
| + {"a carlos.prev that is a directory", func(t *testing.T, f *channelFixture) { |
| + f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) |
| + if err := os.Remove(systemPrevPath(f.a.optDir)); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(systemPrevPath(f.a.optDir), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + }, "%opt%/carlos.prev holds a build this command did not install, so nothing was installed"}, |
| + {"an unreadable guard", func(t *testing.T, f *channelFixture) { |
| + f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) |
| + if err := os.MkdirAll(systemGuardDir(f.a.optDir), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(systemGuardDir(f.a.optDir), "policy.json"), []byte("{"), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + }, "this box's host lifecycle guard could not be read, so nothing was installed: hostfloor: authenticated floor required"}, |
| + {"an open update journal", func(t *testing.T, f *channelFixture) { |
| + f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) |
| + if err := os.WriteFile(f.a.file("journal.json"), []byte("{}"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + }, "an update on this box has not finished, so nothing was installed: hostfloor: authenticated floor required: %opt%/system/router/journal.json is open; let the update finish or recover first"}, |
| + {"an update-begun marker", func(t *testing.T, f *channelFixture) { |
| + f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) |
| + if err := os.MkdirAll(systemGuardDir(f.a.optDir), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(systemGuardDir(f.a.optDir), updateBegunFile), []byte("{}"), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + }, "an update on this box has not finished, so nothing was installed: hostfloor: authenticated floor required: %opt%/system/guard/" + updateBegunFile + " is present; let the update finish or recover first"}, |
| + {"a census that cannot be read", func(t *testing.T, f *channelFixture) { |
| + f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) |
| + f.a.rootMoves = func() (rootFloorIdentity, bool, error) { |
| + return rootFloorIdentity{}, true, errors.New("census unreadable") |
| + } |
| + }, "the stable channel could not be read, so nothing was installed: census unreadable"}, |
| } { |
| t.Run(c.name, func(t *testing.T) { |
| f, _ := updatedChannelBox(t) |
| @@ -340,6 +399,10 @@ func TestInstallChannelPolicyInstallsAFreshBoxBeforeTheFloor(t *testing.T) { |
| if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) || f.fetches != 1 { |
| t.Fatalf("slots %q %q after %d fetches", cur, prev, f.fetches) |
| } |
| + // With --policy carlos.prev's version record is part of the install. |
| + if !prevRecorded(f.a.optDir, "v1.4.0") { |
| + t.Fatal("the fresh install kept no version record for carlos.prev") |
| + } |
| if got, err := f.in.run(ctx); err != nil || !got.Already { |
| t.Fatalf("again %+v %v", got, err) |
| } |
| @@ -354,6 +417,113 @@ func TestInstallChannelPolicyInstallsAFreshBoxBeforeTheFloor(t *testing.T) { |
| } |
| } |
| +// failPrevRecord makes completePrev's write of carlos.prev's version record |
| +// fail until the returned function restores it. |
| +func failPrevRecord(t *testing.T) func() { |
| + t.Helper() |
| + real := channelPrevVersionWrite |
| + channelPrevVersionWrite = func(string, string) error { return errors.New("disk full") } |
| + restore := func() { channelPrevVersionWrite = real } |
| + t.Cleanup(restore) |
| + return restore |
| +} |
| + |
| +const prevRecordRefusal = "the record of the release in carlos.prev could not be written, so the install is not complete. run it again: disk full" |
| + |
| +// Fix round 1: the version record of carlos.prev is mandatory in the |
| +// --policy refresh, armed floor or not. A run whose record cannot be |
| +// written fails, keeps no identity and reports nothing done; the retry |
| +// completes it. |
| +func TestInstallChannelPolicyFailsWhenThePrevRecordCannotBeWritten(t *testing.T) { |
| + f, _ := updatedChannelBox(t) |
| + ctx := context.Background() |
| + f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) |
| + restore := failPrevRecord(t) |
| + got, err := f.in.run(ctx) |
| + if err == nil || err.Error() != prevRecordRefusal || got.Replaced || got.Already { |
| + t.Fatalf("a failed record: %+v %v", got, err) |
| + } |
| + if _, err := os.Stat(channelIdentityPath(f.a.optDir)); !os.IsNotExist(err) { |
| + t.Fatalf("a failed record kept the identity: %v", err) |
| + } |
| + if _, ok := readLine(prevVersionPath(f.a.optDir)); ok { |
| + t.Fatal("a failed record left a version record") |
| + } |
| + // Still failing: never "already installed". |
| + if got, err := f.in.run(ctx); err == nil || err.Error() != prevRecordRefusal || got.Already { |
| + t.Fatalf("the second failed record: %+v %v", got, err) |
| + } |
| + restore() |
| + // The failed run had already copied carlos.prev, so the retry replaces |
| + // nothing and its line says only what is installed (M4). |
| + got, err = f.in.run(ctx) |
| + if err != nil || got.Replaced || got.Already || installedLine(got) != "installed v1.4.0 (v1.4.0) from the stable channel as carlos and carlos.prev" { |
| + t.Fatalf("the retry: %+v %v", got, err) |
| + } |
| + if !prevRecorded(f.a.optDir, "v1.4.0") { |
| + t.Fatal("the retry did not record carlos.prev") |
| + } |
| + if _, err := os.Stat(channelIdentityPath(f.a.optDir)); err != nil { |
| + t.Fatal(err) |
| + } |
| + if again, err := f.in.run(ctx); err != nil || !again.Already { |
| + t.Fatalf("after the retry: %+v %v", again, err) |
| + } |
| +} |
| + |
| +// Fix round 1, the interruption with a matching identity already present: |
| +// install A, update to B, update back to A, and the --policy refresh cut |
| +// off after carlos.prev is copied and before its record is written. Both |
| +// slots and the identity then match A, which the shortcut alone would call |
| +// "already installed"; the retry completes the record instead. |
| +func TestInstallChannelPolicyCompletesACutOffRefreshOverAMatchingIdentity(t *testing.T) { |
| + f := newChannelFixture(t, "") |
| + ctx := context.Background() |
| + f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) |
| + if got, err := f.in.run(ctx); err != nil || got.Already || got.Replaced { |
| + t.Fatalf("install A: %+v %v", got, err) |
| + } |
| + identityA, err := os.ReadFile(channelIdentityPath(f.a.optDir)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + b := []byte("#!/bin/sh\necho carlos v1.5.0\n") |
| + applyChannelUpdate(t, f, "v1.5.0", b, true) |
| + applyChannelUpdate(t, f, "v1.4.0", f.body, false) |
| + if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, b) || !prevRecorded(f.a.optDir, "v1.5.0") { |
| + t.Fatalf("after the updates: %q %q", cur, prev) |
| + } |
| + // The cut-off refresh: carlos.prev copied, its record not written. |
| + failPrevRecord(t) |
| + if got, err := f.in.run(ctx); err == nil || err.Error() != prevRecordRefusal || got.Already || got.Replaced { |
| + t.Fatalf("the cut-off refresh: %+v %v", got, err) |
| + } |
| + if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) { |
| + t.Fatal("the cut-off refresh did not copy carlos.prev") |
| + } |
| + if held, _ := os.ReadFile(channelIdentityPath(f.a.optDir)); !bytes.Equal(held, identityA) { |
| + t.Fatal("the identity is not A's") |
| + } |
| + if _, ok := readLine(prevVersionPath(f.a.optDir)); ok { |
| + t.Fatal("the cut-off refresh left a version record") |
| + } |
| + // Still failing: the shortcut does not answer over it. |
| + if got, err := f.in.run(ctx); err == nil || err.Error() != prevRecordRefusal || got.Already { |
| + t.Fatalf("over incomplete metadata: %+v %v", got, err) |
| + } |
| + channelPrevVersionWrite = writePrevVersion |
| + got, err := f.in.run(ctx) |
| + if err != nil || !got.Already { |
| + t.Fatalf("the retry: %+v %v", got, err) |
| + } |
| + if !prevRecorded(f.a.optDir, "v1.4.0") { |
| + t.Fatal("the retry did not complete carlos.prev's record") |
| + } |
| + if f.fetches != 1 { |
| + t.Fatalf("%d fetches", f.fetches) |
| + } |
| +} |
| + |
| // --policy's file is refused before anything else is read when it cannot |
| // be read or is not an armed policy at all. |
| func TestInstallChannelPolicyFileIsReadUpFront(t *testing.T) { |
| @@ -366,10 +536,37 @@ func TestInstallChannelPolicyFileIsReadUpFront(t *testing.T) { |
| if err := os.WriteFile(missing, []byte(body), 0o600); err != nil { |
| t.Fatal(err) |
| } |
| - if _, err := readChannelPolicy(missing); err == nil || err.Error() != missing+" is not an armed policy signed for this deployment, so nothing was installed" { |
| + if _, err := readChannelPolicy(missing); err == nil || err.Error() != "the --policy file is not an armed policy signed for this deployment, so nothing was installed" { |
| t.Fatalf("%q: %v", body, err) |
| } |
| } |
| + // M3: a symlink, a FIFO, a directory and an oversized file are refused |
| + // before anything is read; the FIFO's open does not wait for a writer. |
| + raw0 := signedPolicyBytes(t) |
| + real := filepath.Join(dir, "real.json") |
| + if err := os.WriteFile(real, raw0, 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + link := filepath.Join(dir, "link.json") |
| + if err := os.Symlink(real, link); err != nil { |
| + t.Fatal(err) |
| + } |
| + fifo := filepath.Join(dir, "fifo.json") |
| + if err := syscall.Mkfifo(fifo, 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + big := filepath.Join(dir, "big.json") |
| + if err := os.WriteFile(big, append(raw0, bytes.Repeat([]byte(" "), channelPolicyLimit)...), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, path := range []string{link, fifo, dir, big} { |
| + if _, err := readChannelPolicy(path); err == nil || !strings.HasPrefix(err.Error(), "the policy "+path+" could not be read, so nothing was installed: ") { |
| + t.Fatalf("%s: %v", path, err) |
| + } |
| + } |
| + if got, err := readChannelPolicy(real); err != nil || !bytes.Equal(got, raw0) { |
| + t.Fatalf("the real file: %v", err) |
| + } |
| _, key, _ := ed25519.GenerateKey(nil) |
| raw := signedPolicy(t, key, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) |
| if err := os.WriteFile(missing, raw, 0o600); err != nil { |
| @@ -380,6 +577,62 @@ func TestInstallChannelPolicyFileIsReadUpFront(t *testing.T) { |
| } |
| } |
| +// signedPolicyBytes is a well-formed armed policy under a throwaway key. |
| +func signedPolicyBytes(t *testing.T) []byte { |
| + t.Helper() |
| + _, key, _ := ed25519.GenerateKey(nil) |
| + return signedPolicy(t, key, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) |
| +} |
| + |
| +// An updated box with no carlos.prev at all gets the release in it and its |
| +// record; nothing was replaced, so the line says what is installed. |
| +func TestInstallChannelPolicyFillsAnAbsentPrev(t *testing.T) { |
| + f, _ := updatedChannelBox(t) |
| + if err := os.Remove(systemPrevPath(f.a.optDir)); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) |
| + got, err := f.in.run(context.Background()) |
| + if err != nil || got.Replaced || got.Already { |
| + t.Fatalf("%+v %v", got, err) |
| + } |
| + if line := installedLine(got); line != "installed v1.4.0 (v1.4.0) from the stable channel as carlos and carlos.prev" { |
| + t.Fatalf("line %q", line) |
| + } |
| + if _, prev := f.slots(t); !bytes.Equal(prev, f.body) || !prevRecorded(f.a.optDir, "v1.4.0") || f.fetches != 0 { |
| + t.Fatalf("carlos.prev %q", prev) |
| + } |
| +} |
| + |
| +// I2: on a box with a root participation census, the --policy path takes |
| +// the update's root moves floor branch. Here, not root, the branch cannot |
| +// keep the release's identity in root's guarded state, so the run refuses |
| +// with the box unchanged, where the census-less branch would go on. The |
| +// production constructor sets the census source only with --policy. |
| +func TestInstallChannelPolicyTakesTheCensusBranch(t *testing.T) { |
| + if os.Geteuid() == 0 { |
| + t.Skip("the root branch succeeds as root; TestRootInstallChannelPolicyCensusBox covers it") |
| + } |
| + f, _ := updatedChannelBox(t) |
| + f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) |
| + f.a.rootMoves = func() (rootFloorIdentity, bool, error) { |
| + return rootFloorIdentity{Deployment: "bucket", Box: "box", Enrollment: "en"}, true, nil |
| + } |
| + before := boxFiles(t, f) |
| + if _, err := f.in.run(context.Background()); err == nil || !strings.HasPrefix(err.Error(), "the stable channel could not be read, so nothing was installed: ") { |
| + t.Fatalf("a census box off root: %v", err) |
| + } |
| + if after := boxFiles(t, f); !equalFiles(before, after) { |
| + t.Fatalf("changed: %v", diffFiles(before, after)) |
| + } |
| + if newChannelInstall("stable", nil).a.rootMoves != nil { |
| + t.Fatal("the no-policy applier reads the census") |
| + } |
| + if newChannelInstall("stable", []byte("{}")).a.rootMoves == nil { |
| + t.Fatal("the --policy applier does not read the census") |
| + } |
| +} |
| + |
| // floorUnreadable fails every read of the host floor's two objects. |
| type floorUnreadable struct{ release.Store } |
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go| index a6f1f682a..2bebbf108 100644 |
| --- a/internal/provision/plan14_root_reads_test.go |
| +++ b/internal/provision/plan14_root_reads_test.go |
| @@ -866,6 +866,14 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal.tsv"), &inv, true); err != nil { |
| t.Fatal(err) |
| } |
| + // Census overlay claim-prev-rehearsal-fix1 (fix round 1: mandatory |
| + // carlos.prev record, unfinished-update refusal, the census branch for |
| + // --policy, the hardened policy read), loaded last. PROPOSED |
| + // (docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md, |
| + // "Fix round 1"). It adds no accepted-legacy row, so the seal stands. |
| + if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal-fix1.tsv"), &inv, true); err != nil { |
| + t.Fatal(err) |
| + } |
| errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...) |
| errs = append(errs, checkRootReadDeclarations(got, inv)...) |
| if len(errs) > 0 { |
diff --git a/internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal-fix1.tsv b/internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal-fix1.tsv| new file mode 100644 |
| index 000000000..460758745 |
| --- /dev/null |
| +++ b/internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal-fix1.tsv |
| @@ -0,0 +1,8 @@ |
| +# Census overlay claim-prev-rehearsal-fix1 (fix round 1: Astra's metadata finding and platform-00's I1, I2, M3, M5, as ruled by the controller), loaded LAST, after claim-prev-rehearsal. PROPOSED, not ruled: docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md, section "Fix round 1". |
| +# install-channel --policy only; without --policy nothing changes. carlos.prev's version record is mandatory in the --policy path (completePrev: under the prev lock the old verdicts and record are removed, carlos copied when refreshing, the record written and read back, every failure fatal, then the floor's judgement by notePrev without a replace), and the --policy "already installed" answer requires that record. An open update journal or update-begun marker refuses (guardArmIdle). The --policy applier reads the root participation census (rootMovesFloorIdentity), so a census box takes the update's root moves floor branch. The policy file is opened with O_NOFOLLOW and O_NONBLOCK, fstat'd regular and capped at 64 KiB (openChannelPolicy); readChannelPolicy keeps no read site of its own (@remove). |
| +# channelInstall.run's digest and class are unchanged; only its basis is corrected (M5). The seal stands: no accepted-legacy row. The 9B8 inventory is unchanged. |
| +@coverage internal/host ebc571e3abd07501ffcd2992ad2f2269c242b0dd5c792314662c6988a6264e34 |
| +@remove internal/host/installchannel.go readChannelPolicy |
| +internal/host/installchannel.go channelInstall.run authority F,J4 rev3=PROPOSED-claim-prev-rehearsal-fix1;basis=carlos system install-channel (root): at a fresh box's start-up, and with --policy also at an updated box's attended root shell before the host floor is published; its one read site opens root's apply lock; the install goes through verifyPointer, hostfloor.CheckImage (against the published floor, or with --policy the verified policy floor) and stageCandidate into both protected slots, or with --policy on an updated box at the channel head through refreshPrev; it refuses a box whose guard is armed or that already runs another release, and with --policy a box whose update has not finished file:OpenFile@196 f0cdb6dd28e83da282c3b0d7bc2012dcd80db181a8cecbf1a2db6bcc41d1d5e3 |
| +internal/host/installchannel.go channelInstall.refreshPrev authority F,J4 rev3=PROPOSED-claim-prev-rehearsal-fix1;identity=uid 0 at the box's attended root shell (carlos system install-channel --policy);basis=under the apply lock, after the armed-guard, unfinished-update, other-release and floor refusals: carlos must hash, through slotState's O_NOFOLLOW descriptor, to the channel release's signed artifact sha256; its one read site Lstats carlos.prev to refuse a symlink or anything but a regular file; completePrev then removes the old verdicts and version record, copies carlos over carlos.prev and writes and reads back the record, each step mandatory; it then reaches notePrev and through it the floor's judgement (judgePrevWith and judgeSeedsPrev, or with a root participation census the root moves floor judge), which can open the registry as root when a legacy local floor exists; both slots are re-hashed before the identity is kept; nothing is fetched or executed and applied.json and seen.json are not written file:Lstat@907 1426d3230768f721a7daf2eadd42590d69125ade16c81d6ff70fdb89543b2abe |
| +internal/host/installchannel_linux.go openChannelPolicy authority F,J4 rev3=PROPOSED-claim-prev-rehearsal-fix1;identity=uid 0 at the box's attended root shell (carlos system install-channel --policy);basis=reads the operator-staged --policy file once, before the apply lock or any store read: opened with O_NOFOLLOW (a symlink is refused) and O_NONBLOCK (a FIFO does not block), fstat'd to be a regular file of at most 64 KiB, then read from the descriptor up to that cap; the bytes are used only after channelPolicy verifies the root-signed moves-protocol authorization for this deployment's bucket under CARLOS_RELEASE_PUBKEY, and a published floor must equal them byte for byte file:Open@79,file:ReadAll@485,file:Stat@263 eb5725bd24aa51f5d4f93b01a065351a278bb82cbf013a106b2f1fba36987f94 |