Skip to content

feat(qt): install the toolchain from a hash-locked closure - #183

Merged
rldyourmnd merged 1 commit into
mainfrom
fixtures/qt-tool-closure
Aug 15, 2026
Merged

feat(qt): install the toolchain from a hash-locked closure#183
rldyourmnd merged 1 commit into
mainfrom
fixtures/qt-tool-closure

Conversation

@rldyourmnd

Copy link
Copy Markdown
Contributor

What was open

qt-ci.yml ran:

uvx --from 'aqtinstall@3.3.0' --with 'py7zr==1.0.0' aqt install-qt …

Two names pinned. Everything they pull, unbounded — aqtinstall 3.3.0 declares:

bs4, defusedxml, humanize, patch-ng, semantic-version, texttable   # no bound at all
py7zr>=0.22.0, requests>=2.31.0                                     # lower bound only

Two runs of the same workflow SHA could install different code without this
repository changing.

This mattered more here than it would elsewhere: driving aqtinstall directly was
itself a supply-chain decision, taken to escape jurplel/install-qt-action whose
nested action graph could not be pinned (#150). Escaping one unpinned graph into
another is not an improvement.

The closure

requirements-qt.txt28 packages, all hashed, installed with --require-hashes
into an isolated environment.

How a reusable workflow gets a lock it does not have

qt-ci.yml runs in the caller's checkout, so the lock is not there. It is fetched
from the exact commit of the workflow file:

  • job.workflow_sha is the runner's own answer to "what is executing" — a caller
    cannot forge it, and the receipt already records it;
  • the fetched lock's sha256 is checked against LOCK_SHA256, published in the
    workflow file itself
    , before anything installs.

So pinning the workflow pins the closure. And --require-hashes means that even a
substituted lock cannot introduce a package — it can only fail.

Three statements that must agree

check_qt_toolchain_lock.py holds them together:

  1. the digest qt-ci.yml publishes == the lock in the tree
  2. the versions the lock pins == the versions catalog/tools.yml records
  3. every requirement in the lock carries --hash entries at all — otherwise
    --require-hashes fails on the runner rather than here

catalog/tools.yml now registers aqtinstall and py7zr, which it did not before,
each with the lock digest. The receipt records toolchain_lock_sha256 beside
aqt_version, because aqt 3.3.0 said nothing about the 26 packages underneath it.

Evidence, before the ledger moved

Run 31855694582:

success  Provision the locked Qt toolchain
success  Resolve the Qt release
success  Install Qt
success  Configure / Build / Test
success  Emit runtime evidence
success  observe / qt-ci

observe / qt-ci is the observer validating the receipt — including the new field.
proven_digest and last_run were updated only after that succeeded.

Verification

  • validate_all — all tiers OK with GH_TOKEN
  • actionlint — clean · zizmor@1.26.1 --persona pedantic --min-severity low — no findings

qt-ci.yml ran `uvx --from 'aqtinstall@3.3.0' --with 'py7zr==1.0.0'`, which
pinned two names and left everything they pull unbounded: aqtinstall declares
bs4, defusedxml, humanize, patch-ng, semantic-version and texttable with no
upper bound, plus requests>=2.31.0. Two runs of the same workflow SHA could
install different code without this repository changing.

That mattered here more than most places. Driving aqtinstall directly was
itself a supply-chain decision, taken to escape an action whose nested graph
could not be pinned; escaping one unpinned graph into another is not an
improvement.

requirements-qt.txt is the closure -- 28 packages, all hashed, installed with
--require-hashes into an isolated environment.

This is a reusable workflow, so its checkout is the caller's tree and the lock
is not in it. The lock is fetched from the exact commit of the workflow file
(job.workflow_sha is the runner's own answer to what is executing, which the
caller cannot forge) and its digest is checked against a value published in
that file before anything installs. Pinning the workflow pins the closure, and
--require-hashes means a substituted lock still cannot introduce a package.

catalog/tools.yml registers both tools and the lock digest.
check_qt_toolchain_lock.py holds the digest, the versions and the presence of
hashes together. The receipt records toolchain_lock_sha256 beside aqt_version,
because `aqt 3.3.0` said nothing about the 26 packages underneath it.

Re-proven by fixture run 31855694582 before the ledger was updated.
@github-actions

Copy link
Copy Markdown

Dependency Review

The following issues were found:
  • ❌ 1 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 1 package(s) with unknown licenses.
  • ⚠️ 1 packages with OpenSSF Scorecard issues.
See the Details below.

Vulnerabilities

requirements-qt.txt

NameVersionVulnerabilitySeverity
py7zr1.0.0py7zr: Arbitrary File Write Vulnerabilityhigh
py7zr: Decompression bomb (zip bomb) denial of service via unchecked extraction sizemoderate
py7zr: O(n^2) algorithmic complexity DoS in PackInfo._read()moderate
Only included vulnerabilities with severity moderate or higher.

License Issues

requirements-qt.txt

PackageVersionLicenseIssue Type
backports-zstd1.6.0NullUnknown License

OpenSSF Scorecard

Scorecard details
PackageVersionScoreDetails
pip/py7zr 1.0.0 UnknownUnknown
pip/aqtinstall 3.3.0 UnknownUnknown
pip/backports-zstd 1.6.0 UnknownUnknown
pip/beautifulsoup4 4.15.0 UnknownUnknown
pip/brotli 1.2.0 🟢 7.4
Details
CheckScoreReason
Packaging⚠️ -1packaging workflow not detected
Security-Policy🟢 10security policy file detected
Code-Review🟢 4Found 5/11 approved changesets -- score normalized to 4
Token-Permissions⚠️ -1No tokens found
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow⚠️ -1no workflows found
Pinned-Dependencies⚠️ -1no dependencies found
Binary-Artifacts🟢 10no binaries found in the repo
CII-Best-Practices⚠️ 2badge detected: InProgress
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Dependency-Update-Tool🟢 10update tool detected
Vulnerabilities🟢 100 existing vulnerabilities detected
License🟢 10license file detected
Fuzzing🟢 10project is fuzzed
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
SAST⚠️ 2SAST tool is not run on all commits -- score normalized to 2
CI-Tests🟢 1016 out of 16 merged PRs checked by a CI test -- score normalized to 10
Contributors🟢 10project has 17 contributing companies or organizations
pip/brotlicffi 1.2.0.1 🟢 4.9
Details
CheckScoreReason
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 6Found 16/24 approved changesets -- score normalized to 6
Binary-Artifacts🟢 10no binaries found in the repo
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Security-Policy🟢 10security policy file detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
License🟢 10license file detected
Fuzzing⚠️ 0project is not fuzzed
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
Packaging🟢 10packaging workflow detected
pip/bs4 0.0.2 UnknownUnknown
pip/certifi 2026.7.22 🟢 6.4
Details
CheckScoreReason
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 3Found 1/3 approved changesets -- score normalized to 3
Binary-Artifacts🟢 10no binaries found in the repo
Maintained🟢 1013 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Security-Policy🟢 10security policy file detected
Pinned-Dependencies🟢 5dependency not pinned by hash detected -- score normalized to 5
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
License🟢 9license file detected
Fuzzing⚠️ 0project is not fuzzed
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/cffi 2.1.1 UnknownUnknown
pip/charset-normalizer 3.5.0 UnknownUnknown
pip/defusedxml 0.7.1 🟢 5.1
Details
CheckScoreReason
Code-Review⚠️ 0Found 1/24 approved changesets -- score normalized to 0
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Security-Policy🟢 10security policy file detected
Binary-Artifacts🟢 10no binaries found in the repo
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
License🟢 9license file detected
Fuzzing🟢 10project is fuzzed
Packaging⚠️ -1packaging workflow not detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/humanize 4.16.0 UnknownUnknown
pip/idna 3.18 UnknownUnknown
pip/inflate64 1.0.4 UnknownUnknown
pip/multivolumefile 0.2.3 🟢 3.4
Details
CheckScoreReason
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Binary-Artifacts🟢 10no binaries found in the repo
Code-Review⚠️ 0Found 2/28 approved changesets -- score normalized to 0
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing🟢 10project is fuzzed
License🟢 10license file detected
Packaging⚠️ -1packaging workflow not detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/patch-ng 1.19.1 UnknownUnknown
pip/psutil 7.2.2 UnknownUnknown
pip/pybcj 1.0.8 UnknownUnknown
pip/pycparser 3.0 UnknownUnknown
pip/pycryptodomex 3.23.0 UnknownUnknown
pip/pyppmd 1.2.0 UnknownUnknown
pip/pyzstd 0.19.1 UnknownUnknown
pip/requests 2.34.2 UnknownUnknown
pip/semantic-version 2.10.0 🟢 3.1
Details
CheckScoreReason
Code-Review⚠️ 0Found 2/25 approved changesets -- score normalized to 0
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Binary-Artifacts🟢 10no binaries found in the repo
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/soupsieve 2.9.2 UnknownUnknown
pip/texttable 1.7.0 ⚠️ 2.1
Details
CheckScoreReason
Token-Permissions⚠️ -1No tokens found
Binary-Artifacts🟢 10no binaries found in the repo
Code-Review⚠️ 1Found 3/24 approved changesets -- score normalized to 1
Pinned-Dependencies⚠️ -1no dependencies found
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow⚠️ -1no workflows found
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/typing-extensions 4.16.0 UnknownUnknown
pip/urllib3 2.7.0 UnknownUnknown

Scanned Files

  • requirements-qt.txt

@github-actions github-actions Bot added ci Continuous integration and removed ci Continuous integration labels Aug 15, 2026
@rldyourmnd
rldyourmnd merged commit 805be3e into main Aug 15, 2026
112 of 115 checks passed
@rldyourmnd
rldyourmnd deleted the fixtures/qt-tool-closure branch August 15, 2026 01:21
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.

1 participant