carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// The identity of a staged artifact (zero-downtime deploys spec §4.2, §7.9,
2// §8.5): which bytes a process was exec'd from, kept so that recovery runs
3// exactly those bytes again and can tell when they have changed.
4package release
5
6import (
7 "errors"
8 "fmt"
9 "os"
10 "path/filepath"
11 "strings"
12)
13
14// StagedIdentity is the retained identity of a staged artifact (spec §4.2,
15// §8.5): the version name it was staged under, the immutable path it is
16// exec'd from, the sha256 of its bytes, and the release's own swap demand as
17// verified for those bytes ("hold" or ""). The agent captures it at exec,
18// never from the live link afterwards, because the link moves under a
19// running process.
20type StagedIdentity struct {
21 Version string
22 StagedPath string
23 Digest string
24 Demand string
25}
26
27// ErrStagedCopyChanged is recovery's refusal (spec §4.2): the staged file no
28// longer hashes to the digest retained for it, so it is not the build that
29// passed, and running it would be running something nobody verified.
30var ErrStagedCopyChanged = errors.New("staged copy no longer matches the digest it was verified with")
31
32// ErrVersionReused is root staging's refusal (spec §4.2): the store holds
33// different bytes under a version name this box already staged, and
34// something on the box still refers to the staged copy, so it is not
35// overwritten. The text is the spec's.
36var ErrVersionReused = errors.New("version name reused with different content, ship under a new name")
37
38// maxLinkHops caps a live link's symlink chain, as Linux caps the symlinks
39// one path lookup follows.
40const maxLinkHops = 40
41
42// linkChain follows link's leaf symlink chain one hop at a time: link's own
43// target, then that target's target for as long as it is a symlink. An
44// absolute target is a hop as written (cleaned). A relative target is
45// resolved as the OS resolves it: against the REAL directory of the link
46// that holds it (filepath.EvalSymlinks of that link's parent), so a link
47// reached through a symlinked directory reaches what the OS reaches (Astra r2
48// N3, ruling R17). A parent that does not exist leaves the lexical join,
49// whose hop then does not exist either; any other error resolving a parent
50// is an error.
51//
52// It returns link's raw first target, every hop in order (the last is where
53// the chain ends), and aliases: the lexical join of each relative hop whose
54// real path differs from it. BoxPins pins those too, so a box whose opt dir
55// is itself reached through a symlink keeps the pin it had under the opt
56// dir's own name, which is how VersionPath names a staged copy. A hop that
57// does not exist ends the chain and is included. A loop, more than
58// maxLinkHops hops, or any other error reading a hop is an error: a chain
59// that cannot be followed to its end names nothing for certain. ResolveLive
60// and BoxPins both walk links with it, so the build a worker execs and the
61// copies the pin set protects cannot disagree.
62func linkChain(link string) (raw string, hops, aliases []string, err error) {
63 raw, err = os.Readlink(link)
64 if err != nil {
65 return "", nil, nil, fmt.Errorf("read live link %s: %w", link, err)
66 }
67 seen := map[string]bool{filepath.Clean(link): true}
68 holder, target := link, raw
69 for {
70 if len(hops) == maxLinkHops {
71 return "", nil, nil, fmt.Errorf("live link %s: more than %d symlink hops", link, maxLinkHops)
72 }
73 if !filepath.IsAbs(target) {
74 parent := filepath.Dir(holder)
75 lexical := filepath.Join(parent, target)
76 realParent, err := filepath.EvalSymlinks(parent)
77 switch {
78 case errors.Is(err, os.ErrNotExist):
79 target = lexical
80 case err != nil:
81 return "", nil, nil, fmt.Errorf("live link %s: resolve %s: %w", link, parent, err)
82 default:
83 target = filepath.Join(realParent, target)
84 if target != lexical {
85 aliases = append(aliases, lexical)
86 }
87 }
88 }
89 target = filepath.Clean(target)
90 if seen[target] {
91 return "", nil, nil, fmt.Errorf("live link %s: symlink loop at %s", link, target)
92 }
93 seen[target] = true
94 hops = append(hops, target)
95 fi, err := os.Lstat(target)
96 if errors.Is(err, os.ErrNotExist) {
97 return raw, hops, aliases, nil
98 }
99 if err != nil {
100 return "", nil, nil, fmt.Errorf("live link %s: %w", link, err)
101 }
102 if fi.Mode()&os.ModeSymlink == 0 {
103 return raw, hops, aliases, nil
104 }
105 next, err := os.Readlink(target)
106 if err != nil {
107 return "", nil, nil, fmt.Errorf("live link %s: %w", link, err)
108 }
109 holder, target = target, next
110 }
111}
112
113// ResolveLive reads host's live link ONCE and reports the file at the end of
114// its symlink chain (linkChain) and that file's sha256. Version is the link's
115// version as LiveVersion reads it, from the link's own target, "" for a link
116// outside the version store; the path and digest identify the bytes either
117// way. Demand is left empty: only CanonicalDemand can vouch for it.
118func ResolveLive(optDir, host string) (StagedIdentity, error) {
119 id, err := ResolveLiveLink(optDir, host)
120 if err != nil {
121 return StagedIdentity{}, err
122 }
123 sum, err := FileSHA256(id.StagedPath)
124 if err != nil {
125 return StagedIdentity{}, fmt.Errorf("hash %s: %w", id.StagedPath, err)
126 }
127 id.Digest = sum
128 return id, nil
129}
130
131// ResolveLiveLink is ResolveLive without the hash: the file at the end of
132// the live link's chain and the version that same reading of the link names,
133// for a caller that opens and hashes the file itself (the claimed launch
134// snapshot). Digest is left empty.
135func ResolveLiveLink(optDir, host string) (StagedIdentity, error) {
136 raw, hops, _, err := linkChain(LiveLink(optDir, host))
137 if err != nil {
138 return StagedIdentity{}, err
139 }
140 return StagedIdentity{Version: versionOfTarget(optDir, raw), StagedPath: hops[len(hops)-1]}, nil
141}
142
143// RetainedStaged is ver's staged executable of app on this box, when its
144// bytes still hash to this platform's artifact digest in its staged manifest
145// sibling: the manifest the copy was verified against when it was staged
146// (settings spec §4.1's recovery check; root's stageAuthorized and the held
147// wake check both use it). nil when the sibling is absent or unreadable,
148// names no artifact for this platform, or the bytes differ. Demand is left
149// empty, as ResolveLive leaves it: only CanonicalDemand can vouch for it.
150func RetainedStaged(optDir, acct, app, ver string) (*StagedIdentity, *Manifest) {
151 m, err := ReadStagedManifest(optDir, acct, ver, app)
152 if err != nil || m == nil {
153 return nil, nil
154 }
155 a, ok := m.Artifacts[adoptPlatform]
156 if !ok {
157 return nil, nil
158 }
159 id := StagedIdentity{Version: ver, StagedPath: VersionPath(optDir, acct, ver, app), Digest: strings.ToLower(a.SHA256)}
160 if VerifyStaged(id) != nil {
161 return nil, nil
162 }
163 return &id, m
164}
165
166// ResolveLivePath is ResolveLiveLink: one read of host's live link, the file
167// at the end of its chain (linkChain) and the link's version, with Digest
168// empty. It is for a start that needs only to exec exactly the build it
169// named (the activator's live exec gate, Task 9), where hashing a build of
170// up to a hundred megabytes at every wake would buy nothing.
171func ResolveLivePath(optDir, host string) (StagedIdentity, error) {
172 return ResolveLiveLink(optDir, host)
173}
174
175// VerifyStaged checks the file at id.StagedPath against id.Digest, the digest
176// retained when the build was exec'd (spec §4.2's recovery check). It never
177// consults the canonical manifest: a later re-ship of the same name must not
178// revoke local recovery.
179func VerifyStaged(id StagedIdentity) error {
180 if id.StagedPath == "" || id.Digest == "" {
181 return fmt.Errorf("%w: no retained path and digest for %q", ErrStagedCopyChanged, id.Version)
182 }
183 sum, err := FileSHA256Cached(id.StagedPath)
184 if err != nil {
185 return fmt.Errorf("%w: %s: %v", ErrStagedCopyChanged, id.StagedPath, err)
186 }
187 if !strings.EqualFold(sum, id.Digest) {
188 return fmt.Errorf("%w: %s hashes to %s, retained %s", ErrStagedCopyChanged, id.StagedPath, sum, id.Digest)
189 }
190 return nil
191}
192