| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "github.com/carlosframework/platform/internal/moves" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/rootparticipation" |
| 11 | "github.com/carlosframework/platform/internal/rootstate" |
| 12 | ) |
| 13 | |
| 14 | func rootFloorPrevVerdict(j rootFloorJudge) error { |
| 15 | p, err := j.floors.projection() |
| 16 | if err != nil { |
| 17 | if j.floors.private != nil { |
| 18 | if _, e := j.floors.record(rootFloorAccepted); e == nil { |
| 19 | return err |
| 20 | } |
| 21 | } |
| 22 | bound, e := j.floors.localBound() |
| 23 | if e != nil { |
| 24 | return e |
| 25 | } |
| 26 | b, e := j.binary(j.prev) |
| 27 | if e != nil { |
| 28 | return e |
| 29 | } |
| 30 | if !j.permits(b, bound) { |
| 31 | return errRootFloorHistory |
| 32 | } |
| 33 | return nil |
| 34 | } |
| 35 | raw, err := j.floors.public.Read("prev.json") |
| 36 | if err != nil { |
| 37 | return err |
| 38 | } |
| 39 | var v rootFloorVerdict |
| 40 | if len(raw) > movesFloorMaxBytes || rootparticipation.Decode(raw, &v) != nil || v.Identity != p.Identity || v.Generation != p.Generation || v.PrevAbsent { |
| 41 | return errRootFloorHistory |
| 42 | } |
| 43 | binary, err := j.binary(j.prev) |
| 44 | if err != nil { |
| 45 | return err |
| 46 | } |
| 47 | if !sameRootFloorImage(binary, v.Prev) || !j.permits(binary, p.Floor) { |
| 48 | return errRootFloorHistory |
| 49 | } |
| 50 | return nil |
| 51 | } |
| 52 | |
| 53 | func (j rootFloorJudge) permits(b rootFloorBinary, bound *moves.Floor) bool { |
| 54 | if !b.supports(nil) { |
| 55 | return false |
| 56 | } |
| 57 | if b.supports(bound) { |
| 58 | return true |
| 59 | } |
| 60 | paths := []string{j.current, j.prev} |
| 61 | if filepath.IsAbs(j.current) { |
| 62 | paths = append(paths, filepath.Join(systemRootDir(filepath.Dir(j.current)), "staging", "carlos")) |
| 63 | } |
| 64 | for _, path := range paths { |
| 65 | if path == "" { |
| 66 | continue |
| 67 | } |
| 68 | other, err := j.binary(path) |
| 69 | if err == nil && other.supports(bound) { |
| 70 | return false |
| 71 | } |
| 72 | } |
| 73 | return true |
| 74 | } |
| 75 | |
| 76 | // Called under release, previous-binary and floor exclusion. A rejected prev |
| 77 | // falls back to the certified current executor, including below a raised bound. |
| 78 | func rootFloorBootCandidate(ctx context.Context, j rootFloorJudge, journal bool, reader func(context.Context, string) error, idle func(context.Context) error) (string, error) { |
| 79 | bound, err := j.floors.localBound() |
| 80 | if err != nil { |
| 81 | return "", err |
| 82 | } |
| 83 | if journal && rootFloorPrevVerdict(j) == nil && idle(ctx) == nil && reader(ctx, j.prev) == nil { |
| 84 | return j.prev, nil |
| 85 | } |
| 86 | current, err := j.binary(j.current) |
| 87 | if err != nil { |
| 88 | return "", err |
| 89 | } |
| 90 | if !current.supports(nil) { |
| 91 | return "", errRootFloorHistory |
| 92 | } |
| 93 | if !current.supports(bound) { |
| 94 | rootFloorAlert() |
| 95 | } |
| 96 | return j.current, nil |
| 97 | } |
| 98 | |
| 99 | // The additional argument deliberately fails in an older floor-allows-prev |
| 100 | // reader. It is an internal unit protocol, not an operator reset interface. |
| 101 | func rootFloorAllowsPrev(opt string) error { |
| 102 | if !filepath.IsAbs(opt) || filepath.Clean(opt) != opt { |
| 103 | return errFloorGateUsage |
| 104 | } |
| 105 | id, required, err := rootMovesFloorIdentity() |
| 106 | if err != nil || !required { |
| 107 | return errors.Join(err, errRootFloorHistory) |
| 108 | } |
| 109 | return rootFloorAllowsPrevIdentity(opt, id) |
| 110 | } |
| 111 | func rootFloorAllowsPrevIdentity(opt string, id rootFloorIdentity) error { |
| 112 | f, err := openRootMovesFloors(opt, id, true) |
| 113 | if err != nil { |
| 114 | return err |
| 115 | } |
| 116 | defer f.close() |
| 117 | if err := rootSeedsAllowsBinary(opt, id, systemPrevPath(opt)); err != nil { |
| 118 | return err |
| 119 | } |
| 120 | return rootFloorPrevVerdict(rootFloorJudge{floors: f, current: systemBinaryPath(opt), prev: systemPrevPath(opt), binary: func(path string) (rootFloorBinary, error) { return readRootFloorBinary(opt, path) }}) |
| 121 | } |
| 122 | |
| 123 | func withRootFloorGuardCandidate(ctx context.Context, opt string, id rootFloorIdentity, journal bool, effect func(string) error) error { |
| 124 | return withClaimRootRelease(ctx, true, func() (rootstate.Store, error) { return rootstate.Open(rootReleasePrivate, true, false) }, func() error { |
| 125 | return withPrevLock(opt, func() error { |
| 126 | idleErr := rootFloorDowngradeIdle(ctx) |
| 127 | f, err := openRootMovesFloors(opt, id, true) |
| 128 | if err != nil { |
| 129 | return err |
| 130 | } |
| 131 | defer f.close() |
| 132 | unlock, err := f.private.Lock(ctx, "floor.lock") |
| 133 | if err != nil { |
| 134 | return err |
| 135 | } |
| 136 | defer unlock() |
| 137 | seeds, err := openRootProtocolFloors(rootSeedsFloorDir(opt), id, true) |
| 138 | if err != nil { |
| 139 | return err |
| 140 | } |
| 141 | defer seeds.close() |
| 142 | unlockSeeds, err := seeds.private.Lock(ctx, "floor.lock") |
| 143 | if err != nil { |
| 144 | return err |
| 145 | } |
| 146 | defer unlockSeeds() |
| 147 | j := rootFloorJudge{floors: f, current: systemBinaryPath(opt), prev: systemPrevPath(opt), binary: func(path string) (rootFloorBinary, error) { return readRootFloorBinary(opt, path) }} |
| 148 | candidate, err := rootFloorBootCandidate(ctx, j, journal, func(ctx context.Context, path string) error { |
| 149 | _, err := productionSysOps().run(ctx, systemVersionWait, path, "system", "floor-allows-prev", "--root", opt) |
| 150 | return err |
| 151 | }, func(context.Context) error { return idleErr }) |
| 152 | if err != nil { |
| 153 | return err |
| 154 | } |
| 155 | if candidate != j.current { |
| 156 | if err := rootSeedsAllowsBinary(opt, id, candidate); err != nil { |
| 157 | candidate = j.current |
| 158 | } |
| 159 | } |
| 160 | if candidate == j.current { |
| 161 | b, err := j.binary(candidate) |
| 162 | if err != nil { |
| 163 | return err |
| 164 | } |
| 165 | bound, err := readRootSeedsBound(opt, id) |
| 166 | if err != nil { |
| 167 | return err |
| 168 | } |
| 169 | movesBound, err := f.localBound() |
| 170 | if err != nil { |
| 171 | return err |
| 172 | } |
| 173 | if !b.forSeeds().supports(bound) && b.supports(movesBound) { |
| 174 | rootFloorAlert() |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | return effect(candidate) |
| 179 | }) |
| 180 | }) |
| 181 | } |
| 182 | |
| 183 | func rootFloorJournalPresent(opt string) (bool, error) { |
| 184 | f, err := rootstate.OpenAuthorityFile(filepath.Join(systemRootDir(opt), "journal.json")) |
| 185 | if errors.Is(err, os.ErrNotExist) { |
| 186 | return false, nil |
| 187 | } |
| 188 | if err != nil { |
| 189 | return false, err |
| 190 | } |
| 191 | return true, f.Close() |
| 192 | } |
| 193 | |
| 194 | func sameRootFloorImage(a, b rootFloorBinary) bool { |
| 195 | // Each observation reopens a root-owned image through checked ancestry |
| 196 | // and verifies its bytes against the signed identity. Device numbers and |
| 197 | // inode numbers are not authorization across boots or signed repairs. |
| 198 | return a.Digest != "" && a.Digest == b.Digest && a.Version == b.Version && a.Protocol == b.Protocol && a.SeedsProtocol == b.SeedsProtocol |
| 199 | } |
| 200 | |