carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// The wrong-program check — D2 of the console release defences (spec
2// 2026-10-06-console-release-defences-design.md §3).
3//
4// On 2026-10-06 the public carlos CLI binary was shipped as the console. Its
5// architecture and its version stamp were both right, so nothing objected;
6// what was wrong was the program. A Go binary records its main package path
7// in its build info, readable from stripped bytes for any architecture
8// without running them, so a ship can compare it with the app's previous
9// release and a box can compare it with what its route has run.
10package release
11
12import (
13 "bytes"
14 "context"
15 "debug/buildinfo"
16 "encoding/json"
17 "errors"
18 "fmt"
19 "log"
20 "os"
21 "sort"
22 "strings"
23)
24
25// ArtifactProgram extracts the Go main package path from a binary's debug/buildinfo,
26// returning "" if the bytes are not a Go binary or buildinfo is unreadable.
27func ArtifactProgram(b []byte) string {
28 bi, err := buildinfo.Read(bytes.NewReader(b))
29 if err != nil {
30 return ""
31 }
32 return bi.Path
33}
34
35// BuildStampFlag is the linker flag that stamps a carlos build's version
36// (internal/version.Build). Its value is a release build's tag.
37const BuildStampFlag = "github.com/carlosframework/platform/internal/version.Build"
38
39// ArtifactBuild is the version.Build stamp a linux-arm64 Go binary's build
40// info records: the value of the one "-X <BuildStampFlag>=<v>" in its
41// recorded -ldflags. It is "" when the bytes fail CheckELFBounds, have no
42// build info, or record the flag absent, more than once or in any other
43// form, so a caller that needs a stamp refuses all of those alike.
44//
45// It reads the RECORDED flag, not the linked value of version.Build: a
46// release binary is stripped (-s -w), so there is no symbol table to read
47// the variable from, and a build's recorded flags could be edited without
48// changing its linked value. Its caller (claims publish-identity) relies on
49// the release key's authentication of the artefact: the signed pointer,
50// manifest digest and sha256 only the operator can produce. Owed before
51// production cohorts: trusted build attestation tied to the artefact digest
52// (controller ruling F2, accepted as a trial gap).
53func ArtifactBuild(b []byte) string {
54 if CheckELFBounds(b) != nil {
55 return ""
56 }
57 bi, err := buildinfo.Read(bytes.NewReader(b))
58 if err != nil {
59 return ""
60 }
61 ldflags := ""
62 for _, s := range bi.Settings {
63 if s.Key == "-ldflags" {
64 ldflags = s.Value
65 }
66 }
67 fields := strings.Fields(ldflags)
68 stamp, seen := "", 0
69 for i, f := range fields {
70 if strings.Contains(f, BuildStampFlag) {
71 seen++
72 if i > 0 && fields[i-1] == "-X" && strings.HasPrefix(f, BuildStampFlag+"=") {
73 stamp = strings.TrimPrefix(f, BuildStampFlag+"=")
74 }
75 }
76 }
77 if seen != 1 {
78 return ""
79 }
80 return stamp
81}
82
83// FileProgram extracts the Go main package path from a file at path,
84// returning "" if the file is not a Go binary or buildinfo is unreadable.
85func FileProgram(path string) string {
86 bi, err := buildinfo.ReadFile(path)
87 if err != nil {
88 return ""
89 }
90 return bi.Path
91}
92
93// ReleaseProgram is the one program every Go artifact in files reports, ""
94// when none is Go.
95func ReleaseProgram(files map[string][]byte) (string, error) {
96 seen := map[string][]string{}
97 for name, b := range files {
98 if p := ArtifactProgram(b); p != "" {
99 seen[p] = append(seen[p], name)
100 }
101 }
102 if len(seen) > 1 {
103 var parts []string
104 for p, names := range seen {
105 sort.Strings(names)
106 parts = append(parts, fmt.Sprintf("%s is %s", strings.Join(names, ", "), p))
107 }
108 sort.Strings(parts)
109 return "", fmt.Errorf("this release's artifacts are different programs: %s", strings.Join(parts, "; "))
110 }
111 for p := range seen {
112 return p, nil
113 }
114 return "", nil
115}
116
117// PreviousProgram is the program of the newest release (among the 20 newest)
118// whose manifest declares one, and that release's version. It skips missing
119// manifests (os.ErrNotExist) and manifests that don't parse or validate
120// (logging each), returning store errors to fail the check closed.
121func PreviousProgram(ctx context.Context, s Store, acct, app string) (string, string, error) {
122 vers, _, err := VersionCandidates(ctx, s, acct, app, 20)
123 if err != nil {
124 return "", "", err
125 }
126 var best *Manifest
127 for _, v := range vers {
128 // Read the manifest using GetQualified, exactly as GetManifest does.
129 raw, _, err := GetQualified(ctx, s, ManifestKey(acct, app, v))
130 if err != nil {
131 if errors.Is(err, os.ErrNotExist) {
132 // In-flight release with no manifest yet; skip it.
133 continue
134 }
135 // Store error; fail closed.
136 return "", "", err
137 }
138
139 // Decode and validate the manifest ourselves, separating parse errors
140 // (which we skip) from store errors (which fail closed).
141 var m Manifest
142 if err := json.Unmarshal(raw, &m); err != nil {
143 log.Printf("program check: manifest %s/%s: decode error: %v", app, v, err)
144 continue
145 }
146 if err := validateManifest(&m); err != nil {
147 log.Printf("program check: manifest %s/%s: validation error: %v", app, v, err)
148 continue
149 }
150 if m.App != app || m.Version != v {
151 log.Printf("program check: manifest %s/%s: declares wrong app/version", app, v)
152 continue
153 }
154
155 if m.Program == "" {
156 continue
157 }
158 if best == nil || m.BuiltAt > best.BuiltAt {
159 best = &m
160 }
161 }
162 if best == nil {
163 return "", "", nil
164 }
165 return best.Program, best.Version, nil
166}
167
168// ProgramMismatchError is returned when a release attempts to ship a different program
169// than the app's previous releases without the --program-change override.
170//
171// The message names `carlos ship` because `carlos deploy` and canary meet
172// this refusal too and do not take the flag. Nothing was written, so the
173// same version can be shipped again.
174type ProgramMismatchError struct{ App, Got, Want, WantVersion string }
175
176func (e *ProgramMismatchError) Error() string {
177 return fmt.Sprintf("this artifact is %s; %s's releases are %s (%s). If the app really is changing program, ship again with carlos ship --program-change",
178 e.Got, e.App, e.Want, e.WantVersion)
179}
180
181// StampProgram records a binary release's Go program on m, and the operator's
182// --program-change as m.ProgramChange. A static release, or one with no Go
183// artifact, is left unstamped. It errors only when the artifacts are
184// different programs.
185//
186// The flag is recorded whenever it was given, not only when the release
187// differs from the newest one: a box decides by its own route's history, and a
188// box that missed the release which changed the program (ship A, ship B1 with
189// the flag, a box still on A) must find the consent on B2 too.
190//
191// It is the half of CheckShipProgram that does not read the store, so the
192// direct-bucket door can stamp a re-ship of an existing version exactly as
193// its first ship was stamped (the version's manifest is compared byte for
194// byte by release.Ship) without judging it against the history again.
195func StampProgram(m *Manifest, files map[string][]byte, allowChange bool) error {
196 if m.Kind != "binary" {
197 return nil
198 }
199 prog, err := ReleaseProgram(files)
200 if err != nil || prog == "" {
201 return err
202 }
203 m.Program = prog
204 m.ProgramChange = allowChange
205 return nil
206}
207
208// CheckShipProgram is the one ship-side rule both ship doors call before
209// release.Ship for a new version. It stamps m (StampProgram), then refuses a
210// release whose program differs from the app's previous release unless
211// allowChange: the comparison only decides whether a ship WITHOUT the flag
212// refuses.
213func CheckShipProgram(ctx context.Context, s Store, acct string, m *Manifest, files map[string][]byte, allowChange bool) error {
214 if err := StampProgram(m, files, allowChange); err != nil || m.Program == "" {
215 return err
216 }
217 prog := m.Program
218 prev, prevVer, err := PreviousProgram(ctx, s, acct, m.App)
219 if err != nil {
220 return fmt.Errorf("reading %s's previous releases: %w", m.App, err)
221 }
222 if prev == "" || prev == prog || allowChange {
223 return nil
224 }
225 return &ProgramMismatchError{App: m.App, Got: prog, Want: prev, WantVersion: prevVer}
226}
227