| 1 | // What a named unit is running, established from the bytes — D3 of the |
| 2 | // console release defences (spec 2026-10-06-console-release-defences-design.md |
| 3 | // §4.2, §4.3, §4.5). |
| 4 | // |
| 5 | // observe checks the first three of spec §4.3's mint conditions: the binding |
| 6 | // is verified, the unit is active in one invocation for at least `after`, the |
| 7 | // sha256 of /proc/<MainPID>/exe equals the live link's bytes, and the route is |
| 8 | // ready now. The root pass mints on it (proofroot.go) and records the build |
| 9 | // in the healthy history on it. recordSettled is the other way into that |
| 10 | // history: the hook D1's checkedUnitRestart runs when the settle check passes. |
| 11 | // |
| 12 | // Both paths hash the running executable's bytes. A sha256 is never taken |
| 13 | // from a manifest or a version directory's name: the only build a box may |
| 14 | // vouch for is the one it is running. |
| 15 | package host |
| 16 | |
| 17 | import ( |
| 18 | "context" |
| 19 | "fmt" |
| 20 | "os" |
| 21 | "time" |
| 22 | |
| 23 | "github.com/carlosframework/platform/internal/activator" |
| 24 | "github.com/carlosframework/platform/internal/registry" |
| 25 | "github.com/carlosframework/platform/internal/release" |
| 26 | ) |
| 27 | |
| 28 | type liveBuild struct { |
| 29 | binding proofBinding |
| 30 | SHA256 string |
| 31 | Version string |
| 32 | ActiveSince time.Time |
| 33 | // The process observed, so a later look can tell it is the same one. |
| 34 | invocation string |
| 35 | pid int |
| 36 | start string |
| 37 | } |
| 38 | |
| 39 | type proofObserver struct { |
| 40 | resolve proofBindingResolver |
| 41 | sys unitSystem |
| 42 | procExe func(pid int) string // "/proc/<pid>/exe" in production |
| 43 | optDir string |
| 44 | now func() time.Time |
| 45 | ready func(rt registry.Route) func() error // routeReady in production; nil func = no upstream |
| 46 | } |
| 47 | |
| 48 | func productionProofObserver(optDir string) proofObserver { |
| 49 | return proofObserver{sys: systemdUnits{}, procExe: procExePath, optDir: optDir, now: time.Now, |
| 50 | resolve: productionRootRestartChecks(systemdUnits{}, optDir).resolve, |
| 51 | ready: func(rt registry.Route) func() error { return proofRouteReady(rt, 2*time.Second) }} |
| 52 | } |
| 53 | |
| 54 | func procExePath(pid int) string { return fmt.Sprintf("/proc/%d/exe", pid) } |
| 55 | |
| 56 | func (o proofObserver) observe(rt registry.Route, after time.Duration) (liveBuild, error) { |
| 57 | if o.resolve == nil { |
| 58 | return liveBuild{}, os.ErrPermission |
| 59 | } |
| 60 | binding, err := o.resolve(context.Background(), rt) |
| 61 | if err != nil { |
| 62 | return liveBuild{}, err |
| 63 | } |
| 64 | if binding.Source == "" { |
| 65 | return liveBuild{}, os.ErrPermission |
| 66 | } |
| 67 | rt = binding.Route |
| 68 | link := release.LiveLink(o.optDir, rt.Host) |
| 69 | before, err := o.sys.Show(rt.Unit) |
| 70 | if err != nil { |
| 71 | return liveBuild{}, fmt.Errorf("cannot read %s: %v", rt.Unit, err) |
| 72 | } |
| 73 | if before.ExecPath != proofExecPath(binding, o.optDir) { |
| 74 | return liveBuild{}, fmt.Errorf("binding: %s runs %s, not %s", rt.Unit, before.ExecPath, link) |
| 75 | } |
| 76 | if before.ActiveState != "active" || before.MainPID <= 0 { |
| 77 | return liveBuild{}, fmt.Errorf("%s is %s", rt.Unit, before.ActiveState) |
| 78 | } |
| 79 | if before.ActiveEnterTimestamp.IsZero() { |
| 80 | return liveBuild{}, fmt.Errorf("%s reports no ActiveEnterTimestamp", rt.Unit) |
| 81 | } |
| 82 | start, err := o.sys.StartTime(before.MainPID) |
| 83 | if err != nil || start == "" { |
| 84 | return liveBuild{}, os.ErrPermission |
| 85 | } |
| 86 | if up := o.now().Sub(before.ActiveEnterTimestamp); up < after { |
| 87 | return liveBuild{}, fmt.Errorf("%s has been active for %s, needs %s", rt.Unit, up.Round(time.Second), after) |
| 88 | } |
| 89 | exe, err := release.FileSHA256(o.procExe(before.MainPID)) |
| 90 | if err != nil { |
| 91 | return liveBuild{}, fmt.Errorf("hashing the running executable: %v", err) |
| 92 | } |
| 93 | live, err := release.FileSHA256(link) |
| 94 | if err != nil { |
| 95 | return liveBuild{}, fmt.Errorf("hashing the live link's build: %v", err) |
| 96 | } |
| 97 | if exe != live { |
| 98 | return liveBuild{}, fmt.Errorf("the running executable (sha256 %s) is not the live link's build (sha256 %s)", exe, live) |
| 99 | } |
| 100 | if binding.Network { |
| 101 | if o.ready == nil { |
| 102 | return liveBuild{}, os.ErrPermission |
| 103 | } |
| 104 | if ready := o.ready(rt); ready != nil { |
| 105 | if err := ready(); err != nil { |
| 106 | return liveBuild{}, fmt.Errorf("not ready: %v", err) |
| 107 | } |
| 108 | } else { |
| 109 | return liveBuild{}, os.ErrPermission |
| 110 | } |
| 111 | } |
| 112 | again, err := o.sys.Show(rt.Unit) |
| 113 | if err != nil || again.ActiveState != "active" || again.InvocationID != before.InvocationID || again.MainPID != before.MainPID { |
| 114 | return liveBuild{}, fmt.Errorf("%s restarted during the check", rt.Unit) |
| 115 | } |
| 116 | current, err := o.resolve(context.Background(), rt) |
| 117 | if err != nil || current.Source != binding.Source { |
| 118 | return liveBuild{}, os.ErrPermission |
| 119 | } |
| 120 | againExe, err := release.FileSHA256(o.procExe(again.MainPID)) |
| 121 | if err != nil || againExe != exe || again.ExecPath != before.ExecPath { |
| 122 | return liveBuild{}, os.ErrPermission |
| 123 | } |
| 124 | againStart, err := o.sys.StartTime(again.MainPID) |
| 125 | if err != nil || againStart != start { |
| 126 | return liveBuild{}, os.ErrPermission |
| 127 | } |
| 128 | ver, _ := release.LiveVersion(o.optDir, rt.Host) |
| 129 | return liveBuild{SHA256: exe, Version: ver, ActiveSince: before.ActiveEnterTimestamp, |
| 130 | invocation: before.InvocationID, pid: before.MainPID, start: start, binding: binding}, nil |
| 131 | } |
| 132 | |
| 133 | func (o proofObserver) recordSettled(history string) func(registry.Route) error { |
| 134 | return func(rt registry.Route) error { |
| 135 | if rt.Unit == "" || rt.Unit == activator.UnitName(rt.Host) { |
| 136 | return nil |
| 137 | } |
| 138 | b, err := o.observe(rt, 0) |
| 139 | if err != nil { |
| 140 | return err |
| 141 | } |
| 142 | return release.RecordHealthy(history, b.binding.Route.Account, b.binding.Route.App, |
| 143 | release.HealthyBuild{SHA256: b.SHA256, Version: b.Version, How: "settle", At: o.now().UTC()}) |
| 144 | } |
| 145 | } |
| 146 | |