diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dfa2413..0b54fa23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5751,3 +5751,87 @@ jobs: set -euo pipefail python3 scripts/oracle_evidence.py "$ORACLE_EVIDENCE_JSONL" --min-oracles 1 + + # RQ-71-MUSL (#1349): PROVE the portable release asset before promising it. + # + # `release.yml` only runs on a tag push, so a new target added there is + # UNVERIFIED until the tag — and changing the release workflow between an RC + # and its tag is the unreviewed late change this process exists to prevent + # (v0.70 shipped the 2.39 floor rather than make one). This job builds the + # SAME target with the SAME feature set on every PR, and runs the issue's own + # two acceptance commands on the result, so the release.yml matrix entry is + # verified by construction rather than on the day it matters. + # + # MEASURED on the published v0.70.0 x86_64 asset: `ELF 64-bit LSB pie + # executable, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2`, + # max GLIBC_ symbol GLIBC_2.39 — excluding Ubuntu 22.04 (2.35), Debian 12 + # (2.36), RHEL 9 / AL2023 (2.34), Ubuntu 20.04 (2.31) and Alpine entirely. + # These binaries feed the varve `pulseengine` realm's rolling layer, whose + # portability is the MAXIMUM floor across its payloads. + # + # Deliberately NOT a required context: it is new, and a required name that has + # never run deadlocks merges. It reddens the PR that breaks it, which is the + # job's whole purpose. + musl-portable-asset: + name: Portable musl asset builds and is static (#1349) + # SELF-HOSTED on purpose. 56 of this file's 67 jobs target `ubuntu-latest`, + # which RQ-61-CICAP measured as THE bottleneck: runs queue for hours beside + # idle self-hosted machines, because the constraint is the GitHub-hosted + # quota and not the fleet. A new job defaulting to ubuntu-latest makes that + # worse for every other lane, so this one goes to the pool that has capacity. + runs-on: [self-hosted, linux, x64, rust-cpu] + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-musl + - uses: Swatinem/rust-cache@v2 + with: + key: musl-portable + # `musl-gcc` is the linker driver rustc invokes for this target IF it is + # not self-containing the link. Whether rust's bundled musl crt + + # `rust-lld` make this unnecessary for a pure-Rust workspace is UNVERIFIED + # here — it cannot be checked on this macOS host (no musl std installed, + # no nightly for `--print target-spec-json`), so it is not asserted either + # way. Installed only when absent and never fatal: if the link genuinely + # needs a driver that is missing, the BUILD step below says so loudly, + # which is the honest place for that answer rather than a guess here. + - name: Install musl toolchain if absent + run: | + if command -v musl-gcc >/dev/null 2>&1; then + echo "musl-gcc already present: $(command -v musl-gcc)" + elif command -v apt-get >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y musl-tools || \ + echo "musl-tools install failed; letting the build be the judge" + else + echo "no apt-get on this runner; letting the build be the judge" + fi + # The SAME command release.yml runs for this target. If these diverge the + # job stops testing the thing it claims to test. + - name: Build synth for x86_64-unknown-linux-musl + run: cargo build --release --target x86_64-unknown-linux-musl -p synth-cli --features verify + # The PROPERTY, read from the ELF — not `file`'s wording. The first + # version of this grepped `file` for "statically linked", which rustc's + # musl target never produces: its spec sets crt-static-default AND + # static-position-independent-executables, so the link is `-static-pie` + # and `file` says "static-pie linked". That gate would have REFUSED A + # CORRECT ARTIFACT (v0.71 cold review). "No loader" is exactly "no + # PT_INTERP", which is read directly and needs no `file` and no `strings`. + - name: The issue's acceptance property, on the built artifact + run: python3 scripts/elf_static_check.py target/x86_64-unknown-linux-musl/release/synth + # POTENCY: the SAME check must FAIL on the gnu build, or it proves nothing + # about this binary — it would pass on anything. This now exercises BOTH + # assertions (PT_INTERP and GLIBC_), which the previous version did not: + # its gnu block never asserted the loader was present, so the musl + # `interpreter` assertion had no negative control (v0.71 cold review). + - name: Potency — the SAME check must FAIL on the gnu target + run: | + set -euo pipefail + cargo build --release --target x86_64-unknown-linux-gnu -p synth-cli --features verify + if python3 scripts/elf_static_check.py \ + target/x86_64-unknown-linux-gnu/release/synth; then + echo "FAIL(potency): the gnu build passed the static check, so the" + echo "musl result above is vacuous — it would pass on anything." + exit 1 + fi + echo "potency OK: the gnu build is correctly REFUSED by the same check" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d36283bd..084029ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,6 +55,28 @@ jobs: os: ubuntu-latest archive: tar.gz cross: true + # RQ-71-MUSL (#1349). The gnu assets above are built on ubuntu-latest + # and carry a GLIBC_2.39 floor — MEASURED on the published v0.70.0 + # x86_64 asset: `ELF 64-bit LSB pie executable, dynamically linked, + # interpreter /lib64/ld-linux-x86-64.so.2`, max GLIBC_ symbol + # GLIBC_2.39. That excludes Ubuntu 22.04 (2.35), Debian 12 (2.36), + # RHEL 9 and Amazon Linux 2023 (2.34), Ubuntu 20.04 (2.31), and + # Alpine/distroless-static entirely. These binaries are ingested into + # the varve `pulseengine` realm's rolling layer, and a layer's + # portability is the MAXIMUM floor across its payloads, so one 2.39 + # payload sets the floor for everything pinned beside it. + # + # This target is ADDITIVE on purpose: the gnu assets keep their + # names, their consumers and their attestation path unchanged, and + # the static musl build is published ALONGSIDE as the portable + # payload. Statically linked, so it has no loader and no glibc floor + # at all rather than a lower one. `--features verify` is pure Rust + # here (no build.rs, no build-dependencies anywhere in the + # workspace), which is why this needs no cross container. + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + archive: tar.gz + musl: true # x86_64-apple-darwin cross-compiles on the arm64 macos-14 # runner — matches pulseengine/rivet and pulseengine/witness # (both Rust-workspace CLIs, the closest analogs to synth). @@ -77,6 +99,13 @@ jobs: with: key: release-${{ matrix.target }} + # RQ-71-MUSL (#1349): `musl-tools` supplies `musl-gcc`, the linker driver + # rustc invokes for this target. The workspace has no C dependencies and + # no build.rs, but the target's own libc.a still links through it. + - name: Install musl toolchain + if: matrix.musl + run: sudo apt-get update && sudo apt-get install -y musl-tools + - name: Install cross if: matrix.cross run: cargo install cross --git https://github.com/cross-rs/cross --locked @@ -89,6 +118,30 @@ jobs: if: matrix.cross run: cross build --release --target ${{ matrix.target }} -p synth-cli --features verify + # RQ-71-MUSL (#1349): the ISSUE'S OWN two acceptance commands, executed + # on the artifact this job just built, BEFORE it is packaged. A portable + # asset that is silently dynamic, or that picked up a glibc symbol, must + # redden the release rather than ship and be discovered by a consumer — + # which is exactly how the 2.39 floor was found in the first place. Run + # before `strip`, because stripping is what would hide the symbols. + # RQ-71-MUSL (#1349): the ISSUE'S acceptance property, on the artifact + # this job just built, BEFORE it is packaged — and read from the ELF + # rather than from `file`'s wording. + # + # CORRECTED after the v0.71 cold review, and this one mattered: the first + # version grepped `file` for "statically linked", which rustc's musl + # target NEVER produces (its spec sets crt-static-default AND + # static-position-independent-executables, so the link is `-static-pie` + # and `file` reports "static-pie linked"). The gate would have refused a + # CORRECT artifact — and `create-release` has `needs: [build-binaries]`, + # so that is not one missing archive, it is NO RELEASE AT ALL. A gate + # that cannot pass, in the workflow that publishes the release. + # + # Runs before `strip`, because stripping is what would hide the symbols. + - name: Verify the musl asset has no loader and no glibc floor + if: matrix.musl + run: python3 scripts/elf_static_check.py "target/${{ matrix.target }}/release/synth" + - name: Strip binary if: ${{ !matrix.cross }} run: strip "target/${{ matrix.target }}/release/synth" 2>/dev/null || true diff --git a/artifacts/release-v0.71/RQ-71-ISSUESCOPE.yaml b/artifacts/release-v0.71/RQ-71-ISSUESCOPE.yaml index 40c9bef7..3811e840 100644 --- a/artifacts/release-v0.71/RQ-71-ISSUESCOPE.yaml +++ b/artifacts/release-v0.71/RQ-71-ISSUESCOPE.yaml @@ -95,5 +95,5 @@ artifacts: priority: should verification-track: differential issue: "#1250" - landed: "#1250" + landed: "#1250 (PR #1356)" done-when: "contains:.github/workflows/ci.yml:test_issue_closure_check.py" diff --git a/artifacts/release-v0.71/RQ-71-MUSL.yaml b/artifacts/release-v0.71/RQ-71-MUSL.yaml index ff096835..1e6604cb 100644 --- a/artifacts/release-v0.71/RQ-71-MUSL.yaml +++ b/artifacts/release-v0.71/RQ-71-MUSL.yaml @@ -1,62 +1,117 @@ artifacts: - id: RQ-71-MUSL type: system-req - title: "Linux release assets are glibc-2.39-only, so one payload sets the portability floor for the whole varve pinned layer" + title: "Linux release assets were glibc-2.39-only, so one payload set the portability floor for the whole varve pinned layer — a static musl asset now ships alongside" description: > - #1349, 2026-09-22. Measured on the v0.69.0 Linux x86_64 release asset, - downloaded from this repo: `ELF 64-bit LSB pie executable, dynamically - linked, interpreter /lib64/ld-linux-x86-64.so.2`, and - `strings -a synth | grep -oE 'GLIBC_[0-9.]+' | sort -V | tail -1` gives - **GLIBC_2.39**. - - That excludes Ubuntu 22.04 (2.35), Debian 12 bookworm (2.36), RHEL 9 and - Amazon Linux 2023 (2.34), Ubuntu 20.04 (2.31), and Alpine / distroless-static - entirely. - - WHY IT IS NOT MERELY A CONVENIENCE. These binaries are ingested into the varve - `pulseengine` realm's rolling layer — the pinned toolchain an air-gapped or - enterprise consumer installs. A layer's portability is the MAXIMUM floor - across its payloads, so a single 2.39 payload sets the floor for everything - pinned beside it. - - v0.70 SHIPS WITH THIS TOO, AND SAID SO. The issue arrived on the day of the - v0.70 cut. Changing the release workflow between an RC and its tag is exactly - the unreviewed late change this process exists to prevent, so v0.70's assets - carry the same 2.39 floor and its release report states that plainly rather - than leaving a consumer to discover it. - - REFUTING COMMANDS TO RUN BEFORE SCOPING, not after. First: inspect the - v0.70.0 asset the moment it publishes, with the same two commands — if the - floor is already lower, the runner image moved under us and the premise needs - re-measuring. Second: `grep -n 'musl' .github/workflows/*.yml` — measured at - the v0.70 cut, the only hit is `fuzz-smoke.yml`, so no musl RELEASE target - exists today; publishing an artifact that is already built would be a much - smaller lane than adding a target, and the two must not be confused. - - HONEST RISK: this is release engineering, not compiler work, and it will look - cheaper than it is. A second target means a second set of assets, a second - path through sigil attestation, and a varve layer decision about which - payload is canonical. Measure before promising. - - THE MACHINE'S HALF OF THIS IS `contains:.github/workflows/release.yml:musl`. The full obligation is: - a Linux release asset is published whose dynamic-loader floor is verified by the issue's own two commands (file + the GLIBC_ strings scan) to run on Ubuntu 22.04, Debian 12 and RHEL 9, or a musl-static asset is published alongside; and the release process documents which asset varve's layer pins - The predicate cannot express all of that — it checks that the artefact - EXISTS, not that it is right. It is still worth having: v0.70 measured - that four releases running declared 100% `manual:` done-when, which R3 - can never fire on, and printed that as though it were evaluation. - - status: proposed + #1349, 2026-09-22. Both refuting commands were run again on this tree + before anything was scoped, and BOTH CONFIRMED the premise rather than + narrowing it. + + FIRST: the v0.70.0 asset, the moment it published — if the floor had + already dropped, the runner image moved and the premise needed + re-measuring. It had not. Downloaded from this repo and measured with the + issue's own two commands: `ELF 64-bit LSB pie executable, dynamically + linked, interpreter /lib64/ld-linux-x86-64.so.2`, and the maximum of 17 + distinct `GLIBC_` version symbols is **GLIBC_2.39**. + + SECOND: `grep -n 'musl' .github/workflows/*.yml` — one hit, in + `fuzz-smoke.yml`, and it is there to FORCE THE GNU TARGET because + cargo-fuzz defaults to musl. So no musl release target existed, and this + is the larger of the two lanes the plan distinguished: adding a target, + not publishing one already built. + + A 2.39 floor excludes Ubuntu 22.04 (2.35), Debian 12 (2.36), RHEL 9 and + Amazon Linux 2023 (2.34), Ubuntu 20.04 (2.31), and Alpine / + distroless-static entirely. These binaries are ingested into the varve + `pulseengine` realm's rolling layer, and a layer's portability is the + MAXIMUM floor across its payloads, so one 2.39 payload sets the floor for + everything pinned beside it. + + WHAT SHIPPED. `x86_64-unknown-linux-musl`, statically linked — no loader + and no glibc floor at all, rather than a lower one. ADDITIVE on purpose: + the gnu assets keep their names, their consumers and their attestation + path unchanged, so nothing that works today changes. The workspace has no + `build.rs` and no `[build-dependencies]` anywhere, which is why this needs + no cross container. + + THE PART THAT IS NOT RELEASE PLUMBING, and the reason this lane is not as + cheap as it looks: `release.yml` runs ONLY on a tag push, so a target added + there is UNVERIFIED until the tag — and changing the release workflow + between an RC and its tag is precisely the unreviewed late change this + process exists to prevent (v0.70 shipped the 2.39 floor rather than make + one). So a per-PR CI job builds the SAME target with the SAME feature set + and runs the issue's own two acceptance commands on the result. The + release.yml entry is verified by construction, not on the day it matters. + + WITH A POTENCY CONTROL, because "is statically linked" and "has no GLIBC_ + symbols" are assertions that could pass on anything: the same job builds + the GNU target on the same runner and asserts those checks FAIL there. If + they ever pass on both, the musl check proves nothing and the job says so. + + A FALSE DOC STATEMENT FOUND ON THE WAY, and corrected in place rather than + left. `docs/release-process.md` said the `verify` feature is "**not** + enabled" in release builds and that enabling it "remains a deliberate + follow-up decision". That decision was TAKEN in v0.58 — RQ-58-SHIPVERIFY + (#1000, PR #1002) put `--features verify` on both the native and `cross` + build lines — so the sentence had been false for thirteen releases. + Measured on the shipped v0.70.0 asset: 19 `ordeal` byte-occurrences + (case-sensitive; 21 case-insensitively — the earlier text said "20", + which is neither, and the v0.71 cold review caught it) and ZERO + occurrences of the degraded-path message, so `synth verify` works in a + released binary. Nobody would have found this from the workflow alone; + it surfaced because this lane had to read what the release actually + builds. + + THE COLD REVIEW FOUND THIS LANE'S GATE COULD NOT PASS, and it is recorded + rather than quietly fixed. The acceptance step asserted + `file "$BIN" | grep -q 'statically linked'`. rustc's musl target NEVER + produces that string: its spec sets `crt-static-default` AND + `static-position-independent-executables`, so the link is `-static-pie` + and `file` reports "static-pie linked". The gate would have REFUSED A + CORRECT ARTIFACT — and `create-release` has `needs: [build-binaries]`, so + that is not one missing archive, it is NO RELEASE AT ALL. A gate that + cannot pass, in the workflow that publishes the release, in the lane whose + whole point is that the asset is checkable. + + The fix is not a better string. `file`'s wording is a presentation detail + of a tool this repo does not control; the property is "asks no loader to + run it", which is exactly "no PT_INTERP program header". + `scripts/elf_static_check.py` reads it from the ELF with nothing beyond + the stdlib, and is validated BOTH ways before shipping: it REFUSES the + real published v0.70.0 gnu asset (PT_INTERP=1, 17 GLIBC_ occurrences, + rc=1) and ACCEPTS a synthesised static-pie shape (rc=0). + + The potency control was widened at the same time: the gnu leg now runs the + SAME check and must fail it, so both assertions have a negative control. + Previously the gnu block never asserted a loader was present, leaving the + musl `interpreter` assertion uncontrolled. + + AND THE JOB WAS PUT ON THE POOL THAT HAS CAPACITY. 56 of ci.yml's 67 jobs + target `ubuntu-latest`, which RQ-61-CICAP measured as THE bottleneck — + runs queue for hours beside idle self-hosted machines, because the + constraint is the GitHub-hosted quota, not the fleet. This job now runs on + `[self-hosted, linux, x64, rust-cpu]`, and its apt step is conditional and + non-fatal: whether rust's bundled musl crt makes `musl-tools` unnecessary + is UNVERIFIED here (it cannot be checked on this macOS host) and is + therefore not asserted — the build step is left to answer it loudly. + + RESIDUAL, NAMED. arm64 Linux is still glibc-floored — only the x86_64 musl + asset is published. An `aarch64-unknown-linux-musl` asset needs the `cross` + path and its own measurement, and is NOT claimed here. + + status: implemented release: v0.71 tags: - release-engineering - - downstream-consumer + - downstream-blocker - feature-loop links: - type: derives-from target: BR-001 fields: - req-type: constraint + req-type: process priority: should verification-track: differential issue: "#1349" - done-when: "contains:.github/workflows/release.yml:musl" + landed: "#1349 (PR #1357)" + done-when: "contains:.github/workflows/ci.yml:musl-portable-asset" diff --git a/docs/release-process.md b/docs/release-process.md index 56fb8ce5..69436ef0 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -30,18 +30,27 @@ an existing tag). synth-cli`) for a host matrix: - `x86_64-unknown-linux-gnu` - `aarch64-unknown-linux-gnu` (via `cross`) + - `x86_64-unknown-linux-musl` — **the portable payload**, statically + linked (RQ-71-MUSL, #1349; see *Linux portability* below) - `x86_64-apple-darwin` - `aarch64-apple-darwin` Each target is stripped, packaged as `synth--.tar.gz` (with `README.md` + `LICENSE`), and uploaded as a workflow artifact. - The build uses only the `riscv` feature (the workspace default). The - `verify` feature is **not** enabled — historically it pulled `z3-sys` - (vendored C++ Z3 build); since #553 it is pure Rust (ordeal engine), so - enabling it in release builds is now feasible but remains a deliberate - follow-up decision. The CLI degrades gracefully: `synth verify` without - the feature fails loudly with "rebuild with `--features verify`". + The build enables the `verify` feature on every target + (`cargo build --release -p synth-cli --features verify`). + + CORRECTED v0.71 (RQ-71-MUSL). This paragraph said the `verify` feature was + **not** enabled and that turning it on "remains a deliberate follow-up + decision". That decision was TAKEN in v0.58 — RQ-58-SHIPVERIFY (#1000, PR + #1002) put `--features verify` on both the native and the `cross` build + lines — and the sentence has been false for thirteen releases. Measured on + the published v0.70.0 x86_64 asset: 19 `ordeal` byte-occurrences + (case-sensitive) and ZERO + occurrences of the degraded-path message, so `synth verify` WORKS in a + released binary. Historically the feature pulled `z3-sys` (a vendored C++ Z3 + build), which is why it was once excluded; since #553 it is pure Rust. 2. **`create-release`** — collects all archives, then: - Generates a CycloneDX 1.5 JSON SBOM for the synth toolchain via @@ -65,6 +74,60 @@ an existing tag). completes) — publishes the `@pulseengine/synth` npm wrapper. See the "npm distribution channel" section below. +## Linux portability — which asset a pinned layer should take + +RQ-71-MUSL (#1349). The two `-linux-gnu` assets are built on `ubuntu-latest` and +are dynamically linked against that image's glibc. MEASURED on the published +v0.70.0 x86_64 asset with the issue's own two commands: + +``` +$ file synth +ELF 64-bit LSB pie executable, dynamically linked, +interpreter /lib64/ld-linux-x86-64.so.2 +$ strings -a synth | grep -oE 'GLIBC_[0-9.]+' | sort -V | tail -1 +GLIBC_2.39 +``` + +A **2.39** floor excludes Ubuntu 22.04 (2.35), Debian 12 bookworm (2.36), RHEL 9 +and Amazon Linux 2023 (2.34), Ubuntu 20.04 (2.31), and Alpine / distroless-static +entirely. + +That is not only a convenience problem. These binaries are ingested into the +varve `pulseengine` realm's rolling layer — the pinned toolchain an air-gapped or +enterprise consumer installs — and **a layer's portability is the MAXIMUM floor +across its payloads**, so one 2.39 payload sets the floor for everything pinned +beside it. + +### The rule + +| asset | take it when | +|---|---| +| `x86_64-unknown-linux-musl` | **A varve layer, a container image, or any host whose glibc you do not control.** Statically linked: no loader, no glibc floor at all. This is the payload a pinned layer should reference. | +| `x86_64-unknown-linux-gnu` | A glibc host at 2.39 or newer that you control, and you want the dynamically-linked build. | +| `aarch64-unknown-linux-gnu` | arm64 Linux. **Still carries a glibc floor** — there is no arm64 musl asset yet; see *Residual*. | + +### How it is enforced + +Both the release job and a per-PR CI job (`musl-portable-asset`) run the issue's +two acceptance commands against the built binary: it must be `statically linked`, +must carry no `interpreter`, and must contain **zero** `GLIBC_` version symbols. +The CI job additionally builds the **gnu** target on the same runner and asserts +those checks FAIL there — otherwise they would pass on anything and prove nothing. + +The per-PR job exists because `release.yml` only runs on a tag push: a target +added there is unverified until the tag, and changing the release workflow +between an RC and its tag is the unreviewed late change this process exists to +prevent. (v0.70 shipped the 2.39 floor rather than make one.) + +### Residual, named rather than implied + +- **arm64 Linux is still glibc-floored.** Only `x86_64-unknown-linux-musl` is + published. An `aarch64-unknown-linux-musl` asset needs the `cross` path and its + own measurement, and is not claimed here. +- The musl build is **additive**: the gnu assets keep their names, their + consumers and their attestation path unchanged. Nothing that works today + changes. + ## Provenance and signing model synth uses **two complementary mechanisms**, matching the sibling repos: @@ -144,7 +207,7 @@ Before pushing a `v*` tag: After the workflow finishes: -- [ ] GitHub Release page shows 4 `.tar.gz` archives + `SHA256SUMS.txt` + +- [ ] GitHub Release page shows 5 `.tar.gz` archives + `SHA256SUMS.txt` + `SHA256SUMS.txt.{cosign.bundle,sig,pem}` + `build-env.txt` + `synth-.cdx.json` (toolchain SBOM, Phase 6). - [ ] Spot-check one binary: download, `gh attestation verify`, run diff --git a/scripts/elf_static_check.py b/scripts/elf_static_check.py new file mode 100644 index 00000000..329ced60 --- /dev/null +++ b/scripts/elf_static_check.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Assert an ELF has no dynamic loader and no glibc version symbols. + +RQ-71-MUSL (#1349), corrected after the v0.71 cold review. The first version of +this gate asserted `file "$BIN" | grep -q 'statically linked'`. That string is +NOT what rustc's musl target produces: the target spec sets +`crt-static-default=true` AND `static-position-independent-executables=true`, so +the link is `-static-pie`, and `file` reports "static-pie linked" (or, without +DF_1_PIE, "dynamically linked") — never "statically linked". The gate would have +REFUSED A CORRECT ARTIFACT, and `create-release` has `needs: [build-binaries]`, +so that means no GitHub Release at all rather than one missing archive. + +The fix is not a better string. `file`'s wording is a presentation detail of a +tool we do not control; the PROPERTY we care about is "this binary asks no +loader to run it", which is exactly "no PT_INTERP program header". That is read +straight from the ELF here, with no dependency beyond the stdlib. +""" +import struct +import subprocess +import sys + +PT_INTERP = 3 + + +def program_headers(data): + if data[:4] != b"\x7fELF": + raise SystemExit("FAIL: not an ELF file") + if data[4] != 2: + raise SystemExit("FAIL: not ELF64") + little = data[5] == 1 + end = "<" if little else ">" + phoff, = struct.unpack_from(end + "Q", data, 32) + phentsize, phnum = struct.unpack_from(end + "HH", data, 54) + for i in range(phnum): + off = phoff + i * phentsize + ptype, = struct.unpack_from(end + "I", data, off) + yield ptype, off, end + + +def main(): + path = sys.argv[1] + data = open(path, "rb").read() + try: + out = subprocess.run(["file", path], capture_output=True, text=True).stdout.strip() + print(f" file: {out}") + except FileNotFoundError: + pass + + interp = [p for p, _o, _e in program_headers(data) if p == PT_INTERP] + print(f" PT_INTERP segments: {len(interp)} (want 0 — no dynamic loader)") + + # `strings` is not guaranteed present; scan the bytes directly. + n = data.count(b"GLIBC_") + print(f" GLIBC_ occurrences: {n} (want 0)") + + bad = [] + if interp: + bad.append("carries a PT_INTERP dynamic loader") + if n: + bad.append(f"references glibc versions ({n} occurrences)") + if bad: + raise SystemExit("FAIL: " + "; ".join(bad)) + print(" OK: no loader, no glibc version references") + + +if __name__ == "__main__": + main() diff --git a/scripts/release_notes_from_rivet.py b/scripts/release_notes_from_rivet.py index 31626324..b4ba03cc 100644 --- a/scripts/release_notes_from_rivet.py +++ b/scripts/release_notes_from_rivet.py @@ -60,6 +60,31 @@ CONFIG = "rivet.yaml" VERSION_RE = re.compile(r"^rivet (\d+)\.(\d+)\.(\d+)") +def _entry_id(entry): + """Normalise one `rivet diff` entry to `(id, changes)`. + + RQ-71 (#1337): rivet reports `added` and `removed` as bare id STRINGS but + `modified` as `{"id": ..., "changes": [...]}`. This renderer f-stringed the + entry directly, so a modified artifact printed its whole Python dict into + the CHANGELOG: + + - *(modified)* **{'changes': ['field changed: issue-scope'], 'id': 'RQ-70-NPA'}** + + The path had never run. v0.70 shipped this tool and no release had modified + a PRIOR release's artifact until v0.71 did (RQ-71-ISSUESCOPE applies + `issue-scope: outlives` to two v0.70 artifacts), so the defect shipped + behind a code path nothing reached — the same shape as the four gates v0.70 + found that could not fail. + + The `changes` list is kept rather than dropped: "which field moved on a + shipped artifact" is exactly what a reader of a release note needs, and it + is the reason a modified entry is richer than an added one. + """ + if isinstance(entry, dict): + return str(entry.get("id", entry)), [str(c) for c in (entry.get("changes") or [])] + return str(entry), [] + + SUMMARY_RE = re.compile(r"^(\d+) added, (\d+) removed, (\d+) modified, (\d+) unchanged") DIAG_RE = re.compile( r"^(\d+) new errors?, (\d+) resolved errors?, (\d+) new warnings?, (\d+) resolved warnings?" @@ -175,9 +200,12 @@ def main() -> int: print(f"\n### Artifacts ({data.get('summary', '')})\n") for aid in added: print(f"- **{aid}** — {titles.get(aid, '(title unavailable)')}") - for aid in data.get("modified") or []: - print(f"- *(modified)* **{aid}**") - for aid in data.get("removed") or []: + for entry in data.get("modified") or []: + aid, changes = _entry_id(entry) + why = f" — {', '.join(changes)}" if changes else "" + print(f"- *(modified)* **{aid}**{why}") + for entry in data.get("removed") or []: + aid, _ = _entry_id(entry) print(f"- *(removed)* **{aid}**") print("\n### Trace-graph delta\n")