package release import ( "context" "errors" "os" "path/filepath" "sort" "strings" "github.com/carlosframework/platform/internal/rootstate" ) // RootGenerationPins is read under release exclusion, held through destruction. // Verify must reconcile the records with root installation/launch inventory and // kernel references. An empty directory alone never establishes completeness. type RootGenerationPins struct { Ledger *RootGenerations Verify func(context.Context, []GenerationReservation) error Reconcile func(context.Context, []GenerationReservation) (GenerationEvidence, error) } func (s RootGenerationPins) StagedPins(ctx context.Context, optDir string) (StagedPaths, error) { if s.Ledger == nil || s.Ledger.Store == nil || !filepath.IsAbs(optDir) { return nil, os.ErrPermission } records, err := s.Ledger.Reservations(ctx) if err != nil { return nil, err } pins := StagedPaths{} for _, r := range records { b := r.Binding if b.Path != VersionPath(optDir, b.Binding.Account, b.Version, b.Binding.App) { return nil, os.ErrPermission } if r.Retirement != "" { continue } for _, item := range []struct { path string expected, proof rootstate.FileIdentity }{ {b.Path, b.Executable, b.ExecutableProof}, {b.Tree, b.TreeIdentity, b.TreeProof}, } { if err := generationFileIdentity(item.path, item.expected, item.proof, b.Target.BootID); err != nil { return nil, err } } pins.add(b.Path) pins.add(b.Tree) } if s.Reconcile != nil { if _, err := s.Ledger.Certify(ctx, records, s.Reconcile); err != nil { return nil, err } return pins, nil } if s.Verify == nil { return nil, os.ErrPermission } if err := s.Verify(ctx, records); err != nil { return nil, err } return pins, nil } func generationFileIdentity(path string, expected, proof rootstate.FileIdentity, boot string) error { if proof.Boot != "" { if proof.Boot != boot || proof.Device != expected.Device || proof.Inode != expected.Inode { return os.ErrPermission } var f *os.File var err error if proof.Mode&0170000 == 0040000 { f, err = rootstate.OpenAuthorityDirectory(path) } else { f, err = rootstate.OpenAuthorityFile(path) } if err != nil { return err } defer f.Close() actual, err := rootstate.PersistentIdentity(f) if err != nil { return err } if !GenerationFileProof(path, proof, actual) { return os.ErrPermission } return nil } info, err := os.Lstat(path) if err != nil { return err } if info.Mode()&os.ModeSymlink != 0 { return os.ErrPermission } f, err := os.Open(path) if err != nil { return err } defer f.Close() actual, err := rootstate.Identity(f) if err != nil { return err } observed, err := rootstate.ObjectIdentity(f) if err != nil { return err } if boot == "" || observed.Boot != boot || actual != expected { return os.ErrPermission } return nil } // WithRootGenerationPins protects new writes as well as replacement of a // differing artifact: a removed executable can still be held by a generation. func WithRootGenerationPins(source PinSource) PassOption { return func(c *passConfig) { c.generationPins = source } } // Sibling repair can change a reserved tree even when its binary already // matches. Check only when a write is needed, preserving the legacy no-op path. func stageGenerationSibling(optDir, acct, app, ver string, raw []byte, checks []stageCheck) error { for _, check := range checks { if check.beforeWrite != nil { if err := check.beforeWrite(StagedManifestPath(optDir, acct, ver, app)); err != nil { return err } } } return StageManifest(optDir, acct, ver, app, raw) } func (c passConfig) generationWriteCheck(ctx context.Context, optDir, path string) error { if c.generationPins == nil { return nil } // Creating a wholly absent version tree cannot destroy an existing pin. // Keep fresh signed recovery available while pin completeness is unknown. rel, err := filepath.Rel(filepath.Join(optDir, "versions"), path) version, _, ok := strings.Cut(rel, string(filepath.Separator)) if err != nil || !filepath.IsLocal(rel) || !ok || !ValidVersion(version) { return os.ErrPermission } if _, err := os.Lstat(filepath.Join(optDir, "versions", version)); errors.Is(err, os.ErrNotExist) { return nil } else if err != nil { return err } pins, err := c.generationPins.StagedPins(ctx, optDir) if err != nil { return err } pinned, err := pins.Refers(path) if err != nil { return err } if pinned { return ErrVersionReused } for parent := filepath.Dir(path); parent != optDir && parent != filepath.Dir(parent); parent = filepath.Dir(parent) { pinned, err := pins.Refers(parent) if err != nil { return err } if pinned { return ErrVersionReused } } return nil } // Reservations validates and recommits every retained record under release exclusion. func (g *RootGenerations) Reservations(ctx context.Context) ([]GenerationReservation, error) { if g == nil || g.Store == nil { return nil, os.ErrPermission } names, err := g.Store.Names() if err != nil { return nil, err } sort.Strings(names) records := make([]GenerationReservation, 0, len(names)) for _, name := range names { if name == generationCompletenessKey { if _, err := g.Completeness(); err != nil { return nil, err } continue } if err := ctx.Err(); err != nil { return nil, err } raw, err := g.Store.Read(name) if err != nil { return nil, err } var r GenerationReservation if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &r) != nil || !validGeneration(r) || generationKey(r.Binding) != name { return nil, os.ErrPermission } // Read retries the directory sync before this record can inform a grant. r, err = g.Read(r.Binding) if err != nil { return nil, err } records = append(records, r) } return records, nil }