// 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 }