// The wrong-program check — D2 of the console release defences (spec // 2026-10-06-console-release-defences-design.md §3). // // On 2026-10-06 the public carlos CLI binary was shipped as the console. Its // architecture and its version stamp were both right, so nothing objected; // what was wrong was the program. A Go binary records its main package path // in its build info, readable from stripped bytes for any architecture // without running them, so a ship can compare it with the app's previous // release and a box can compare it with what its route has run. package release import ( "bytes" "context" "debug/buildinfo" "encoding/json" "errors" "fmt" "log" "os" "sort" "strings" ) // ArtifactProgram extracts the Go main package path from a binary's debug/buildinfo, // returning "" if the bytes are not a Go binary or buildinfo is unreadable. func ArtifactProgram(b []byte) string { bi, err := buildinfo.Read(bytes.NewReader(b)) if err != nil { return "" } return bi.Path } // BuildStampFlag is the linker flag that stamps a carlos build's version // (internal/version.Build). Its value is a release build's tag. const BuildStampFlag = "github.com/carlosframework/platform/internal/version.Build" // ArtifactBuild is the version.Build stamp a linux-arm64 Go binary's build // info records: the value of the one "-X =" in its // recorded -ldflags. It is "" when the bytes fail CheckELFBounds, have no // build info, or record the flag absent, more than once or in any other // form, so a caller that needs a stamp refuses all of those alike. // // It reads the RECORDED flag, not the linked value of version.Build: a // release binary is stripped (-s -w), so there is no symbol table to read // the variable from, and a build's recorded flags could be edited without // changing its linked value. Its caller (claims publish-identity) relies on // the release key's authentication of the artefact: the signed pointer, // manifest digest and sha256 only the operator can produce. Owed before // production cohorts: trusted build attestation tied to the artefact digest // (controller ruling F2, accepted as a trial gap). func ArtifactBuild(b []byte) string { if CheckELFBounds(b) != nil { return "" } bi, err := buildinfo.Read(bytes.NewReader(b)) if err != nil { return "" } ldflags := "" for _, s := range bi.Settings { if s.Key == "-ldflags" { ldflags = s.Value } } fields := strings.Fields(ldflags) stamp, seen := "", 0 for i, f := range fields { if strings.Contains(f, BuildStampFlag) { seen++ if i > 0 && fields[i-1] == "-X" && strings.HasPrefix(f, BuildStampFlag+"=") { stamp = strings.TrimPrefix(f, BuildStampFlag+"=") } } } if seen != 1 { return "" } return stamp } // FileProgram extracts the Go main package path from a file at path, // returning "" if the file is not a Go binary or buildinfo is unreadable. func FileProgram(path string) string { bi, err := buildinfo.ReadFile(path) if err != nil { return "" } return bi.Path } // ReleaseProgram is the one program every Go artifact in files reports, "" // when none is Go. func ReleaseProgram(files map[string][]byte) (string, error) { seen := map[string][]string{} for name, b := range files { if p := ArtifactProgram(b); p != "" { seen[p] = append(seen[p], name) } } if len(seen) > 1 { var parts []string for p, names := range seen { sort.Strings(names) parts = append(parts, fmt.Sprintf("%s is %s", strings.Join(names, ", "), p)) } sort.Strings(parts) return "", fmt.Errorf("this release's artifacts are different programs: %s", strings.Join(parts, "; ")) } for p := range seen { return p, nil } return "", nil } // PreviousProgram is the program of the newest release (among the 20 newest) // whose manifest declares one, and that release's version. It skips missing // manifests (os.ErrNotExist) and manifests that don't parse or validate // (logging each), returning store errors to fail the check closed. func PreviousProgram(ctx context.Context, s Store, acct, app string) (string, string, error) { vers, _, err := VersionCandidates(ctx, s, acct, app, 20) if err != nil { return "", "", err } var best *Manifest for _, v := range vers { // Read the manifest using GetQualified, exactly as GetManifest does. raw, _, err := GetQualified(ctx, s, ManifestKey(acct, app, v)) if err != nil { if errors.Is(err, os.ErrNotExist) { // In-flight release with no manifest yet; skip it. continue } // Store error; fail closed. return "", "", err } // Decode and validate the manifest ourselves, separating parse errors // (which we skip) from store errors (which fail closed). var m Manifest if err := json.Unmarshal(raw, &m); err != nil { log.Printf("program check: manifest %s/%s: decode error: %v", app, v, err) continue } if err := validateManifest(&m); err != nil { log.Printf("program check: manifest %s/%s: validation error: %v", app, v, err) continue } if m.App != app || m.Version != v { log.Printf("program check: manifest %s/%s: declares wrong app/version", app, v) continue } if m.Program == "" { continue } if best == nil || m.BuiltAt > best.BuiltAt { best = &m } } if best == nil { return "", "", nil } return best.Program, best.Version, nil } // ProgramMismatchError is returned when a release attempts to ship a different program // than the app's previous releases without the --program-change override. // // The message names `carlos ship` because `carlos deploy` and canary meet // this refusal too and do not take the flag. Nothing was written, so the // same version can be shipped again. type ProgramMismatchError struct{ App, Got, Want, WantVersion string } func (e *ProgramMismatchError) Error() string { 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", e.Got, e.App, e.Want, e.WantVersion) } // StampProgram records a binary release's Go program on m, and the operator's // --program-change as m.ProgramChange. A static release, or one with no Go // artifact, is left unstamped. It errors only when the artifacts are // different programs. // // The flag is recorded whenever it was given, not only when the release // differs from the newest one: a box decides by its own route's history, and a // box that missed the release which changed the program (ship A, ship B1 with // the flag, a box still on A) must find the consent on B2 too. // // It is the half of CheckShipProgram that does not read the store, so the // direct-bucket door can stamp a re-ship of an existing version exactly as // its first ship was stamped (the version's manifest is compared byte for // byte by release.Ship) without judging it against the history again. func StampProgram(m *Manifest, files map[string][]byte, allowChange bool) error { if m.Kind != "binary" { return nil } prog, err := ReleaseProgram(files) if err != nil || prog == "" { return err } m.Program = prog m.ProgramChange = allowChange return nil } // CheckShipProgram is the one ship-side rule both ship doors call before // release.Ship for a new version. It stamps m (StampProgram), then refuses a // release whose program differs from the app's previous release unless // allowChange: the comparison only decides whether a ship WITHOUT the flag // refuses. func CheckShipProgram(ctx context.Context, s Store, acct string, m *Manifest, files map[string][]byte, allowChange bool) error { if err := StampProgram(m, files, allowChange); err != nil || m.Program == "" { return err } prog := m.Program prev, prevVer, err := PreviousProgram(ctx, s, acct, m.App) if err != nil { return fmt.Errorf("reading %s's previous releases: %w", m.App, err) } if prev == "" || prev == prog || allowChange { return nil } return &ProgramMismatchError{App: m.App, Got: prog, Want: prev, WantVersion: prevVer} }