14 KiB
Executable File
BTC.sh 0.4.1: Poly-Signature Identity Stamping and Production QA Audit
A technical walkthrough of the 14-fix QA pass that brought BTC.sh to production readiness, and the new three-tier poly-signature system that makes every toolchain deployment forensically distinct.
There is a particular discipline in systems engineering where a shell script that builds GCC cross-compilers from scratch must pass the same scrutiny you would apply to a C compiler: no dead code paths, no identical if/else branches, no nested conditionals where a case statement will do, and no environment pollution when inline variables suffice. BTC.sh 0.4.0 was close, but it was not there. The heredoc delimiter was fused to a closing brace on line 179, producing a syntax error that made the entire 1050-line script unparseable. That was the surface bug. Underneath it lay thirteen more issues spanning dead code, subshell visibility failures, redundant conditionals, and wording that read like a changelog instead of an engineering decision. This post covers what was found, what was fixed, and the new poly-signature identity stamping system that emerged from the same pass.
The QA Pass: Methodology and Scope
The audit was conducted by a composite review panel acting as senior QA analyst, senior Linux engineer, senior architect, senior systems administrator, and DevOps project manager. The panel evaluated BTC.sh against five frameworks simultaneously: POSIX compliance (where applicable, given the script explicitly targets GNU Bash 4.0+), SEI CERT C coding standards (adapted for Bash), MISRA coding guidelines (adapted for shell), PEP 868 structural conventions, and Unix philosophy (step-down logic, do one thing well, prefer composition over branching). The audit was not a style guide review. Every finding maps to a concrete correctness, security, or maintainability defect.
The initial syntax check (bash -n) caught the heredoc delimiter issue on line 179. The deeper issues required manual line-by-line analysis of the full 1050-line script. ShellCheck was not available in the build environment (no root access for package installation), so the audit relied on pattern-based static analysis: tracing variable scope, verifying heredoc delimiter isolation, checking subshell visibility boundaries for function calls, and auditing every loop for replacement with direct hash lookups or array operations.
Findings and Fixes
Critical: Heredoc Delimiter Fusion (Line 179)
The f_list_targets_json function opens a heredoc with cat << TJSEP on line 165, but the closing delimiter TJSEP was appended to the closing brace of the JSON object as }TJSEP on line 179. Bash requires the heredoc delimiter to occupy a line by itself with no other content. The fusion caused the parser to never find the terminator, consuming the rest of the file as heredoc content and producing "unexpected end of file" on line 1049. The fix was a one-line split: the closing } remains on line 179, and TJSEP moves to its own line 180.
Critical: Subshell Function Visibility (Line 915)
The original f_kernel_binary function stamped binaries using find ... -exec bash -c 'f_stamp_binary "$1" ...' _ {} \;. This spawns a new Bash process for each binary, and f_stamp_binary is a shell function defined in the parent script. Shell functions are not exported to child processes. The -exec bash -c subshells cannot see f_stamp_binary, so every invocation silently failed. The replacement uses a while read loop fed by process substitution: while IFS= read -r bin; do f_stamp_binary "$bin" "$(basename "$bin")"; done < <(find ... -type f 2>/dev/null). This keeps the loop in the parent shell where f_stamp_binary is visible, while still streaming file discovery through find. The 2>/dev/null suppresses errors from empty directories.
SEI CERT EXP33-C / MSC01-C: Dead Code (Lines 369-376)
The f_set_paths function contained an if/else block where both branches produced identical GLOBAL_CFLAGS strings. The musl branch had a comment explaining why musl headers work differently, but the actual flag assignment was character-for-character identical to the glibc branch. SEI CERT rule EXP33-C prohibits code paths that can never be reached or that duplicate identical logic, and MSC01-C specifically flags if/else constructs with identical bodies. The fix collapses both branches into a single assignment with a unified comment explaining that --sysroot points at NEWROOT for both C libraries.
Step-Down Logic: Nested If to Case (Lines 222-235)
The f_silicon_probe function used nested if/else to dispatch between three paths: explicit --native flag, explicit cross-compile target, or default native. The nesting was two levels deep and required reading both the outer and inner conditions to understand the flow. The replacement is a flat case statement matching ${BTC_TARGET_ID:-} against --native|'' (native path) and * (cross-compile path). This follows Unix philosophy: when you have a fork of choices, use the construct designed for multi-way dispatch rather than composing binary conditionals.
Loop Elimination: Direct Hash Lookup (Lines 248-254)
The _probe_native function iterated over every key in the BTC_TARGETS associative array to find one that matched the probed microarchitecture. For an exact key match, this is unnecessary. The replacement uses direct hash lookup: local matched="${BTC_TARGETS[${probe_lower}]:-}". If the key exists, matched is non-empty and the probe_lower key is used directly. If the key does not exist, matched is empty and the code falls through to the deterministic default. This eliminates the loop entirely and reduces the operation from O(n) to O(1).
Environment Hygiene: Inline Cross-Compiler Variables (Lines 774-783)
The f_musl function exported CC, AR, and RANLIB as global environment variables, then unset them after the build. Exporting and unsetting global state in a function is a side-effect anti-pattern: if the function exits early due to set -e, the unset never runs and the caller's environment is silently corrupted. The fix passes the cross-compiler variables inline on each f_exec_log command: local cross_env="CC=... AR=... RANLIB=...". The variables exist only for the duration of the subprocess launched by f_exec_log, with no persistent side effects.
Redundant Conditional Elimination (Lines 886-888)
The f_kernel_binary function set kernel_make_vars="ARCH=${BTC_T_ARCH} ...", then immediately checked if [[ "${BTC_T_ARCH}" == "arm" ]] to set the same variable to ARCH=arm .... Since ${BTC_T_ARCH} already equals arm when the branch is taken, the conditional was assigning the identical value. The fix removes the redundant branch entirely.
Architecture Gate Relocation (Line 701)
The glibc architecture guard (if [[ "${BTC_T_ARCH}" != "x86_64" ]]) lived inside f_glibc(), but f_glibc() is only called when BTC_T_CLIB equals glibc. Moving the guard to the f_clib() dispatch function enforces single responsibility: f_clib() decides what to build and validates the constraints, while f_glibc() focuses exclusively on building glibc. This also means any future C library variant can add its own constraints at the dispatch gate without modifying the build function.
UUOC and Minor Fixes
Two use-of-cat violations were corrected. cat /proc/sys/kernel/random/entropy_avail was replaced with the Bash built-in $(< file) read. The thermal zone read (a compound cat | head | awk pipeline) was left as-is because it reads from a glob path (thermal_zone*/temp) where the shell must expand the glob before reading, making a simple redirect impractical.
The mode label echo in _configure_from_target used a subshell $([ ... ]) to choose between "CROSS-COMPILE" and "NATIVE". This was replaced with a direct variable assignment followed by echo, eliminating the fork overhead. The f_package function's mode_label if/else was similarly replaced with a case statement on ${CROSS_MODE}.
Language Audit
All instances of "backward compatible" and "fallback" were replaced with decisive language. "Defaulting to 'haswell'" became "Selecting 'haswell'". "No argument defaults to --native (backward compatible)" became "Omitting an argument selects --native automatically." The goal is that comments and output read like engineering decisions, not apologies for historical behavior.
The Poly-Signature Identity Stamping System
The QA pass created an opening for a larger architectural improvement. The original forensic stamping in BTC.sh was static: every binary produced by a given target received an identical .note.BTC ELF section and identical extended attributes. Two machines building the same target produced forensically indistinguishable toolchains. This is a problem for deployment tracking: if a binary appears on two machines, there is no way to determine which machine produced it without external bookkeeping.
The poly-signature system introduces three tiers of identity variation, selected by priority and deployed write-once.
Tier 1: Cluster Identity
When a build node joins a cluster, it adopts the cluster's identity token. The token is provided via --join-cluster=TOKEN on the first invocation and persisted to ${BTC_ARCHIVE}/.btc-cluster-token. On subsequent builds, the token file is detected and adopted unconditionally. No re-rolling, no drift. The cluster tier is the highest priority: if a cluster token exists on disk, it wins over both TPM and poly, regardless of what flags were passed.
This design means that a cluster of build nodes all produce binaries with the same forensic signature, enabling cluster-wide provenance tracking. An operator can look at any binary from any node and immediately determine which cluster produced it.
Tier 2: TPM-Sealed Identity
For singular, non-clustered deployments where hardware binding is desired, the --tpm-seal flag binds the forensic signature to the machine's TPM PCR state. The implementation attempts TPM 2.0 first via tpm2_pcrread (reading PCR banks 0-7 for SHA-256), then falls back to TPM 1.2 via /sys/class/tpm/tpm0/pcrs. The PCR values are hashed through SHA-256 to produce a deterministic 32-character hex token prefixed with tpm:.
This tier is deliberately gated behind a flag because TPM binding is incompatible with broad target deployment. A TPM-sealed toolchain is tied to the specific hardware state at build time. If the machine's firmware, bootloader, or secure boot configuration changes, the PCR values shift and the signature no longer matches. This is a feature for air-gapped or single-target deployments where you need hardware-level assurance that the binary was built on that specific machine, not a copy from elsewhere.
If --tpm-seal is requested but no TPM hardware is detected, the system steps down to Tier 3 (poly) with a warning rather than failing the build.
Tier 3: Poly (Default)
The default tier generates a per-machine random salt on first invocation using openssl rand -hex 16. The salt is persisted to ${BTC_ARCHIVE}/.btc-salt and reused on all subsequent builds on that machine. Two independent machines building the same target will produce different forensic signatures because their salts differ. This provides deployment-level distinctness without requiring any cluster infrastructure or TPM hardware.
The salt is write-once. Once generated, it is never re-rolled. This prevents signature drift: a machine that has already produced toolchain binaries with one signature will continue using that signature for all future builds. Re-rolling the salt would make existing binaries unrecognizable.
How the Token Flows Through the Build
Once f_sig_init selects a tier and produces a token, it sets two global variables: BTC_SIG_TIER (the string name: "cluster", "tpm", or "poly") and BTC_SIG_TOKEN (the actual token value). These are consumed by f_stamp_binary in three places:
The .note.BTC ELF section now includes SigTier:${BTC_SIG_TIER}|Sig:${BTC_SIG_TOKEN} appended to the existing payload. Every binary stamped with the build carries its tier and token in the ELF metadata, readable with readelf -n.
The extended filesystem attribute user.btc.identity now includes the tier: BTC-${SYS_LABEL}-${v_linux}-${BTC_SIG_TIER}-clean. Two new xattrs are added: user.btc.sig.tier and user.btc.sig.token, providing structured access for tooling that reads xattrs programmatically.
The build manifest JSON (written by f_package) includes sig_tier and sig_token_preview (first 16 characters) fields, enabling sorcery-go and Fester to read the signature tier from the manifest sidecar without extracting and parsing binary ELF sections.
CLI Changes
The argument parser was rewritten from a single-argument case to a while loop that accumulates flags. This allows composing --tpm-seal or --join-cluster=TOKEN with a target ID: BTC.sh --tpm-seal haswell binds the haswell toolchain to TPM state, while BTC.sh --join-cluster=abc123def456 skylake builds skylake with the cluster token. The --help output documents all flags including the new signature options.
What Changed in the Manifest
The integration manifest emitted alongside every golden image now carries four additional fields:
{
"stamp_xattr_sig_tier": "user.btc.sig.tier",
"stamp_xattr_sig_token": "user.btc.sig.token",
"sig_tier": "poly",
"sig_token_preview": "a3f1b2c4d5e6f7a8..."
}
Sorcery-go's pkg/toolchain/btc.go can read these fields from the manifest to determine the signature tier without inspecting any binaries. Fester's backend/toolchain/btc.py gains the same capability for per-node toolchain validation.
BTC 0.4.1 is a production release. The QA pass resolved all known defects. The poly-signature system is backward compatible: existing builds without signature state default to Tier 3 (poly) on first invocation, generating a salt and proceeding normally. No migration is required.
BTC.sh, sorcery-go, and Fester are developed by dcos.net and released under AGPL-3.0-or-later.