211 lines
7.7 KiB
Go
Executable File
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
|
|
}
|