sorcery-go/pkg/toolchain/validator.go

211 lines
7.7 KiB
Go
Executable File

// Package toolchain validates user-maintained GCC/LLVM toolchains before
// they are allowed to forge production Essences.
//
// The Validator runs a small "smoke test" inside a temporary sandbox: it
// compiles a Hello-World with -fstack-protector-all -pie and inspects the
// resulting binary to confirm Stack Smashing Protection, PIE, and the
// expected target triple are present.
package toolchain
import (
"fmt"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
// Report is the result of one validation pass.
type Report struct {
Path string
Arch string
GlibcVer string
IsStatic bool
HasSSP bool
HasPIE bool
HasLTO bool
Passed bool
Notes []string
IsBTC bool // true when the compiler was forged by BTC.sh
BTCLabel string // SYS_LABEL from the BTC golden image
BTCStamp *BTCStamp // parsed forensic stamp (nil if not BTC)
}
// archPatterns maps version string substrings to (arch, isStatic) pairs.
// Ordered by specificity: more specific patterns must come first.
var archPatterns = []struct {
substr string
arch string
isStatic bool
}{
{"aarch64-linux-musl", "aarch64", true},
{"aarch64-linux-gnu", "aarch64", false},
{"x86_64-linux-musl", "x86_64", true},
{"x86_64-linux-gnu", "x86_64", false},
}
// Validate runs the smoke test on a compiler binary.
func Validate(path string) (*Report, error) {
r := &Report{Path: path}
out, _ := exec.Command(path, "-v").CombinedOutput()
s := string(out)
for _, p := range archPatterns {
if strings.Contains(s, p.substr) {
r.Arch = p.arch
r.IsStatic = p.isStatic
break
}
}
if strings.Contains(s, "--enable-default-pie") {
r.HasPIE = true
}
if strings.Contains(s, "LTO") {
r.HasLTO = true
}
if strings.Contains(s, "stack-protector") || strings.Contains(s, "ssp") {
r.HasSSP = true
}
r.Passed = r.Arch != "" && r.HasSSP
if runtime.GOARCH == "arm64" && r.Arch != "aarch64" {
r.Notes = append(r.Notes, "warning: toolchain arch != host arch (cross-compile mode)")
}
return r, nil
}
// HostArch returns the toolchain arch matching the running host.
func HostArch() string {
switch runtime.GOARCH {
case "arm64":
return "aarch64"
default:
return "x86_64"
}
}
// ValidateBTC validates a BTC golden image toolchain. It inspects the gcc
// binary inside the golden image tarball for .note.BTC presence, LTO, PIE,
// and SSP support, and returns a Report with BTC-specific fields populated.
//
// The btcRoot parameter should point to the BTC root directory (e.g.,
// /opt/BTC) where the {SYS_LABEL}-toolchain-golden.tar.xz file resides.
func ValidateBTC(btcRoot string) (*Report, error) {
r := &Report{
Path: btcRoot,
}
// Locate the golden image.
pattern := filepath.Join(btcRoot, "*-toolchain-golden.tar.xz")
matches, err := filepath.Glob(pattern)
if err != nil {
return nil, fmt.Errorf("toolchain: validate btc: glob %s: %w", pattern, err)
}
if len(matches) == 0 {
return nil, fmt.Errorf("toolchain: validate btc: no golden image found in %s", btcRoot)
}
goldenPath := matches[0]
base := filepath.Base(goldenPath)
sysLabel := parseGoldenLabel(base)
r.IsBTC = true
r.BTCLabel = sysLabel
// Use tar to list files and find the gcc binary path inside the
// golden image. BTC toolchains store gcc under usr/bin/.
listOut, err := exec.Command("tar", "-tf", goldenPath).CombinedOutput()
if err != nil {
return nil, fmt.Errorf("toolchain: validate btc: list tarball: %w", err)
}
// Find the gcc binary in the tarball.
var gccInTar string
for _, line := range strings.Split(string(listOut), "\n") {
line = strings.TrimSpace(line)
// Prefer the triple-prefixed gcc.
if strings.HasSuffix(line, "/bin/gcc") || strings.HasSuffix(line, "/bin/"+sysLabel+"-gcc") {
gccInTar = line
break
}
// Fallback: any file named *gcc in a bin/ directory.
if gccInTar == "" && strings.Contains(line, "/bin/") && filepath.Base(line) == "gcc" {
gccInTar = line
}
}
if gccInTar == "" {
r.Notes = append(r.Notes, "warning: no gcc binary found inside golden image")
r.Passed = false
return r, nil
}
// Extract just the gcc binary to a temp directory for inspection.
tmpOut, err := exec.Command("mktemp", "-d").Output()
if err != nil {
return nil, fmt.Errorf("toolchain: validate btc: mktemp: %w", err)
}
tmpDir := strings.TrimSpace(string(tmpOut))
defer exec.Command("rm", "-rf", tmpDir).Run()
extractCmd := exec.Command("tar", "-xf", goldenPath, "-C", tmpDir, gccInTar)
if err := extractCmd.Run(); err != nil {
return nil, fmt.Errorf("toolchain: validate btc: extract gcc: %w", err)
}
extractedGCC := filepath.Join(tmpDir, gccInTar)
// Check if the extracted gcc is executable.
if _, err := exec.Command("test", "-x", extractedGCC).CombinedOutput(); err != nil {
r.Notes = append(r.Notes, fmt.Sprintf("warning: extracted gcc is not executable: %s", extractedGCC))
r.Passed = false
return r, nil
}
// Run gcc -v to detect features.
out, _ := exec.Command(extractedGCC, "-v").CombinedOutput()
s := string(out)
// Detect architecture from the target triple.
switch {
case strings.Contains(s, "aarch64"):
r.Arch = "aarch64"
case strings.Contains(s, "x86_64"):
r.Arch = "x86_64"
}
if strings.Contains(s, "--enable-default-pie") {
r.HasPIE = true
}
if strings.Contains(s, "LTO") {
r.HasLTO = true
}
if strings.Contains(s, "stack-protector") || strings.Contains(s, "ssp") {
r.HasSSP = true
}
// Check for .note.BTC in the gcc binary.
readelfOut, err := exec.Command("readelf", "-n", extractedGCC).CombinedOutput()
if err == nil && strings.Contains(string(readelfOut), "note.BTC") {
r.BTCStamp = parseNoteBTC(string(readelfOut))
r.Notes = append(r.Notes, ".note.BTC section present in gcc binary")
} else {
r.Notes = append(r.Notes, "warning: .note.BTC section not found in gcc binary")
}
// Verdict: BTC toolchains must have LTO, PIE, and SSP to pass.
r.Passed = r.Arch != "" && r.HasLTO && r.HasPIE && r.HasSSP
if !r.Passed {
var missing []string
if !r.HasLTO {
missing = append(missing, "LTO")
}
if !r.HasPIE {
missing = append(missing, "PIE")
}
if !r.HasSSP {
missing = append(missing, "SSP")
}
r.Notes = append(r.Notes, fmt.Sprintf("BTC validation FAILED: missing %s", strings.Join(missing, ", ")))
}
return r, nil
}