// Package tomb is the content-addressable storage (CAS) layer of the Coven. // // "In the Coven, we forge the living. In the Tomb, we preserve the eternal." // // A Sarcophagus is the on-disk record of one Essence — the binaries, // libraries and metadata produced by one cast. The Epitaph is the metadata // sidecar (JSON): name, version, y/n flags, toolchain id, Merkle root. // Reanimation is the act of binding an Essence into a Sanctum. // Technically this means creating reflinks (preferred — zero-copy on // btrfs/xfs) or hardlinks from the Tomb's blob store into the Sanctum's // rootfs. If neither is possible (cross-filesystem), we fall back to a // plain copy. // // Works identically across all runtimes: LXC, Podman, Firecracker, baremetal. // // Files are stored by their SHA-256 hash under // /var/lib/sorcery-go/tomb/blobs// (sharded by first two hex // chars) so a single directory never grows past ~65k entries — keeping // `ls` fast on cold disks. package tomb import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "log" "os" "path/filepath" "sort" "strings" ) // Sarcophagus is the on-disk record of one Essence variant. type Sarcophagus struct { EssenceID string `json:"essence_id"` // Merkle root SpellName string `json:"spell_name"` Version string `json:"version"` VariantHash string `json:"variant_hash"` // sha256(version + flags + arch + toolchain) Arch string `json:"arch"` Linkage string `json:"linkage"` // "dynamic" | "static" | "hermetic" Config map[string]bool `json:"config"` // y/n answers Files map[string]string `json:"files"` // path -> sha256 CreatedAt string `json:"created_at"` Toolchain string `json:"toolchain"` License string `json:"license"` SignedBy string `json:"signed_by,omitempty"` // PGP key fingerprint } // Tomb is the storage manager. type Tomb struct { Root string // /var/lib/sorcery-go/tomb } // New returns a Tomb rooted at `root`. func New(root string) *Tomb { return &Tomb{Root: root} } // IngestBlob copies a file from `srcPath` into the Tomb's blob store, // returning the SHA-256 hash. Content-addressed storage means duplicate // blobs are harmlessly re-written with identical content. func (t *Tomb) IngestBlob(srcPath string) (string, error) { hash, err := HashFile(srcPath) if err != nil { return "", err } dst := t.blobPath(hash) if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil { return "", err } in, err := os.Open(srcPath) if err != nil { return "", err } defer in.Close() // Use O_CREATE|O_TRUNC directly — no TOCTOU race from a prior Stat. // Content-addressed means re-writing the same content is safe. out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return "", err } defer out.Close() if _, err := io.Copy(out, in); err != nil { return "", err } return hash, nil } // Store writes a Sarcophagus's epitaph to disk. Callers must have already // ingested every blob via IngestBlob. func (t *Tomb) Store(sarc *Sarcophagus) error { if err := os.MkdirAll(filepath.Join(t.Root, "epitaphs"), 0700); err != nil { return err } // Recompute the Merkle root from the file map — this is the canonical // EssenceID. If the caller pre-set EssenceID we verify it matches. computed := ComputeMerkleRoot(sarc.Files) if sarc.EssenceID == "" { sarc.EssenceID = computed } else if sarc.EssenceID != computed { return fmt.Errorf("tomb: essence_id mismatch (stored=%s, computed=%s) — refusing to write taint", sarc.EssenceID, computed) } path := filepath.Join(t.Root, "epitaphs", sarc.EssenceID+".json") data, err := json.MarshalIndent(sarc, "", " ") if err != nil { return err } return os.WriteFile(path, data, 0600) } // GetSarcophagus loads the epitaph for an Essence ID. func (t *Tomb) GetSarcophagus(essenceID string) (*Sarcophagus, error) { path := filepath.Join(t.Root, "epitaphs", essenceID+".json") data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("tomb: sarcophagus %s not found: %w", essenceID, err) } var s Sarcophagus if err := json.Unmarshal(data, &s); err != nil { return nil, err } return &s, nil } // VerifyRoot recomputes the Merkle root of a Sarcophagus from its file // hashes and compares it against the stored EssenceID. Any mismatch // means "Taint" — the Warding must reject Reanimation. // // Note: this only verifies the metadata. To verify the blobs themselves // are not bit-rotted, use VerifyBlobs which re-hashes every blob on disk. func (t *Tomb) VerifyRoot(essenceID string) error { s, err := t.GetSarcophagus(essenceID) if err != nil { return err } computed := ComputeMerkleRoot(s.Files) if computed != essenceID { return fmt.Errorf("tomb: merkle mismatch for %s (expected %s, recomputed %s)", s.SpellName, essenceID, computed) } return nil } // VerifyBlobs re-hashes every blob referenced by the Sarcophagus and // confirms the bytes on disk still match. This is the slow bit-rot check // run by `sorcery tomb verify --all`. func (t *Tomb) VerifyBlobs(essenceID string) error { s, err := t.GetSarcophagus(essenceID) if err != nil { return err } for path, expectedHash := range s.Files { blobPath := t.blobPath(expectedHash) actualHash, err := HashFile(blobPath) if err != nil { return fmt.Errorf("tomb: blob %s missing for %s: %w", expectedHash, path, err) } if actualHash != expectedHash { return fmt.Errorf("tomb: blob %s bit-rotted (path %s)", expectedHash, path) } } return nil } // List returns every Essence currently resting in the Tomb. func (t *Tomb) List() ([]*Sarcophagus, error) { dir := filepath.Join(t.Root, "epitaphs") var out []*Sarcophagus err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } if !strings.HasSuffix(path, ".json") { return nil } data, e := os.ReadFile(path) if e != nil { log.Printf("tomb: skipping unreadable file %s: %v", path, e) return nil } var s Sarcophagus if e := json.Unmarshal(data, &s); e == nil { out = append(out, &s) } return nil }) return out, err } // FindByVariant returns the EssenceID of the Sarcophagus whose VariantHash // matches, or "" when not found. This is the cache-hit check in the Cast // pipeline. func (t *Tomb) FindByVariant(variantHash string) (string, error) { all, err := t.List() if err != nil { return "", err } for _, s := range all { if s.VariantHash == variantHash { return s.EssenceID, nil } } return "", nil } // FindBySpell returns the most recently created Sarcophagus for a spell // (any variant). Used by `cauldron` to pick the latest essence for an ISO. func (t *Tomb) FindBySpell(spell, arch string) (*Sarcophagus, error) { all, err := t.List() if err != nil { return nil, err } var best *Sarcophagus for _, s := range all { if s.SpellName != spell { continue } if arch != "" && s.Arch != arch { continue } if best == nil || s.CreatedAt > best.CreatedAt { best = s } } if best == nil { return nil, fmt.Errorf("tomb: no essence for %s on %s", spell, arch) } return best, nil } // Prune (Garbage Collection) walks the blob store and removes any hash // that is not referenced by an active epitaph. Returns the bytes reclaimed. func (t *Tomb) Prune() (int64, error) { active := make(map[string]bool) epitaphs, err := t.List() if err != nil { return 0, fmt.Errorf("tomb: prune: list epitaphs: %w", err) } for _, s := range epitaphs { for _, h := range s.Files { active[h] = true } } var reclaimed int64 blobDir := filepath.Join(t.Root, "blobs") err := filepath.Walk(blobDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } name := info.Name() if !active[name] { reclaimed += info.Size() _ = os.Remove(path) } return nil }) return reclaimed, err } // blobPath returns /var/lib/sorcery-go/tomb/blobs//. func (t *Tomb) blobPath(hash string) string { if len(hash) < 2 { return filepath.Join(t.Root, "blobs", hash) } return filepath.Join(t.Root, "blobs", hash[:2], hash) } // HashFile computes the sha256 of a file on disk — used by the Committer // and the Cast pipeline before storing a Sarcophagus. func HashFile(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } // ComputeMerkleRoot concatenates every file path + hash (sorted by path) // and hashes the result. This catches a single-bit flip anywhere in the // file set. A real binary Merkle tree would let us localise the failure; // for now we trade precision for simplicity and speed. func ComputeMerkleRoot(files map[string]string) string { keys := make([]string, 0, len(files)) for k := range files { keys = append(keys, k) } sort.Strings(keys) h := sha256.New() for _, k := range keys { h.Write([]byte(k)) h.Write([]byte(files[k])) } return hex.EncodeToString(h.Sum(nil)) }