Skip to content

SDK fixture evidence: callee provenance, generic-vs-canonical contracts, and three review corrections #147

Description

@rldyourmnd

Independent verification of the #129 checkpoint (checkpoint/2026-08-13-ci-workflows-129-sdk,
head 7cbd1e85) against main at 2d25598e. Filed from the main-line side; the
branch itself was not touched.

Three of these change what the work should be, so they are listed first.


1. The Gitleaks failure is a false positive — no secret, no rotation

Reproduced with the same pinned gitleaks 8.30.1 the workflow's container uses,
--redact, over 2d25598e..7cbd1e85:

RuleID   : generic-api-key
File     : scripts/check_sdk_runtime_fixtures.py
StartLine: 238
Entropy  : 3.7254806
Match    : pub_cache_key": "REDACTED"

Line 238 is a negative self-test sample:

"cache_key": "flutter-cache-3.47.0", "pub_cache_key": "flutter-pub-3.47.0"},

A cache-key literal in a synthetic receipt. generic-api-key fired on the _key
suffix plus the value's entropy. No containment or rotation is needed, and an
allowlist is the wrong fix
— lowering the literal's entropy (e.g.
"flutter-pub-cache") keeps the scanner strict.

Related: secret-scan.yml on main now scopes to the checked-out ref by default
(#146), because this finding also failed the scan on unrelated branches.

2. The Flutter pin is correct — do not repin it

A review of this checkpoint flagged stable + 3.47.0 as unresolvable. Checked
against the live official manifest:

current_release.stable = 4cf24164269a5ebf0c16a028a00727d0e77bbb05
  -> version 3.47.0, channel stable, dart 3.13.0, release_date 2026-08-12
revision 4cf24164...  -> exactly one row: (3.47.0, stable)
sha256   26cd99d3...  -> exactly one row: (3.47.0, stable)

All five fields in tests/fixtures/sdk-runtime-spec.yml match one official
immutable stable row. Stable 3.47.0 shipped 2026-08-12; 3.44.9 is the previous
stable. Acting on that finding would break a correct pin. The useful residue
is a validator that resolves the tuple against the manifest so it is never
checked by eye — and it belongs in the scheduled tier, since it is a calendar-
driven external fact.

3. catalog/runner-routing.yml does not exist

Referenced twice in review material. git cat-file -e says absent on both
7cbd1e85 and 2d25598e. The real file is catalog/workflow-routing.yml.


4. Receipts hash the caller's tree, not the called workflow (blocking)

All three reusables do a plain actions/checkout — which in a reusable resolves
the caller's repository — then hash
${{ github.workspace }}/.github/workflows/<callee>.yml as workflow_sha256:

  • dart-flutter-ci.yml:121,147
  • kotlin-android-ci.yml:117,241
  • qt-ci.yml:140,161

No use of job.workflow_* anywhere. An external consumer either fails (no such
file) or hashes its own lookalike, which then travels as callee provenance.
Same-repository fixtures cannot detect this by construction.

Suggested fix, simpler than a second checkout: don't hash bytes at all.
Record job.workflow_repository + job.workflow_sha + job.workflow_file_path.
A commit SHA is already a cryptographic identity for the content, and these come
from the runner context, which the caller cannot forge — strictly stronger than a
digest read out of a caller-controlled workspace, and it needs no token and does
not break on a private callee. (github.* stays bound to the caller in a
reusable, which is exactly why the job.workflow_* properties exist.)

If a byte digest is still wanted, take it from a second read-only path-scoped
checkout at repository: job.workflow_repository, ref: job.workflow_sha.

Needs a true external-caller test plus two negatives: caller without the file,
and caller with a different-bytes lookalike.

5. Evidence steps break the reusables' own advertised API (blocking)

The evidence steps are unconditional and assert one fixture's shape.

Qttest_command is documented "Empty to skip", and the Test step honours
that, but the evidence step opens TEST_LOG unconditionally. With an empty
test_command the log never exists, so it dies with an unhandled
FileNotFoundError rather than a clean refusal. It also requires qmake and
hard-parses CTest's tests failed out of N / 100% tests passed. All three
"Empty to skip" inputs are now traps.

Android — the workflow says "Works for pure-JVM Kotlin and Android" with
setup_android: false by default, yet the evidence step unconditionally requires
five independent things:

  1. ./gradlew --version — a Gradle wrapper
  2. trusted_root(ANDROID_HOME) — unset wherever the image does not preinstall the SDK
  3. digest("gradle/verification-metadata.xml") — absent in most projects, so this fails on any runner
  4. required_tasks hardcoded to a module literally named app
  5. if not apks — pure-JVM produces none

Worth knowing: on main these three reusables have no outputs: and no
evidence step at all — the whole surface is new here and unreleased. So there is
no compatibility to preserve and no migration to write; this is the last moment
to shape the API before consumers pin it.

Suggested split: the reusable emits a generic execution receipt with a mode
discriminator and no fixture-specific hard failures; the canonical assertions
(APK, task graph, locks, verification metadata, CTest) live in the observer and
check_sdk_runtime_fixtures.py.

Test matrix: Android pure-JVM · Android canonical · Android with a different task
graph · Qt canonical CTest · Qt test_command: '' · Qt non-CTest runner · Flutter
with optional steps disabled.

6. The receipt validator accepts malformed digests

_is_sha (64 lowercase hex) is applied to exactly two fields — test_log_sha256
and build_log_sha256. These are checked for truthiness only:

  • pubspec_lock_sha256
  • apk_sha256 (a list — ["zzz"] passes)
  • lock_sha256 (a dict)
  • verification_metadata_sha256

The negative substitutions cover workflow_sha256, caller_sha, test_count
and some command/JVM fields, but never put a malformed value in the four above.
One shared receipt schema, reused by emitter, observer and negatives, would close
this and #5 together.

7. The root-ownership negative test can false-fail

_sdk_environment._trusted_root accepts info.st_uid not in {0, uid}, so a
root-owned root is always trusted. The "unowned" self-test
(check_sdk_runtime_fixtures.py:371) passes os.getuid() + 1, but the temp
files belong to the executing user. Run as root: files are uid 0, uid is 1,
0 not in {0, 1} is false, no error is raised, the negative case is accepted and
the test reports a false failure.

Extract a pure ownership predicate and test explicit (owner, mode) tuples instead
of building the case from the live filesystem. Do not weaken the production
rule
— trusting root-owned toolchain roots is correct.


Verified against 7cbd1e85 and 2d25598e by reading the objects directly, not
the working tree.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions