Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/actions/rust-setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ runs:
# `@master`, which floats the ACTION's own implementation and can change CI behavior out from
# under the pinned Rust toolchain input below without warning — caught in PR review). The
# `toolchain:` input still wins over whatever default this ref's snapshot implies.
- uses: dtolnay/rust-toolchain@1.96
- uses: dtolnay/rust-toolchain@2fe4ca74464c5902a4f6e302d0a619b4ea911ccc # 1.96
with:
toolchain: ${{ inputs.toolchain }}
components: ${{ inputs.components }}
targets: ${{ inputs.targets }}
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: ${{ inputs.cache-key-suffix }}
- name: Install Linux frontend system deps
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/antigravity-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
# branch throughout. Consequence worth knowing: a PR that edits the reviewer or the
# style guide is reviewed by the version already on the default branch until it merges.
- name: Check out repo (for the style guide + scripts)
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.event.repository.default_branch }}
# Not `fetch-depth: 0`: the large-diff fallback fetches exactly the two refs it
Expand Down
30 changes: 22 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ jobs:
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v7
- uses: dorny/paths-filter@v3
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
id: filter
with:
filters: |
Expand Down Expand Up @@ -88,7 +90,9 @@ jobs:
CARGO_HTTP_MULTIPLEXING: "false"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# fmt/clippy only ever check the HOST target here (no `--target` is ever passed to either
# command in this job) — installing the wasm32/thumbv7em cross targets bought nothing but
# setup time on every single PR push. Only `no_std` (which actually cross-compiles) needs
Expand Down Expand Up @@ -184,7 +188,9 @@ jobs:
CARGO_HTTP_MULTIPLEXING: "false"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: ./.github/actions/rust-setup
with:
cache-key-suffix: test-light
Expand All @@ -206,7 +212,9 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# Same rationale as `lint`: neither fmt nor clippy nor `cargo test` cross-compiles here, so
# no extra targets are installed.
- uses: ./.github/actions/rust-setup
Expand Down Expand Up @@ -265,7 +273,9 @@ jobs:
- rustysnes-cart
- rustysnes-core
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: ./.github/actions/rust-setup
with:
targets: thumbv7em-none-eabihf
Expand All @@ -285,7 +295,9 @@ jobs:
CARGO_HTTP_MULTIPLEXING: "false"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# The `rustysnes-core` `headless_frame` bench is chip-stack-only (no wgpu/winit/egui), so
# no Linux frontend system deps are needed here (unlike `lint`/`full-test`).
- uses: ./.github/actions/rust-setup
Expand All @@ -301,7 +313,9 @@ jobs:
if: needs.setup.outputs.mode != 'skip'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: ./.github/actions/rust-setup
with:
cache-key-suffix: accuracysnes
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ jobs:
CARGO_NET_RETRY: "10"
CARGO_HTTP_MULTIPLEXING: "false"
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: ./.github/actions/rust-setup
with:
# `dtolnay/rust-toolchain` ignores its OWN `toolchain:`/`components:`/`targets:` inputs
Expand Down
12 changes: 8 additions & 4 deletions .github/workflows/pgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ jobs:
promotable: ${{ steps.gate.outputs.promotable }}
speedup_pct: ${{ steps.gate.outputs.speedup_pct }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

# `components: llvm-tools-preview` is listed for documentation, but the EFFECTIVE source is
# `rust-toolchain.toml` (which now includes it) -- `dtolnay/rust-toolchain` silently ignores
Expand Down Expand Up @@ -173,7 +175,7 @@ jobs:

- name: Upload promoted PGO binary
if: steps.gate.outputs.promotable == 'true'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: rustysnes-pgo-${{ steps.triple.outputs.triple }}
path: target/${{ steps.triple.outputs.triple }}/release/rustysnes
Expand All @@ -194,7 +196,9 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: ./.github/actions/rust-setup
with:
components: llvm-tools-preview
Expand Down Expand Up @@ -282,7 +286,7 @@ jobs:

- name: Upload BOLT binary
if: steps.bolt_probe.outputs.have_bolt == 'true'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: rustysnes-bolt-${{ steps.triple.outputs.triple }}
path: target/${{ steps.triple.outputs.triple }}/release/rustysnes
Expand Down
44 changes: 40 additions & 4 deletions .github/workflows/release-auto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,34 @@ jobs:
should_release: ${{ steps.decide.outputs.should_release }}
tag: ${{ steps.decide.outputs.tag }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
# Build the release from the exact commit CI went green on. Full history so the
# annotated-tag creation and the `git ls-remote` existence check both have what they
# need (a shallow clone's tag/ref visibility isn't reliable here).
# annotated-tag creation and the tag-existence check both have what they need (a shallow
# clone's tag/ref visibility isn't reliable here).
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
# DELIBERATELY NOT `persist-credentials: false`, and the only checkout in the repository
# that keeps the token. Every other one was hardened in `v1.26.0` because checkout writes
# the workflow `GITHUB_TOKEN` into `.git/config`, where anything the job then executes
# from the tree can read it.
#
# This job is the exception because it genuinely needs it: it creates an annotated tag and
# `git push origin "$TAG"` (below), which authenticates through exactly that credential.
# The sibling project's audit concluded no job needed the token; that conclusion does NOT
# transfer here, which is why this was audited per-site rather than applied blanket.
#
# The exposure is bounded by the trigger: `workflow_run` on a completed CI run of `main`,
# never a pull request, so the tree this job executes is already-reviewed code.

- name: Decide whether a closed-out version needs releasing
id: decide
shell: bash
env:
# Required by the `gh api` tag-existence check below. `gh` errors out without it rather
# than falling back to the ambient credential, and this step previously used only plain
# `git`, so it had no token of its own. Same form as the release steps further down.
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# ANY non-blank content under [Unreleased] (not just a "- " bullet -- a stray heading
Expand Down Expand Up @@ -93,7 +110,26 @@ jobs:
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "header=${header}" >> "$GITHUB_OUTPUT"
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
# FAIL CLOSED. This was `git ls-remote --exit-code --tags origin "refs/tags/${tag}"`,
# which collapses THREE outcomes into two: tag present (0), tag absent (2), and lookup
# failed (non-zero, various). Reading any non-zero as "absent" means a transient network
# blip, an auth hiccup, or a rate limit sends an ALREADY-RELEASED version down the
# `should_release=true` path — re-tagging a published release, which
# `to-dos/ROADMAP.md`'s release ceremony treats as immutable.
#
# `git/matching-refs` is chosen over `git/ref/tags/<tag>` deliberately: it answers
# "absent" with HTTP 200 and an empty array, so a genuine miss can never be confused with
# an error and no error-body parsing is needed. It matches by PREFIX, though, so the exact
# ref is compared in `jq` — that is not theoretical, `v1.2` prefix-matches several real
# tags while exact-matching none.
#
# Every failure path aborts the job (the script runs under `set -euo pipefail`) rather
# than resolving to a release decision.
if ! matching="$(gh api "repos/${GITHUB_REPOSITORY}/git/matching-refs/tags/${tag}")"; then
echo "::error::Tag lookup for ${tag} failed. Refusing to guess whether it exists."
exit 1
fi
if printf '%s' "$matching" | jq -e --arg ref "refs/tags/${tag}" 'any(.[]; .ref == $ref)' >/dev/null; then
echo "Tag ${tag} already exists -- nothing to release."
echo "should_release=false" >> "$GITHUB_OUTPUT"
else
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ jobs:
# `push` trigger, where `github.ref_name` is already the pushed tag).
TAG: ${{ inputs.tag || github.ref_name }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
ref: ${{ env.TAG }}
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: release-${{ matrix.target }}
- name: Install Linux frontend system deps
Expand Down
22 changes: 14 additions & 8 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ jobs:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# Install the PREBUILT binary, never `cargo install` (build-from-source): the repo pins
# rustc 1.96, but a from-source cargo-audit build can need a newer toolchain than that to
# COMPILE. The prebuilt binary runs fine under any toolchain -- it only parses Cargo.lock,
# it never compiles this project.
- uses: taiki-e/install-action@v2
- uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2
with:
tool: cargo-audit
- run: cargo audit
Expand All @@ -74,10 +76,12 @@ jobs:
name: Cargo Deny Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# Same rationale as the audit job: the prebuilt binary avoids needing a newer toolchain
# than the repo's pinned 1.96 just to build the checker itself. Policy lives in `deny.toml`.
- uses: taiki-e/install-action@v2
- uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2
with:
tool: cargo-deny
- run: cargo deny check
Expand Down Expand Up @@ -112,12 +116,14 @@ jobs:
# indistinguishably from a real finding.
timeout-minutes: 180
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# cargo-fuzz needs nightly (`-Z sanitizer=address`); the project pins 1.96 stable in
# `rust-toolchain.toml`. Installing nightly alongside it, and invoking it explicitly as
# `+nightly`, keeps the pin authoritative for every other job.
- uses: dtolnay/rust-toolchain@nightly
- uses: taiki-e/install-action@v2
- uses: dtolnay/rust-toolchain@4fd1da8b0805d2d2e936788875a7d65dbd677dc2 # nightly
- uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2
with:
tool: cargo-fuzz
# `fuzz/` depends on `rustysnes-frontend` (the patch/preset/config/symbols boundaries live
Expand Down Expand Up @@ -162,7 +168,7 @@ jobs:
# cargo +nightly fuzz run <target> <artifact>
- name: Upload crashing inputs
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: fuzz-artifacts
path: |
Expand Down
14 changes: 8 additions & 6 deletions .github/workflows/web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ jobs:
name: build demo + docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# GOTCHA: keep crates/rustysnes-frontend/web/Trunk.toml's wasm-bindgen pin == Cargo.lock
# library version. `linux-frontend-deps: "true"` because the rustdoc build below compiles
# the whole workspace (incl. rustysnes-frontend) for the host, which needs the native
Expand All @@ -81,7 +83,7 @@ jobs:
# + its whole dependency tree from source on every single run — minutes of pure setup cost
# for a tool whose own source never changes here).
- name: Install trunk
uses: taiki-e/install-action@v2
uses: taiki-e/install-action@6a1bd70eaac3c8bdf093356838d7ee09fda951cf # v2
with:
tool: trunk

Expand Down Expand Up @@ -112,7 +114,7 @@ jobs:
run: cargo doc --workspace --exclude rustysnes-android --no-deps

- name: Set up Python for MkDocs
uses: actions/setup-python@v5
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"

Expand Down Expand Up @@ -148,11 +150,11 @@ jobs:

- name: Configure Pages
if: github.event_name != 'pull_request'
uses: actions/configure-pages@v5
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5

- name: Upload Pages artifact (demo + docs)
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
with:
path: _site

Expand All @@ -168,4 +170,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Security

- **CI supply-chain hardening — every workflow credential, action pin, and the release gate.**
Three items, the first two carried over from the sibling project's `v2.2.2` audit and the third
raised in review of the fuzzing PR.

**`persist-credentials: false` on 15 of 17 checkouts.** `actions/checkout` writes the workflow
`GITHUB_TOKEN` into `.git/config`, where anything the job then executes from the tree — build
scripts, proc macros, test binaries, `scripts/*.sh`, the MkDocs build — can read it. On a pull
request that tree is by definition unreviewed code, and nearly every job here compiles or runs
it. Highest exposure was `web.yml`'s `build`, which holds `pages: write` + `id-token: write`
while running `trunk`, `cargo doc`, `pip install`, and `mkdocs build`.

Audited per site rather than applied blanket, and that mattered: **`release-auto.yml`'s `prepare`
job genuinely needs the credential** — it creates an annotated tag and `git push origin "$TAG"`,
which authenticates through exactly that token. The sibling's audit concluded no job needed it;
that conclusion does not transfer, and a blanket sweep would have broken every future release.
It is now the only checkout in the repository that keeps the token, with the reason recorded
inline, and its exposure is bounded by its trigger (`workflow_run` on a completed CI run of
`main`, never a pull request).

**The release-tag existence check now fails closed.** It was
`git ls-remote --exit-code --tags origin "refs/tags/${tag}"`, which collapses three outcomes into
two — tag present, tag absent, and *lookup failed* — reading any non-zero exit as "absent". A
transient network blip, auth hiccup, or rate limit therefore sent an already-released version
down the `should_release=true` path, re-tagging a release the ceremony treats as immutable. Now a
`gh api git/matching-refs/tags/<tag>` call, chosen over `git/ref/tags/<tag>` because it answers
"absent" with HTTP 200 and an empty array, so a genuine miss can never be confused with an error.
That endpoint matches by **prefix**, so the exact ref is compared in `jq` — verified necessary,
not theoretical: `v1.2` prefix-matches **7** real tags while exact-matching none, and a naive
`length > 0` would have reported it as already released. Every failure path aborts the job.

**All 34 third-party action references pinned to immutable commit SHAs**, across all 8 workflows
plus the composite `rust-setup` action. An upstream tag move could previously change what CI
executes without any repository review. Each pin carries its original tag as a trailing comment
so the intent stays readable. Also aligned one `actions/upload-artifact@v4` (introduced with the
fuzz job) to the `@v7` the rest of the repo already used.

Verified: `actionlint` reports the same three findings before and after (a self-hosted runner
label and two pre-existing shellcheck notes) — no new lint surface; all nine files parse.

### Added

- **Fuzzing infrastructure (`fuzz/`) — fourteen `cargo-fuzz` targets, one per untrusted-input
Expand Down
Loading