You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

24 KiB

G-172 Distributed binaries declare a CPU floor - no shipped artifact is tuned to its build host

Status: achieved 2026-08-07 Scope: src/buildsuites/suite_tcl90/build905.zig and src/buildsuites/suite_tcl86/build86.zig (flagless target/cpu resolution defaults); src/tools/punkzip/build.zig and src/tools/punkres/build.zig (distributed build-path tools); src/buildsuites/suite_tcl90/tools/family_artifacts.tcl (artifact record emission - the recorded floor field); punkbin artifact repo (external c:/repo/jn/punkbin - win32-x86_64/.toml sidecars, defaults.txt curation); bin/ (the shipped zig-built exes); src/scriptapps/bin/punk-runtime. (floor reporting on fetch, floor-vs-host verdict on use/run); scriptlib/developer/ (cpu-floor audit tool) Goal: Every binary this project distributes runs on any CPU meeting a declared instruction-set floor - the floor is a property of the build recipe rather than of whichever machine happened to run it, it travels with the artifact in its metadata record, and neither a build nor a publication can silently ship host-tuned code. Acceptance: A flagless build of each distributed-artifact recipe (suite_tcl90, suite_tcl86, punkzip, punkres) on an AVX-512-capable host emits zero instructions above the declared floor - verified by an audit tool that disassembles the produced binaries and reports out-of-floor instructions, carrying the current AVX-512 regression as its fixture; -Dcpu=native still yields a host-tuned local build, proving the floor is a default and not a restriction; each published artifact's metadata record states the floor it was built to and punk-runtime surfaces it; and selecting or launching a runtime whose recorded floor the local CPU does not meet produces a named diagnosis instead of a silent 0xC000001D, while fetching for any platform stays ungated.

Context

Reported 2026-08-06: on a freshly pulled checkout, bin/punk-runtime.cmd fetch with no runtime name retrieved the curated default tclsh9.0.5-punk-r2.exe, which exited immediately with no prompt, no error and no output. Two other runtimes fetched onto the same machine (tclsh902z.exe, tclsfe-x64.exe) started normally. The failure was initially read as a Windows-version difference (25H2 works, 23H2 does not).

It is not an OS difference. Disassembling .text of the artifacts:

artifact zmm sites ymm sites built by
tclsh9.0.5-punk-r2.exe 2756 617 zig 0.16.0, ReleaseFast
tclsh9.0.5-r2.exe (plain) 2756 617 zig 0.16.0, ReleaseFast
tclsh902z.exe 0 0 BAWT / MSYS2 mingw GCC
tclsfe-x64.exe 0 0 apnadkarni MSVC

The 9.0.5 binaries carry real EVEX-encoded AVX-512 - vmovdqu64, vpscatterqq, vpermt2q, vpmovm2q, vptestnmq, vshufi64x2 - with the first site at RVA 0x2360, the very front of .text. Tcl has no runtime CPU dispatch, so none of it is guarded. On a CPU without AVX-512 the first one raises #UD and the process is killed with STATUS_ILLEGAL_INSTRUCTION (0xC000001D, errorlevel -1073741795) before Tcl writes a byte.

Confirmed against the reporting fleet - the split is exactly the AVX-512 line:

host CPU uarch AVX-512 9.0.5 runtime
is-vmhost-1 (Win11 23H2) TR PRO 3955WX Zen 2 no dies, errorlevel -1073741795
jcross1 (Win11) Ryzen 5 7600X Zen 4 yes works
build/dev box (Win11 25H2) TR PRO 9955WX Zen 5 yes works

Cause: the recipes called b.standardTargetOptions(.{}) and no build path passed -Dtarget/-Dcpu. With no flag, zig resolves cpu_model determined_by_arch_os by NATIVE detection; on a Zen 5 host that is znver5, and ReleaseFast then auto-vectorises freely. Measured with the bundled 0.16.0-dev zig on an auto-vectorisable C loop: flagless -> znver5, zmm=805; -Dcpu=native -> znver5, zmm=805; -Dtarget=x86_64-windows -> x86_64, zmm=0; -Dcpu=baseline -> x86_64, zmm=0; -Dcpu=x86_64_v2 -> x86_64_v2, zmm=0.

The class is wider than the runtime family. Every zig-built binary the project distributes inherits its build host's instruction set:

  • all six published tclsh9.0.5-* artifacts (punk + plain, r1 + r2, both -bi- variants): 2756 zmm each - the whole family, so there is no 9.0.5 fallback to fetch instead;
  • bin/punkzip.exe 1724 zmm, bin/punkres.exe 1423 zmm - both build-path tools, so a bake on a non-AVX-512 machine dies too;
  • bin/punk905.exe, punk9-dev.exe, punk9_beta.exe, punk9bi_beta.exe, punkdeclare.exe, punkfiledemo.exe: 2756 zmm;
  • an older generation (punk901*.exe, punk9.exe, xcritcl.exe, tclsh90s*.exe, mkzipfix.exe) carries ~4500 ymm sites and no AVX-512 - AVX2-only, from an earlier build host. Same defect, milder floor.

Two things make this worse than a portability nit. The artifact record already carries toolchain, optimize and six source-checkout digests but says nothing about the instruction set, so the one fact that determines whether the binary can execute is the one fact not recorded. And punkbin defaults.txt points win32-x86_64 at tclsh9.0.5-punk-r2.exe, so a flagless fetch on a fresh checkout hands every non-AVX-512 machine a dead default runtime - the project's first contact with a new user fails with no message at all.

Approach

  1. Recipe default, not a build-command convention. standardTargetOptions takes .{ .default_target = .{ .cpu_model = .baseline } }, so a flagless build is portable and -Dcpu=native is the opt-in for a host-tuned local build. "Remember to pass -Dcpu= when publishing" is precisely what failed silently for six artifacts and four recipes; the default is what publishes. (Applied 2026-08-06 to all four recipes - see Progress.)
  2. Floor choice: baseline (x86-64 v1) for the x86_64 family (user decision 2026-08-06). For a Tcl interpreter the codegen delta against v2/v3 is negligible against the cost of the default runtime failing to start. The win32-ix86 lane (G-130) makes the same choice on its own axis.
  3. Record the floor in the artifact metadata: [provenance] gains the resolved cpu model and the floor the recipe declared, emitted by family_artifacts.tcl alongside toolchain/optimize, and carried in both the embedded record and the sidecar toml.
  4. Audit tool in scriptlib/developer/ (the advisory-tool tier): disassemble a binary, classify instructions against a named floor, report anything above it. The current AVX-512 artifacts are its fixture - the tool must flag them and must pass a rebuilt one. This is what makes the acceptance criterion measurable rather than a promise about build habits.
  5. punk-runtime surfaces the floor, gating only where execution is implied. fetch stays ungated and reports the floor as information - fetching for another platform (-platform <p>) is a normal cross-machine workflow and a local-CPU verdict would be meaningless there (user point 2026-08-06). The actionable moment is selection and launch: use and run are already the local-only actions (run explicitly takes no -platform because "foreign binaries are not runnable here"), so that is where a floor the host does not meet becomes a named diagnosis. list annotates rows the local CPU cannot run.
  6. Republication is IN-SITU replacement (user decision 2026-08-06): rebuilt binaries replace the unsuitable artifacts under their existing names and revisions, with sidecar tomls and sha1sums.txt regenerated to match. Artifact immutability remains the long-term intention but is explicitly violable during alpha - every consuming system is in-house, there are no third-party users, and leaving known-dead binaries fetchable is the worse outcome. No defaults.txt repoint is needed (it already names tclsh9.0.5-punk-r2.exe). Replacement may proceed as soon as a rebuilt binary passes the zmm audit; if the floor field (Approach 3) is not yet in the record schema, a second in-situ record refresh adds it later - cheap under the same dispensation.

Alternatives considered

  • Pass -Dcpu=baseline at publish time only, leaving recipes native by default
    • rejected: it is the same unenforced convention that produced the defect, and it makes a developer's local build and the published artifact differ in a way nothing checks.
  • x86_64_v2 or x86_64_v3 floor - deferred, not rejected. v2 (SSE4.2/POPCNT, 2008+) excludes nothing realistically in service; v3 (AVX2/BMI2, 2013+) excludes Intel N-series/Pentium Silver, which have no AVX at all. Revisit per artifact class if a measured win justifies it; the recipe already accepts -Dcpu=<model>.
  • Runtime CPU dispatch in the hot paths - rejected as out of proportion: Tcl upstream has none, and the goal is that a shipped binary starts everywhere, not that it is optimal everywhere.
  • Detect and report at first run instead of building portably - rejected: the failure is #UD before main, so there is no point at which our own code could report anything.

Notes

  • Related: G-105 - cross-target builds parameterize the same recipe's target; this goal is the CPU axis of that surface. Whichever lands first, the other inherits the resolved default_target shape.
  • Related: G-130 - the win32-ix86 lane consumes the floor decision on its own axis (an i686 baseline floor is the same class of choice).
  • Related: G-116, G-108 - additional products of the same recipe (tcltls bi-family battery, the debug -dbg<n> tier); both inherit the pin.
  • Related: G-142 - curated listing manifests are where a per-artifact floor becomes visible before download.
  • Related: G-147 - fetch-side sibling; a floor field joins the facts a revision check reads.
  • Related: G-137 - punkres RT_VERSION stamping consumes bin/punkres.exe, itself one of the affected binaries.
  • Related (archived): G-117 (self-describing runtimes - see goals/archive/G-117-self-describing-runtimes.md) and G-123 (runtime tiers, schema v2 - see goals/archive/G-123-thirdparty-runtime-tiers.md) - the artifact record schema this goal extends with the floor field.
  • Related (archived): G-126 (punkzip accelerator), G-128 (punkres stamper) - the two distributed tools whose recipes carry the same defect.
  • Related (archived): G-102 - the suite zig-version pin lives in the same comptime block as the defect.
  • Overlap survey 2026-08-06 (goals_xref.tcl paths src/buildsuites/suite_tcl90 bin/punkzip.exe bin/punkres.exe): surveyed and judged not related in substance - G-110 (shared-lib extraction cache), G-131 (boot payload autodetection), G-141, G-157, G-158 (bin/ sibling-file matches only).
  • Zig version gate (corrected 2026-08-06 - an earlier note here claimed the gate blocks rebuilding on this box; it does not): the recipes' SemVer gate does reject the stray prerelease copy under bin/tools/zig-0.16.0-dev.254+6dd0270a1/ (a prerelease sorts below its release), but that copy is not the pinned toolchain. The zigpin record (sources.config) points at bin/tools/zig-x86_64-windows-0.16.0/zig.exe, a released 0.16.0 that is present, reports 0.16.0, and passes the gate; suite.tcl and the README both resolve to it by default. The removed-zig claim arose from missing the zig-x86_64-windows-* naming family in bin/tools/ and back-inferring a removal from the artifact records. Rejecting a 0.16.0-dev snapshot is arguably correct behavior besides - it predates the 0.16 API the recipes are written against - so nothing here blocks or needs fixing.
  • Verification note: llvm-objdump -d --section=.text <exe> counting zmm[0-9] operands is the quick manual check used throughout the Context measurements. Linear disassembly of a non-AVX-512 binary yields ~3 false positives from data misparse; real usage is in the hundreds-to-thousands, so the distinction is unambiguous at a glance but the audit tool (Approach 4) should decode properly rather than grep.

Progress

  • 2026-08-06: recipe pins applied (Approach 1+2) - default_target with cpu_model = .baseline in src/buildsuites/suite_tcl90/build905.zig, src/buildsuites/suite_tcl86/build86.zig, src/tools/punkzip/build.zig and src/tools/punkres/build.zig, each carrying the finding in-comment. Verified by zig ast-check on all four; the mechanism verified separately with the same toolchain and optimize level on an equivalent recipe (flagless -> baseline/zmm=0, -Dcpu=native -> znver5/zmm=805). A full suite build was NOT run in that session - recorded at the time as gate-blocked, corrected in the next entry (the pinned toolchain passes the gate). The b.graph.host target sites elsewhere in these recipes are build-time helper tools that are never distributed and are correct as native.

  • 2026-08-06 (correction, later session): the gate-blocker claim is wrong - the pinned toolchain bin/tools/zig-x86_64-windows-0.16.0/zig.exe (the zigpin record) is a released 0.16.0, present on this box and passing the recipes' gate; only the stray dev copy is rejected. Notes bullet rewritten accordingly. Nothing blocks the rebuild here.

  • 2026-08-06: user decision - republication is in-situ replacement under the existing artifact names/revisions (Approach 6 rewritten); punkbin immutability is deliberately violable during alpha with zero third-party consumers. Disposition of the r1-generation artifacts (replace vs drop) remains a pending curation call.

  • 2026-08-06 (later session): Approach 6 executed for the r2 generation + tools. Full zig build bootstrap with the pinned released 0.16.0 rebuilt the family at the baseline floor: all three r2 exes audit zmm=0 ymm=0 (was 2756/617), family_check self-contained PASS, punk-r2 build_id unchanged. punkzip 2.4.0 / punkres 0.3.1 rebuilt (zmm 1724/1423 -> 0), own suites pass; local bin/ copies replaced. Core test-gate: two consecutive PASS runs against the dispositioned baseline (69552 run / 8 failed, identical totals both runs; a first attempt failed once on interp-36.7, a bgerror timing race - passed on both reruns; one intermediate run hung 4h inside httpProxy.test "ThreadLevel 1", the socket-flake class - killed and rerun). lib tier audited clean (tcllibc.dll zmm=0; the critcl -target pin already protected it). Published in-situ: punkbin ab5611d (3 exes + 3 schema-2 tomls + 2 versioned tool exes + both sha1sums.txt), pushed to origin/master. Record-honesty residual: the published tomls' [tests] lines carry the emission-time (July) summaries; the fresh shipped-codegen gate evidence lives in _build testreports - fold a [tests] refresh into the Approach 3 record work.

  • Remaining for acceptance: Approach 3 (floor in the artifact record, folding in a [tests]-freshness refresh of the replaced tomls), 4 (audit tool + fixture), 5 (punk-runtime surfacing and the use/run verdict). Behind those: r1-generation disposition (replace vs drop) and the bin/ punk9* kit rebake with the new runtime (punkproject version-gate discipline applies).

  • 2026-08-07: goal activated at user direction (overlap survey re-run via goals_xref.tcl score G-172 - no goals drafted in the interval; the existing Related: notes cover every overlapping pair). Approaches 3, 4, 5 CODE landed:

    • Approach 3 (record floor field): src/buildsuites/suite_tcl90/build905.zig declares const cpu_floor = "baseline"; beside the default_target pin and writes cpu_floor + cpu_model (=target.result.cpu.model.name, resolving to "x86_64" for the default build, "znver5" for -Dcpu=native) into the embedded record's [provenance], after toolchain/optimize (ast-check PASS; 17 format placeholders = 17 args). tools/family_artifacts.tcl derives both from the embedded record (G-117 single-source-of-truth, like origin/packager) and emits them into the sidecar [provenance], failing if absent. tools/family_check.tcl asserts embed_cpu_floor/embed_cpu_model non-empty. suite_tcl86 has no family machinery (G-158 territory) - only the build86.zig cpu_model pin (already in place). NOT yet built/published - the floor field reaches published artifacts at the next republication.

    • Approach 4 (audit tool): scriptlib/developer/cpufloor_audit.tcl (plain tclsh, no deps) disassembles a binary's .text via a located objdump (llvm-objdump preferred, GNU objdump fallback) or audits a pre-disassembled excerpt (-from-dis), classifies by AVX register width (zmm=v4, ymm=v3; baseline/v2 forbid both, v3 forbids zmm, v4 allows all - SSE4.2 not width-detectable so baseline and v2 share a ceiling), reports out-of-floor count + first RVA + sample mnemonics, exit 0 PASS / 1 FAIL. Selftest (-selftest) runs 12 fixture/floor cases - all pass. Fixtures are REAL disassembly excerpts in scriptlib/developer/cpufloor_fixtures/ (avx512, avx2, clean) generated from a temp-built probe on this Zen 5 host (no binary committed). Verified against real binaries: rebuilt bin/punkzip.exe PASS (zmm=0 ymm=0); a -Dcpu=native probe FAILs at baseline (123 zmm, 151 out-of-floor, first RVA 140002952) and PASSes at v4. The acceptance's measurable instrument is in place.

    • Approach 5 (punk-runtime surfacing): both payload twins updated and re-wrapped. src/scriptapps/bin/punk-runtime.ps1 adds Get-PunkHostCpuLevel (IsProcessorFeaturePresent via Add-Type P/Invoke; PF SSE2=10/SSE4.2=38/AVX2=40/AVX512F=41 -> level 1..4, cached, 0 on detection failure), Get-PunkCpuModelLevel (x86_64/baseline->1, _v2->2, _v3->3, _v4->4, else 0), Get-PunkCpuFloorVerdict (meets|below|unknown| norecord; below => block). punk-runtime.bash mirrors with host_cpu_level (/proc/cpuinfo flags avx512f/avx2/sse4_2/sse2), cpu_model_level, cpu_floor_verdict (sets globals). Wired: fetch prints cpu floor: floor=.. model=.. (UNGATED - cross-platform fetch stays free); list metadata_summary adds floor= and !CPU-FLOOR:v<req> (local-platform rows only); use gates on the local platform (below => exit 1, named diagnosis); run gates before launch (below => exit 1); info adds cpu_floor/cpu_model to the embedded-vs-sidecar field table. $host is a read-only automatic variable in PowerShell - the PS verdict uses $hlevel (caught during testing). Re-wrapped via punk::mix::commandset::scriptwrap::multishell punk-runtime -askme 0 -force 1 from src/scriptapps/bin; wrap is deterministic (re-wrap diff = identical); the scriptwrap_runtime_cmd_roundtrip_no_drift test PASSES. Host detection verified (PS level=4 on this Zen 5 box; bash /proc/cpuinfo level=4 in git-bash); verdict logic verified with a mocked v2 host (x86_64_v4 -> below/block, znver5 -> unknown, x86_64 -> meets) in both twins.

    Remaining for acceptance (publication-time, not code): rebuild the family + tools with the floor field in the record, republish in-situ to punkbin (per Approach 6's alpha dispensation), and run the audit tool against the republished artifacts to confirm zmm=0 (the acceptance's "flagless build emits zero instructions above floor"). The [tests]-freshness refresh of the replaced tomls folds into that republication. A real end-to-end use/run below block requires a host whose level is below a published artifact's recorded cpu_model - not demonstrable on this v4 host with baseline-floor artifacts (every host meets v1); the mechanism is verified by the mocked-host tests. Behind those: r1-generation disposition and the bin/ punk9* kit rebake.

  • 2026-08-07: APPROACH 6 executed (rebuild + republish) - the acceptance is met. Full zig build bootstrap (default steps: install, install-libraries, make-zipfs, smoke, tklib, tcllib, tcllibc, tcllibc-linux, kit-family, kit-family-artifacts, library-artifacts) with the pinned released 0.16.0 rebuilt the r2 family WITH the Approach 3 floor field now in the embedded [provenance] record (re-stage picked up the 2026-08-07 build905.zig edit; zig-cached C compiles, only the wrap + emit re-ran). family_check passed for all three members (the new embed_cpu_floor/embed_cpu_model assertions are green). Audit of the rebuilt r2 exes: all three PASS at the baseline floor with zmm=0 ymm=0 out-of-floor=0. Published in-situ to punkbin (git 59e7475, pushed to origin/master): the three r2 exes + three r2 tomls replaced under their existing names/revisions; each toml [provenance] now carries cpu_floor="baseline" + cpu_model="x86_64"; the [tests] tclcore line refreshed to the shipped-codegen gate evidence (passed=56041 failed=8, the Aug-06 baseline-rebuild test-gate - was 56039/9); library [tests] lines unchanged; sha1sums.txt: the 6 r2 lines (3 exes + 3 tomls) updated. build_id unchanged per artifact (the floor field does not enter the identity digest - intended); sha1/size/built updated (the embedded record text changed). The [tests] refresh folded in (the record-honesty residual from the prior republish is cleared).

    Verification of every acceptance conjunct (2026-08-07, this Zen 5 / v4 host):

    • flagless build emits zero instructions above the declared floor: the three republished r2 exes audit PASS (zmm=0) via scriptlib/developer/cpufloor_audit.tcl (also re-verified against the punkbin working-tree copies post-republish).
    • -Dcpu=native still yields a host-tuned local build, proving the floor is a default not a restriction: a temp probe built -mcpu=baseline -> zmm=0 PASS at baseline; -mcpu=native -> zmm=123 FAIL at baseline (out-of-floor=151, first RVA 140002952) and PASS at the v4 floor. (punkzip/punkres were already rebuilt+republished at zmm=0 in the prior session - the build-path tools are clean; suite_tcl86's lib tier (tcllibc.dll) was audited clean then too.)
    • each published artifact's metadata record states the floor: the republished tomls carry cpu_floor/cpu_model in [provenance] (verified by diff vs the prior ab5611d tomls and by punk-runtime info).
    • punk-runtime surfaces it: info shows the cpu_floor/cpu_model rows (embedded = sidecar, sha1 matches); list shows floor=baseline in the metadata summary; use selects cleanly on the meets verdict (this v4 host meets the baseline/v1 floor); fetch prints cpu floor: floor=.. model=.. and stays ungated (code-verified; cross-platform fetch for another machine is not gated by a local-CPU verdict).
    • selecting/launching a runtime whose recorded floor the local CPU does not meet produces a named diagnosis instead of a silent 0xC000001D: verified by the mocked-host tests (2026-08-07, both .ps1 and .bash twins) - a recorded cpu_model=x86_64_v4 against a mocked v2 host yields the below verdict with the 0xC000001D message and use/run exit 1. NOT demonstrable end-to-end on this v4 host with a real published artifact: every published artifact is baseline (v1) which every host meets, and no x86-64 level exceeds v4 (the host's level), so no real below-floor artifact can exist here. The behavior is present and verified by the mocked-host tests; the limitation is the test host, not the implementation.
    • fetching for any platform stays ungated: code-verified (fetch prints the floor as information and never gates on the local CPU; the cross-platform -platform

      workflow is a normal cross-machine path).

Follow-ons

Follow-on: r1-generation artifact disposition (replace vs drop) - the r1 exes remain AVX-512-only (2756 zmm each); under the in-situ alpha dispensation they could be replaced at baseline too, or dropped from defaults curation. No live goal home. => open Follow-on: bin/ punk9* kit rebake with the new floor-bearing runtime (punkproject version-gate discipline applies - the floor surfacing code shipped in 0.57.1; a rebake stamps it into kits). No live goal home. => open