Skip to content

Build release binaries with cargo auditable to embed dependency data - #937

Draft
shashial wants to merge 3 commits into
developfrom
cargo-auditable-builds
Draft

Build release binaries with cargo auditable to embed dependency data#937
shashial wants to merge 3 commits into
developfrom
cargo-auditable-builds

Conversation

@shashial

Copy link
Copy Markdown
Contributor

📝 Summary

Release builds now go through cargo auditable,
which embeds the resolved dependency list into a .dep-v0 section of each binary.

  • make build* targets, docker/Dockerfile.rbuilder, docker/Dockerfile.reproducible, and the
    Mac path of the release workflow build with cargo auditable build
  • New make audit-bin target scans built binaries against the RustSec advisory database
  • cargo-auditable is pinned to 0.7.5 in all build paths so every artifact embeds identically
    formatted data
  • Dev builds (build-dev, CI test builds) are unchanged

💡 Motivation and Context

Makes binaries — and container images built from them — scannable for Rust dependencies and known
vulnerabilities with cargo audit bin, trivy, grype, syft, etc.


✅ I have completed the following steps:

  • Run make lint
  • Run make test
  • [n/a] Added tests (if applicable)

@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @shashial's task in 3m 37s —— View job


Review: cargo auditable release builds

Reviewed the diff against origin/develop, plus Makefile, both Dockerfiles, release.yaml, and the surrounding build paths. The approach is sound; findings below are about the guarantees the PR claims versus what the code enforces. Five inline comments posted.

Blocking-ish

1. make audit-bin passes vacuouslyMakefile:163-172. The target doesn't depend on build, and an unmatched glob leaves the literal target/.../*, so -f fails, the loop never runs, and it exits 0. Running it on a fresh checkout, before make build, or with a BUILD_PROFILE/BUILD_TARGET different from the one built, reports success having scanned nothing. Suggested a count guard inline. (Fix this →)

2. The 0.7.5 pin isn't actually enforcedMakefile:74-77. command -v cargo-auditable >/dev/null || cargo install ...@0.7.5 skips the install if any version is already on PATH. The PR's stated goal ("pinned to 0.7.5 in all build paths so every artifact embeds identically formatted data") doesn't hold for the make path on a machine or runner that already has 0.6.x. Gate on version instead. Same applies to install-cargo-audit. (Fix this →)

Worth addressing

3. No verification that .dep-v0 is actually present in any artifact. This is the structural gap behind 1 and 2: nothing in CI fails if a binary ships without embedded data. The Dockerfile.rbuilder path is the most exposed (docker/Dockerfile.rbuilder:76) — it inherits RUSTC_WRAPPER=sccache from the base stage while cargo-auditable injects via RUSTC_WORKSPACE_WRAPPER, and cargo chef cook (line 66) builds deps unwrapped against a persistent sccache mount. That chain is supposed to work, but if it doesn't, the result is a released image with no audit data and a green build. A cargo audit bin / readelf -S | grep dep-v0 assertion after each build turns a silent regression into a hard failure.

4. Reproducible builds not re-verifieddocker/Dockerfile.reproducible:13. This image backs the Linux release artifacts and the repo works hard for bit-identical output (-C metadata='', --build-id=none, --remap-path-prefix, SOURCE_DATE_EPOCH). cargo-auditable links a generated object produced in a temp dir; worth an actual two-run hash comparison rather than trusting the "sorted and timestamp-free" claim in the new Makefile comment. Secondary: everything above that line is deliberately pinned to a Debian snapshot repo, and cargo install from crates.io is now the one un-snapshotted network fetch in that builder.

5. Version pinned in four independent placesMakefile:77, docker/Dockerfile.rbuilder:39, docker/Dockerfile.reproducible:13, .github/workflows/release.yaml:151. Given the goal is identical embedded data across paths, silent drift between these is the exact failure the PR is trying to prevent. At minimum a Makefile variable plus Docker ARGs.

6. README sections now conflict — the "Reproducible builds" section (lines 157–176) still instructs plain cargo build --release with a fixed sha256sum, which produces a binary without audit data and a hash matching nothing make build emits.

Minor

  • docker/Dockerfile.rbuilder-operator:16 builds via make build-reproducible, which isn't a target in the Makefile — so that path is already broken and isn't covered by this PR. Pre-existing, flagging only because it's another release-ish build path that won't get audit data.
  • cargo install cargo-auditable --locked compiles from source with no registry cache mount in both Dockerfiles and on every Mac release run; cargo-binstall or taiki-e/install-action would cut that.
  • Dangling comment at Makefile:74 reads as a claim about the installer rather than about the build.

Not run: make lint / make test — Bash is restricted in this environment, so I reviewed statically. The shell-semantics claim in finding 1 (unmatched glob → exit 0) is standard POSIX behavior and doesn't depend on execution.
· branch cargo-auditable-builds

Comment thread Makefile
Comment on lines +163 to +172
.PHONY: audit-bin
audit-bin: install-cargo-audit ## Scan built binaries for vulnerable dependencies (uses the audit data embedded by "make build")
@fail=0; \
for bin in $(BUILD_OUTPUT_DIR)/*; do \
if [ -f "$$bin" ] && [ -x "$$bin" ]; then \
echo "==> $$bin"; \
cargo audit bin "$$bin" || fail=1; \
fi; \
done; \
exit $$fail

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

audit-bin passes vacuously when there is nothing to scan.

The target doesn't depend on build, and if $(BUILD_OUTPUT_DIR) doesn't exist (or is empty) the glob stays unexpanded as the literal string target/.../*, [ -f ... ] is false, the loop body never runs, fail stays 0 and the target exits 0. A fresh checkout — or someone running make audit-bin before make build, or with a different BUILD_PROFILE/BUILD_TARGET than the one that was built — gets a green "audit passed" having scanned zero binaries. That's the worst failure mode for a security-scanning target.

Also worth handling: cargo audit bin on a binary with no .dep-v0 section falls back to guessing versions from panic strings, so stale non-auditable artifacts left in target/ produce low-confidence results silently mixed in with the real ones.

Suggest counting what was scanned and failing if it's zero:

Suggested change
.PHONY: audit-bin
audit-bin: install-cargo-audit ## Scan built binaries for vulnerable dependencies (uses the audit data embedded by "make build")
@fail=0; \
for bin in $(BUILD_OUTPUT_DIR)/*; do \
if [ -f "$$bin" ] && [ -x "$$bin" ]; then \
echo "==> $$bin"; \
cargo audit bin "$$bin" || fail=1; \
fi; \
done; \
exit $$fail
.PHONY: audit-bin
audit-bin: install-cargo-audit ## Scan built binaries for vulnerable dependencies (uses the audit data embedded by "make build")
@fail=0; count=0; \
for bin in $(BUILD_OUTPUT_DIR)/*; do \
if [ -f "$$bin" ] && [ -x "$$bin" ]; then \
echo "==> $$bin"; \
count=$$((count + 1)); \
cargo audit bin "$$bin" || fail=1; \
fi; \
done; \
if [ "$$count" -eq 0 ]; then \
echo "error: no binaries found in $(BUILD_OUTPUT_DIR); run 'make build' first" >&2; \
exit 1; \
fi; \
exit $$fail

Comment thread Makefile Outdated
Comment on lines +74 to +77
# The cargo-auditable embedded data is sorted and timestamp-free
.PHONY: install-cargo-auditable
install-cargo-auditable:
@command -v cargo-auditable >/dev/null 2>&1 || cargo install cargo-auditable@0.7.5 --locked

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The command -v guard defeats the version pin that the PR is built around.

If any cargo-auditable is already on PATH — an older 0.6.x on a dev machine, a preinstalled one on a CI runner, whatever a future base image ships — the install is skipped and the build silently uses that version instead of 0.7.5. So the claim that "cargo-auditable is pinned to 0.7.5 in all build paths so every artifact embeds identically formatted data" doesn't actually hold for the make path.

Gate on the version rather than on presence:

Suggested change
# The cargo-auditable embedded data is sorted and timestamp-free
.PHONY: install-cargo-auditable
install-cargo-auditable:
@command -v cargo-auditable >/dev/null 2>&1 || cargo install cargo-auditable@0.7.5 --locked
CARGO_AUDITABLE_VERSION := 0.7.5
.PHONY: install-cargo-auditable
install-cargo-auditable:
@cargo install --list | grep -q '^cargo-auditable v$(CARGO_AUDITABLE_VERSION)' \
|| cargo install cargo-auditable@$(CARGO_AUDITABLE_VERSION) --locked

(The dangling # The cargo-auditable embedded data is sorted and timestamp-free comment sits above the install target where it reads as a claim about the installer; it belongs next to the build target or in the reproducibility section.)

Same pattern applies to install-cargo-audit below. I realize install-cargo-deb already uses command -v, so this is pre-existing repo style — but the pin matters more here, since the whole point is byte-identical embedded metadata across build paths.

--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
cargo build --release --features="$FEATURES" --package=${RBUILDER_BIN}
cargo auditable build --release --features="$FEATURES" --package=${RBUILDER_BIN}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth verifying once that the .dep-v0 section actually survives this build path. cargo-auditable injects via RUSTC_WORKSPACE_WRAPPER, and this stage inherits RUSTC_WRAPPER=sccache from base (line 43), so cargo ends up chaining cargo-auditable → sccache → rustc. That chaining is supposed to work, but combined with the cargo chef cook layer (line 66) building deps without the wrapper and the persistent sccache mount, a cache hit silently returning a non-instrumented artifact would produce a released image with no audit data and no error.

A cargo audit bin /app/target/release/${RBUILDER_BIN} (or a readelf -S | grep dep-v0) right after the build would turn that into a hard failure instead of a silent one.

libclang-dev \
protobuf-compiler \
cmake
RUN cargo install cargo-auditable@0.7.5 --locked

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the image that backs the reproducible Linux release artifacts, and the PR changes what it emits without re-verifying determinism. Two things to confirm before merging:

  1. cargo-auditable links an extra generated object into the binary. It's documented as sorted/deterministic, but this repo goes to unusual lengths for bit-identical output (-C metadata='', --build-id=none, --remap-path-prefix, SOURCE_DATE_EPOCH) — the object file is produced in a temp dir, so it's worth an actual two-run hash comparison rather than taking the docs' word for it.
  2. Note that everything above this line is pinned to a Debian snapshot repo specifically so the toolchain doesn't drift. cargo install ... --locked from crates.io is now the one un-snapshotted network fetch in the reproducible builder. It's pinned by version+lockfile so it should resolve identically, but it does mean the reproducible image's inputs are no longer sourced from a single snapshot.

Also a minor cost: this compiles cargo-auditable and its dependency tree from source on every cold build of this image, with no registry cache mount.

Comment thread README.md
Comment on lines +178 to +189
### Auditable builds

Release builds (`make build`, the Docker images, and the released binaries) go through
[`cargo auditable`](https://github.com/rust-secure-code/cargo-auditable), which embeds the full
dependency list into each binary. To scan the built binaries against the RustSec advisory database:

```bash
make audit-bin
```

The embedded data can also be picked up by scanners such as `trivy`, `grype`, `syft`. Meaning that container
images built from these binaries can be scanned for Rust dependencies as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section says release builds go through cargo auditable, but the "Reproducible builds" section immediately above (lines 157–176) tells users to run plain cargo build --release and shows a specific expected sha256sum. Those two now describe divergent binaries: following the reproducible-builds instructions produces an artifact without the .dep-v0 section, and one whose hash won't match anything make build produces. Worth reconciling the two sections so it's clear which command corresponds to the released artifact.

Nit: "such as trivy, grype, syft. Meaning that container images…" — sentence fragment; syft, meaning that container images…`.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the release build pipeline to run through cargo auditable so produced binaries embed a resolved dependency list (enabling downstream vulnerability/dep scanning), and adds a Makefile target to scan built binaries with cargo audit bin.

Changes:

  • Switch release-oriented make build* targets to cargo auditable build and add helper install targets for cargo-auditable/cargo-audit.
  • Update Docker build paths and the macOS release workflow path to build via cargo auditable.
  • Document auditable builds and add a make audit-bin target for RustSec scanning of built binaries.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
README.md Adds documentation for auditable release builds and make audit-bin.
Makefile Routes release builds through cargo auditable and introduces audit-bin scanning.
docker/Dockerfile.reproducible Installs cargo-auditable so reproducible Docker builds embed audit data.
docker/Dockerfile.rbuilder Installs cargo-auditable and builds packages via cargo auditable.
.github/workflows/release.yaml Installs cargo-auditable on macOS and builds macOS release binaries via cargo auditable.
Suppressed comments (1)

Makefile:161

  • install-cargo-audit pins 0.22.2 in the install command, but the command -v guard means any preinstalled cargo-audit version will be used (which may not behave consistently across environments). Consider checking the installed version and reinstalling when it differs.
	@command -v cargo-audit >/dev/null 2>&1 || cargo install cargo-audit@0.22.2 --locked

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Makefile Outdated
# The cargo-auditable embedded data is sorted and timestamp-free
.PHONY: install-cargo-auditable
install-cargo-auditable:
@command -v cargo-auditable >/dev/null 2>&1 || cargo install cargo-auditable@0.7.5 --locked
Comment thread Makefile Outdated
Comment on lines +165 to +172
@fail=0; \
for bin in $(BUILD_OUTPUT_DIR)/*; do \
if [ -f "$$bin" ] && [ -x "$$bin" ]; then \
echo "==> $$bin"; \
cargo audit bin "$$bin" || fail=1; \
fi; \
done; \
exit $$fail
Comment thread README.md Outdated
Comment on lines +188 to +189
The embedded data can also be picked up by scanners such as `trivy`, `grype`, `syft`. Meaning that container
images built from these binaries can be scanned for Rust dependencies as well.
Comment thread .github/workflows/release.yaml Outdated
@shashial
shashial marked this pull request as draft August 21, 2026 12:36
@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @shashial's task in 4m 23s —— View job


Re-review: cargo auditable release builds

  • Read full diff vs origin/develop
  • Verify prior findings
  • Review the clippy-lint commit
  • Post review (4 inline comments)

Prior round's findings are mostly addressed: audit-bin now fails on zero binaries (Makefile:169-183), both installers gate on version via cargo install --list instead of command -v, .dep-v0 is asserted after every Docker and Mac build, --locked was added to the Mac build, and the README reproducible/auditable sections were reconciled. Remaining items below.

Worth addressing

1. Linux release artifacts are only partially verifieddocker/Dockerfile.reproducible:17-20. The loop checks rbuilder-operator, rbuilder-rebalancer, bid-scraper, but this image produces all six binaries that release.yaml renames and uploads (release.yaml:181-231). rbuilder, reth-rbuilder, and tbv-bidding-service can ship without embedded data and the build stays green — and rbuilder is the artifact most consumers will scan. Inline suggestion posted.

2. Reproducibility still isn't verified for this change. reproducible-build-test (.github/workflows/reprotest.yml) runs only on schedule (cron, every 2 days) and workflow_dispatch — not on PRs. So nothing in this PR's CI confirms that linking cargo-auditable's generated object still yields bit-identical output under -C metadata='' / --build-id=none / --remap-path-prefix. It's cheap to settle: dispatch that workflow on this branch before merging. Note it also only compares rbuilder-operator and rbuilder-rebalancer, so it won't cover the other four either.

Related: cargo install cargo-auditable@0.7.5 --locked (Dockerfile.reproducible:13) is now the one crates.io fetch in a builder whose apt sources are deliberately pinned to a Debian snapshot. Pinned by version + lockfile, so it should resolve identically, but the reproducible image's inputs no longer come from a single snapshot. Worth a conscious call rather than an accident.

3. Version pinned in four placesMakefile:75, docker/Dockerfile.rbuilder:39, docker/Dockerfile.reproducible:13, release.yaml:151. The Makefile variable helps, but the three hardcoded copies are where drift will actually happen, and drift is the exact failure this PR is trying to prevent. ARG CARGO_AUDITABLE_VERSION=0.7.5 in the Dockerfiles at minimum.

4. Mac verify step can't be exercised before a releaserelease.yaml:172-177. Only runs on tag push / manual dispatch, and greps otool -l output for a section name. Should work (__DATA,.dep-v0), but a failure here means a broken release rather than a missed check. One workflow_dispatch run with build-mac: true / draft-release: false would confirm it.

Rust changes (clippy commit)

5. Over-broad allowcrates/rbuilder-operator/src/bidding_service_wrapper/mod.rs:1. The inner #![allow(clippy::result_large_err)] silences the lint for client, server, and any future submodule, not just the generated bidding_service. The bloxroute_grpc.rs:16 change in the same commit scopes it correctly to the generated module — do the same here. Inline suggestion posted.

as_chunks::<N>().0 (crates/eth-sparse-mpt/src/utils.rs:129) is behaviour-preserving — same min(len) zip, same N-multiple offset, and the only instantiation is N = 8, so no zero-N panic path. Fine. The #[allow] on bid-scraper's connect (best_bid_ws_connector.rs:88) papers over a large tungstenite::Error; boxing would be the real fix, but it's a hand-written signature and not worth blocking on.

Minor

  • audit-bin doesn't distinguish binaries with .dep-v0 from ones without: cargo audit bin falls back to guessing versions from panic strings and reports low-confidence results without failing, so a stale non-auditable artifact left in target/ mixes silently into the output. Given the target now guarantees it scanned something, guaranteeing it scanned something auditable is the natural follow-up.
  • docker/Dockerfile.rbuilder-operator:16 still builds via make build-reproducible, which is not a target in the Makefile. Pre-existing and out of scope, but it's another release-ish path that won't get audit data.
  • Makefile:75 — the comment # The cargo-auditable embedded data is sorted and timestamp-free now sits above CARGO_AUDITABLE_VERSION, where it reads as a claim about the version variable. It belongs next to build or in the reproducibility docs.
  • grep -q '^cargo-auditable v0.7.5:' — the .s are regex wildcards. Harmless in practice; grep -qF or escaping is tidier.

Not run: make lint / make test — Bash is restricted here to read-only inspection, so this is a static review. The Makefile shell-semantics and Docker/workflow claims above are from reading the files, not from executing them.
· branch cargo-auditable-builds

Comment on lines +17 to +20
RUN set -e; for bin in rbuilder-operator rbuilder-rebalancer bid-scraper; do \
readelf -S "/build/target/x86_64-unknown-linux-gnu/reproducible/${bin}" | grep -q '\.dep-v0' \
|| { echo "ERROR: .dep-v0 audit section missing from ${bin}"; exit 1; }; \
done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verification covers 3 binaries, but this image produces the Linux release artifacts for 6. .github/workflows/release.yaml renames and uploads rbuilder, rbuilder-operator, tbv-bidding-service, reth-rbuilder, bid-scraper, rbuilder-rebalancer from target/reproducible/ — so rbuilder, reth-rbuilder, and tbv-bidding-service can ship without .dep-v0 and the build stays green. Those are the artifacts most people will actually scan.

The [ -f ] guard keeps it working if a name isn't produced by make build --workspace (though it would then also silently skip — if the list is meant to be exhaustive, drop the guard so a missing binary fails).

Suggested change
RUN set -e; for bin in rbuilder-operator rbuilder-rebalancer bid-scraper; do \
readelf -S "/build/target/x86_64-unknown-linux-gnu/reproducible/${bin}" | grep -q '\.dep-v0' \
|| { echo "ERROR: .dep-v0 audit section missing from ${bin}"; exit 1; }; \
done
RUN set -e; for bin in rbuilder rbuilder-operator tbv-bidding-service reth-rbuilder bid-scraper rbuilder-rebalancer; do \
f="/build/target/x86_64-unknown-linux-gnu/reproducible/${bin}"; \
[ -f "$f" ] || continue; \
readelf -S "$f" | grep -q '\.dep-v0' \
|| { echo "ERROR: .dep-v0 audit section missing from ${bin}"; exit 1; }; \
done

@@ -1,3 +1,5 @@
#![allow(clippy::result_large_err)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inner attribute applies to the whole module tree — client, server, and anything added later — not just the tonic-generated bidding_service. The bloxroute_grpc.rs change in this same commit scopes the allow to the generated module only; same thing works here:

Suggested change
#![allow(clippy::result_large_err)]
#[rustfmt::skip]
#[allow(clippy::result_large_err)]
pub mod bidding_service;

(and drop the #![allow(...)] line). That keeps the lint active for hand-written code in this module.

rm -rf /tmp/sccache.tar.gz /tmp/sccache-v0.8.2-${ARCH_TAG}

RUN cargo install cargo-chef --version ^0.1
RUN cargo install cargo-auditable@0.7.5 --locked

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Makefile now has CARGO_AUDITABLE_VERSION := 0.7.5, but 0.7.5 is still hardcoded here, in docker/Dockerfile.reproducible:13, and in .github/workflows/release.yaml:151. Since the stated goal is identical embedded metadata across every build path, these three are exactly where silent drift happens. An ARG CARGO_AUDITABLE_VERSION=0.7.5 in each Dockerfile (defaulted, overridable from make docker-image-*) at least makes the value greppable and settable from one place.

Also, this compiles cargo-auditable from source on every cold build of the base stage in both Dockerfiles and on every Mac release run, with no registry cache mount here. cargo-binstall or taiki-e/install-action (for the workflow) would cut a couple of minutes.

Comment on lines +172 to +177
- name: Verify audit data embedded (Mac)
if: steps.platform-check.outputs.skip != 'true' && matrix.platform == 'mac'
run: |
for bin in rbuilder rbuilder-operator tbv-bidding-service reth-rbuilder bid-scraper rbuilder-rebalancer; do
otool -l "target/${{ matrix.profile }}/${bin}" | grep -q 'dep-v0' \
|| { echo "ERROR: .dep-v0 audit section missing from ${bin}"; exit 1; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only new check that can break a release, and it only ever executes during one (tag push / manual dispatch with build-mac), so it won't be exercised by this PR. It's also grepping a tool-specific rendering of a Mach-O section name — cargo-auditable stores the data as __DATA,.dep-v0 on Mach-O, so otool -l | grep dep-v0 should match, but if that ever changes the failure is a broken release rather than a missing check.

Worth either confirming once with a manual workflow_dispatch run (build-mac: true, draft-release: false) before merging, or using a name-agnostic check — cargo audit bin --quiet <bin> fails/warns explicitly when there's no embedded data and doubles as the vulnerability scan.

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