Skip to content

feat(release): renew advisory baselines from rehearsal evidence - #1074

Merged
jeremi merged 2 commits into
mainfrom
feat/release-advisory-renewal-tooling
Sep 15, 2026
Merged

jeremi merged 2 commits into
mainfrom
feat/release-advisory-renewal-tooling

Conversation

@jeremi

@jeremi jeremi commented Sep 15, 2026

Copy link
Copy Markdown
Member

Pull Request

Summary

Release tooling (release/). This automates the mechanical part of renewing advisory baselines from rehearsal evidence, so human review can focus on whether each exposure claim still holds. It also adds a review-only ELF exposure report to the rehearsal evidence artifact.

  • registry-release renew-advisory-baselines (release/scripts/advisory_renewal.py) takes a downloaded release-advisory-evidence-* artifact and a source revision copied independently of it. It moves only the machine bindings of already-reviewed exceptions onto that evidence:

    • application and component layer IDs
    • reviewed_at
    • file digests re-hashed from the exported rootfs and matched against native Syft
    • reference image digest, source revision, and local_reproduction provenance
    • runtime and assertion definition digests
    • the live pins in test_check_advisory_baselines.py

    A dry run is the default; --write is required to change files.

  • ELF exposure report (release/scripts/image_exposure.py): the rehearsal collector now writes exposure/<name>.json for each image's Entrypoint executable. The report lists:

    • DT_NEEDED, the interpreter, and undefined dynamic symbols
    • dynamic-loading imports and relocations against dlsym/dlvsym
    • every located dlsym/dlvsym call, jump, or address reference, via the PLT or the GOT

    A constant symbol name is reported only when a RIP-relative lea into %rsi can be proven with no branch target in between. Anything else is marked review_required with a reason. The report is not a gate.

  • release/OPERATIONS.md: the rehearsal advisory section now uses the subcommand (dry run, review, --write, then a second dry run that must report no changes) instead of hand-run jq checks and inline Python digest recomputation. It also describes the exposure report.

  • Smaller changes:

    • release_candidate.advisory_baseline_path is now the single mapping from image name to baseline path.
    • CI runs both new test modules.

Checks

  • New and changed tests:
    • test_advisory_renewal.py: 24 tests covering every refusal, all-or-nothing writes, idempotency, and CLI wiring.
    • test_image_exposure.py: 48 tests. On macOS 2 are skipped; they are Linux-only and compile real PLT and -fno-plt GOT binaries.
    • test_collect_rehearsal_advisory_evidence.py: 16 tests.
  • Existing suites, all passing: test_check_advisory_baselines, test_registry_release, test_release_candidate, test_release_rehearsal, test_release_workflow_structure, test_check_gates_inventory.
  • check-gates-inventory.py passes.
  • Exposure tests on Linux: the exposure and collector tests were run in ubuntu:24.04 with binutils 2.42, with no skips.
  • Renewal on real v0.32.0 rehearsal evidence (run 34923868162):
    • Starting from the v0.31.0-reviewed baselines (the parent of chore(release): prepare v0.32.0 beta-44 #1073), --write reproduced the merged v0.32.0 baselines and live pins. The only differences were the exception rationales, which the tool deliberately never edits.
    • Against current main, the dry run reports 0 files to change.
  • Exposure report on the five real v0.32.0 Entrypoint executables:
    • breg, casework, and discovery: 0 entries require review.
    • evidence and relay: 1 entry each, SQLite's dlsym forwarding wrapper (mov %rdx,%rsi; jmp dlsym@plt), reported as name_argument_not_constant.
    • The located names are _dl_find_object, getrandom, and __pthread_get_minstack, matching the manual disassembly review done for v0.32.0.
  • Not run locally: actionlint. CI runs it.

Notes

Security review notes (release provenance). This changes how reviewed advisory baselines are produced, so please review these points:

  1. What the tool never does. It does not add, remove, or extend an exception. It does not write or change a rationale, and it never moves expires_at. It refuses rather than accepting a new blocking finding, a finding that became fixable, or a finding that disappeared. Human review still owns the vulnerability decision, and the release candidate still enforces the result against its own protected-main image bytes.
  2. Independent source revision. --source-revision is required and must equal collection.json's revision. The documented procedure copies it from gh run view --json headSha, GitHub's record of the dispatched commit, not from the artifact.
  3. Bound evidence.
    • collection.json must have exactly the expected fields, review_only purpose, both acceptance flags false, and the exact version and source.
    • Its images must equal release_candidate._candidate_image_names(version).
    • Each image's Grype, Syft, and OCI config must agree with the collection digest.
    • Each image's rootfs must agree with its Syft file digests. The rootfs is extracted with --no-same-owner --no-same-permissions and rejected if it contains special files.
  4. Refused changes that need a human.
    • A changed runtime base (DiffID prefix) or an image with no application layers.
    • Any change to the OCI process configuration.
    • Assertion kinds other than whole_image_fingerprint_equals.
    • A review date earlier than the recorded one or later than --today.
    • An exception whose expires_at precedes the review date.
    • A missing baseline: a new image's first baseline is still authored by hand.
  5. All or nothing. Every image's renewed baseline must pass validate_v4_baseline and check_grype_findings against the extracted rootfs before any file is written. The live-pin rewrite requires each pin assignment to match exactly once and to cover exactly the renewed roster.
  6. Exposure report scope.
    • The report is review-only and not a gate. It proves nothing about source reachability: indirect branch targets are invisible to it, and a wrapper that forwards a runtime name is flagged, not resolved.
    • It is x86-64 ELF64 only, which matches the rehearsal roster.
    • A failed analysis fails evidence collection instead of producing a partial artifact. That only affects advisory_evidence=true rehearsal runs.
    • The Entrypoint executable is read from the rootfs tar without extraction. Symlinks are resolved inside the archive with a hop limit, and parent-directory escapes are refused.
    • readelf and objdump come from the runner's binutils: Ubuntu 24.04 runner images ship binutils 2.42. The collector checks for them before doing any work.

Deliberately unchanged.

  • The candidate-failure "Renew an image advisory fingerprint" procedure stays manual. Evidence regenerated from a failed candidate is laid out differently from the rehearsal collection, and it may carry official_candidate provenance, which this tool does not write.
  • collection.json is unchanged; it does not list the exposure reports.
  • Structured rationale fields (a schema bump) are deferred.

Possible follow-ups, not in this PR. Report dlopen call sites and their library-name arguments the same way. Optionally teach the candidate procedure to reuse the renewal once its evidence layout matches.

DCO

  • Every commit includes a Signed-off-by trailer.
  • I reviewed the submitted changes and am responsible for the contribution.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread release/scripts/test_image_exposure.py Fixed
@jeremi
jeremi enabled auto-merge (squash) September 15, 2026 10:15
Renewing reviewed advisory exceptions for a new release meant hand-editing
evidence bindings, recomputing canonical definition digests with inline
Python, and moving live test pins by hand. Each release repeated the same
error-prone steps before a human could review the actual exposure claim.

Add `registry-release renew-advisory-baselines`, which moves only the
machine bindings of already-reviewed exceptions onto review-only rehearsal
evidence. It refuses changed bases or process contracts, missing or new
findings, expired exceptions, non-fingerprint assertions, and any result
that fails the strict advisory check, and it writes nothing unless every
image renews.

The rehearsal evidence collector also writes a review-only ELF exposure
report per image, listing loader inputs and located dlsym or dlvsym uses
with provable constant names, so reviewers start from facts rather than
ad-hoc disassembly. It is not a gate.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
CodeQL flags a module imported with both import and import from.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
@jeremi
jeremi force-pushed the feat/release-advisory-renewal-tooling branch from 0423086 to a386867 Compare September 15, 2026 17:28
@jeremi
jeremi merged commit 8b273cf into main Sep 15, 2026
52 checks passed
@jeremi
jeremi deleted the feat/release-advisory-renewal-tooling branch September 15, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants