mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description
An independent OSV sweep of every locked package in the repo (1,463
across PyPI, crates.io and npm) surfaced three RUSTSEC advisories that
**no gate was reporting**:
| advisory | package | status |
|---|---|---|
| RUSTSEC-2026-0258 (GHSA-q83h-524g-xf6h) | h2 0.4.15 | fixed here →
0.4.16 |
| RUSTSEC-2026-0204 | crossbeam-epoch 0.9.18 | fixed here → 0.9.20 |
| RUSTSEC-2024-0436 | paste 1.0.15 | unmaintained, **no patched version
exists** |
**h2 is the one that matters.** It accepted and queued empty DATA frames
without limit; a peer that never drains a stream drives unbounded memory
growth, or a panic when the length overflows. It is not a corner of the
tree — it reaches the published wheel (`hf-hub -> headroom-core ->
headroom-py`) and the entire axum/reqwest/aws-config surface of
`headroom-proxy`.
**Why none of this was visible** is the more important half of this PR.
The `audit` job was already correct in one respect I initially misread —
the `rust-changes` job reports `rust=true` for `schedule`, so it *does*
run nightly rather than only on Rust changes. The actual defect is that
`cargo audit` was `continue-on-error: true`. It has been faithfully
reporting findings into a green run that nobody looks at.
Two of the three are also invisible to Dependabot entirely:
`crossbeam-epoch` and `paste` are RUSTSEC-only with no GHSA, so the
advisory database GitHub scans does not contain them. This job is their
only possible coverage.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `Cargo.lock`: `h2` 0.4.15 → 0.4.16, `crossbeam-epoch` 0.9.18 → 0.9.20.
Version + checksum only, 4 lines each way.
- `.github/workflows/rust.yml`: dropped `continue-on-error: true` from
the `cargo audit` step. `cargo deny check licenses` is deliberately left
soft-fail — `deny.toml` documents itself as intentionally permissive for
now, and tightening license policy is a separate decision.
- `.cargo/audit.toml` (new): lists `RUSTSEC-2024-0436` as accepted, with
the reason. Path matters — cargo-audit reads `.cargo/audit.toml`; a
root-level `audit.toml` is silently ignored.
`paste` is unmaintained rather than vulnerable, and there is nothing to
move to. It arrives via `tokenizers -> paste` and `rav1e -> paste`, both
under `fastembed`, so it is not actionable at our layer. Worth
revisiting when `tokenizers` adopts `pastey`.
## Testing
- [x] Manual testing performed
### Test Output
```text
Checksums verified against the real crates.io tarballs, not just the API field:
OK h2 0.4.16 (173331 bytes)
lock : a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27
real : a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27
OK crossbeam-epoch 0.9.20 (47545 bytes)
lock : 2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f
real : 2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f
Dependency-set equality (crates.io API, kind=normal):
h2 0.4.15 -> 0.4.16 : 11 deps before, 11 after, identical
crossbeam-epoch 0.9.18 -> .20: 2 deps before, 2 after, identical
```
## Real Behavior Proof
- Environment: macOS (darwin 25.4.0), worktree off `main` @ `b77d6129`.
**`cargo` is not installed on this machine** — see below.
- Exact command / steps: (1) parsed `uv.lock`, `Cargo.lock` and all four
`package-lock.json` files into 1,463 unique (ecosystem, name, version)
tuples and queried `api.osv.dev/v1/querybatch`, then pulled full records
for every hit; (2) walked `Cargo.lock` to find which workspace crates
actually reach `h2`, `crossbeam-epoch` and `paste`; (3) fetched both
crates' dependency lists from the crates.io API at the old and new
versions and compared them; (4) downloaded both `.crate` tarballs and
computed SHA-256 locally.
- Observed result: both bumps are patch-level with **byte-identical
dependency sets**, so the edited `Cargo.lock` is exactly what `cargo
update -p h2 -p crossbeam-epoch` would produce, and both checksums match
the real tarballs. `h2` 0.4.16 was published 2026-08-17, which is also
why Dependabot has not raised it yet.
- Not tested: I could not run `cargo audit`, `cargo build` or the test
suite locally — cargo is not installed here. **The lock edit is
hand-written, so CI is the real verification**, and it is well covered:
the `rust` workflow's `test`, `build`, `parity` and now-blocking `audit`
jobs all consume this lock and will fail on a bad checksum or an
unresolvable graph. I would not merge this on green-by-assertion; it
needs the `rust` jobs actually green.
## Runtime Rollout Safety
- Rollout-managed feature(s): None.
- Minimum rollout channel: n/a
- Stable/default behavior changed: No runtime behavior changes. CI
becomes stricter: `cargo audit` can now fail a build.
- Kill switch / disable path: re-adding `continue-on-error: true`
restores the previous (useless) behavior.
- Unsafe override required: No.
- Qualification impact: A newly-disclosed RUSTSEC advisory will now turn
the nightly Rust run red instead of being silently absorbed. That is the
intent, but it does mean advisories become someone's problem on
disclosure day — the escape hatch is a documented entry in
`.cargo/audit.toml`.
- Rollback path: Revert the commit.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Additional Notes
Deliberately **not** bundled here, each worth its own change:
- **`npm` has no audit gate at all** — no `npm audit` anywhere in
`.github/`. Current npm exposure is only `nanoid` 3.3.17
(GHSA-2v37-7h3g-55p8) in three lockfiles, all `dev: true`, which GitHub
auto-dismissed correctly. Low stakes today, but the gate is absent
rather than passing.
- **`pip-audit` only audits `--extra all`**, which excludes the
integration extras (`crewai`, `agno`, `autogen`, `langchain`, `strands`,
`bedrock`, `memory-stack`, `sandbox`). Every Python advisory currently
open against this repo lives in exactly that blind spot — GitPython via
`agno` (#3120), chromadb and json-repair via `crewai`. Dependabot
catches them because it scans the whole lock; the CI gate structurally
cannot.
- **Dependabot's `docker` ecosystem is configured for `directory: /`
only**, so the five non-root Dockerfiles get no base-image updates.
Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
263 lines
10 KiB
YAML
263 lines
10 KiB
YAML
name: rust
|
|
|
|
# Path gating lives in the `rust-changes` job below, NOT in a workflow-level
|
|
# `paths:` filter. The distinction matters for branch protection: a workflow
|
|
# skipped by `paths:` never creates its check runs at all, so a required status
|
|
# check from it sits pending forever on any PR that misses those paths, and the
|
|
# PR can never merge. A job skipped by `if:` still creates a check run, reports
|
|
# `skipped`, and GitHub counts skipped as success for a required check.
|
|
#
|
|
# Same coverage as the old filter — the path list moved verbatim into
|
|
# `rust-changes` — but `parity` is now safe to mark required on `main`.
|
|
on:
|
|
push:
|
|
branches: [ main, rust-rewrite ]
|
|
pull_request:
|
|
schedule:
|
|
# Nightly parity run at 07:17 UTC (weekdays only). Redundant with the
|
|
# per-PR gate below, but catches drift from toolchain/dependency updates
|
|
# that land without touching any filtered path.
|
|
- cron: '17 7 * * 1-5'
|
|
|
|
concurrency:
|
|
group: rust-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
# Default permissions: read-only. Individual jobs override only what they need.
|
|
# Mitigates CodeQL/CWE-275 (missing-workflow-permissions): the GITHUB_TOKEN
|
|
# defaults to whatever the repo policy is, which can be read-write. Pinning
|
|
# this here means even if the repo default changes, this workflow stays safe.
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
# Carries the path list the workflow-level `paths:` filter used to hold. Named
|
|
# `rust-changes` rather than `changes` so it does not collide with ci.yml's
|
|
# `changes` check.
|
|
rust-changes:
|
|
name: rust-changes
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
outputs:
|
|
rust: ${{ steps.decide.outputs.rust }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: dorny/paths-filter@v4
|
|
id: filter
|
|
if: github.event_name == 'pull_request' || github.event_name == 'push'
|
|
with:
|
|
filters: |
|
|
rust:
|
|
- 'crates/**'
|
|
- 'Cargo.toml'
|
|
- 'Cargo.lock'
|
|
- 'rust-toolchain.toml'
|
|
- 'tests/parity/**'
|
|
- 'Makefile'
|
|
- '.github/workflows/rust.yml'
|
|
- id: decide
|
|
# `schedule` and `workflow_dispatch` have no diff to filter against, so
|
|
# they run the full suite — that is the point of the nightly job.
|
|
run: |
|
|
case "${{ github.event_name }}" in
|
|
pull_request|push) echo "rust=${{ steps.filter.outputs.rust }}" >> "$GITHUB_OUTPUT" ;;
|
|
*) echo "rust=true" >> "$GITHUB_OUTPUT" ;;
|
|
esac
|
|
|
|
test:
|
|
name: test (ubuntu)
|
|
needs: rust-changes
|
|
if: needs.rust-changes.outputs.rust == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- name: Install stable toolchain
|
|
# Pin action code to @stable (latest fixes), toolchain version
|
|
# via input. The @1.95.0 ref shipped action code that errors on
|
|
# ubuntu-latest with `detected conflict: 'bin/cargo-clippy'`
|
|
# because the pre-installed runner Rust collides with the
|
|
# clippy-preview component install.
|
|
uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
toolchain: 1.95.0
|
|
components: rustfmt, clippy
|
|
- name: Cache cargo registry + build
|
|
uses: Swatinem/rust-cache@v2
|
|
- uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.11'
|
|
- name: Provide ONNX Runtime dylib
|
|
# headroom-core is built with `ort-load-dynamic` (see its
|
|
# Cargo.toml): the ONNX Runtime shared library is dlopen'd at
|
|
# runtime instead of statically linked, so the magika/detection
|
|
# tests need a real libonnxruntime.so and ORT_DYLIB_PATH pointing
|
|
# at it — same contract `headroom/_ort.py` fulfills for Python
|
|
# users via the pip `onnxruntime` package.
|
|
run: |
|
|
# >= 1.24, not 1.16: `ort`'s ORT_API_VERSION resolves to 24 because
|
|
# `fastembed` enables its `api-24` feature.
|
|
pip install 'onnxruntime>=1.24'
|
|
# Pre-flight, not just a pin. `ort` deadlocks rather than errors on
|
|
# ANY failure inside `load_dylib_from_path` — a version mismatch and
|
|
# a library that cannot be resolved at all both re-enter the `Once`
|
|
# that `setup_api()` is initialising, and `std::sync::Once` blocks
|
|
# forever on re-entry. Either way the job burns its full 30-minute
|
|
# timeout at 0% CPU with nothing in the log. Assert both conditions
|
|
# here so a bad runner fails in seconds with a readable message.
|
|
python - <<'PY' >> "$GITHUB_ENV"
|
|
import pathlib, sys
|
|
|
|
def die(msg: str) -> None:
|
|
print(f"::error::{msg}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
try:
|
|
import onnxruntime
|
|
except Exception as exc: # noqa: BLE001 - any import failure is fatal here
|
|
die(f"onnxruntime is not importable: {exc}")
|
|
|
|
version = onnxruntime.__version__
|
|
try:
|
|
major, minor = (int(part) for part in version.split(".")[:2])
|
|
except ValueError:
|
|
die(f"cannot parse onnxruntime version {version!r}")
|
|
if (major, minor) < (1, 24):
|
|
die(
|
|
f"onnxruntime {version} is too old: ort requires >= 1.24 "
|
|
"(ORT_API_VERSION=24, set by fastembed's api-24 feature). "
|
|
"ort DEADLOCKS instead of erroring below this, so the tests "
|
|
"would hang rather than fail."
|
|
)
|
|
|
|
capi = pathlib.Path(onnxruntime.__file__).parent / "capi"
|
|
libs = sorted(capi.glob("libonnxruntime.so*")) or sorted(capi.glob("libonnxruntime*.dylib"))
|
|
if not libs:
|
|
die(f"no libonnxruntime shared library under {capi}")
|
|
|
|
print(f"ORT_DYLIB_PATH={libs[0]}")
|
|
print(f"onnxruntime {version} -> {libs[0]}", file=sys.stderr)
|
|
PY
|
|
- name: cargo fmt --check
|
|
run: cargo fmt --all -- --check
|
|
- name: cargo clippy
|
|
run: cargo clippy --workspace -- -D warnings
|
|
- name: cargo test
|
|
run: cargo test --workspace
|
|
|
|
simulator-e2e:
|
|
name: simulator e2e (${{ matrix.os }})
|
|
needs: rust-changes
|
|
if: needs.rust-changes.outputs.rust == 'true'
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 30
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- name: Install stable toolchain
|
|
uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
toolchain: 1.95.0
|
|
- name: Cache cargo registry + build
|
|
uses: Swatinem/rust-cache@v2
|
|
- name: cargo test simulator-backed proxy e2e
|
|
run: cargo test -p headroom-proxy --test e2e_simulators
|
|
|
|
wheels:
|
|
name: wheels (${{ matrix.target }})
|
|
needs: rust-changes
|
|
if: needs.rust-changes.outputs.rust == 'true'
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 45
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- os: ubuntu-latest
|
|
target: x86_64-unknown-linux-gnu
|
|
maturin-target: x86_64
|
|
- os: macos-14
|
|
target: aarch64-apple-darwin
|
|
maturin-target: aarch64-apple-darwin
|
|
- os: macos-15-intel
|
|
target: x86_64-apple-darwin
|
|
maturin-target: x86_64-apple-darwin
|
|
# Intel macOS uses `ort-load-dynamic` (no prebuilt ORT from ort-sys);
|
|
# Apple Silicon bundles ORT via `ort-download-binaries-rustls-tls`.
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.11'
|
|
- name: "Build wheel (single-wheel architecture builds headroom-ai)"
|
|
uses: PyO3/maturin-action@v1
|
|
# Maturin reads `[tool.maturin]` from the root `pyproject.toml`
|
|
# which points at `crates/headroom-py/Cargo.toml` for the cdylib.
|
|
# Output is `headroom_ai-<ver>-<py>-<py>-<platform>.whl` containing
|
|
# both Python source and the compiled `headroom/_core.so`.
|
|
with:
|
|
command: build
|
|
args: --release --out dist
|
|
target: ${{ matrix.maturin-target }}
|
|
- name: Upload wheel artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: wheels-${{ matrix.target }}
|
|
path: dist/*.whl
|
|
|
|
audit:
|
|
name: audit
|
|
needs: rust-changes
|
|
if: needs.rust-changes.outputs.rust == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
toolchain: 1.95.0
|
|
- uses: Swatinem/rust-cache@v2
|
|
- name: Install cargo-audit + cargo-deny
|
|
uses: taiki-e/install-action@v2
|
|
with:
|
|
tool: cargo-audit,cargo-deny
|
|
# Blocking. Soft-failing this made it useless: RUSTSEC-2026-0258 (h2,
|
|
# unbounded empty DATA frames -> unbounded memory or a panic) was
|
|
# reported by this job for as long as it existed and never turned a run
|
|
# red, so nobody acted on it. Accepted advisories go in audit.toml with
|
|
# a written reason rather than being swallowed wholesale here.
|
|
- name: cargo audit
|
|
run: cargo audit
|
|
- name: cargo deny check licenses
|
|
continue-on-error: true
|
|
run: cargo deny check licenses
|
|
|
|
# Blocking on every PR that touches Rust. Safe to harden now because the
|
|
# harness fails only on a Diff — `parity-run` sets `any_diffs` inside the
|
|
# diffed loop alone, so the 65 fixtures still served by `stub_comparator!`
|
|
# report as Skipped and cannot turn this red. Measured on main today:
|
|
# 111 matched / 65 skipped / 0 diffed.
|
|
#
|
|
# What it protects: the recorded fixtures are frozen Python output, so this
|
|
# gate catches the Rust side drifting away from that snapshot — exactly the
|
|
# failure mode the ongoing port produces. It cannot detect the Python side
|
|
# drifting away from the fixtures; that needs re-recording, not this job.
|
|
parity:
|
|
name: parity
|
|
needs: rust-changes
|
|
if: needs.rust-changes.outputs.rust == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
toolchain: 1.95.0
|
|
- uses: Swatinem/rust-cache@v2
|
|
# No Python toolchain: headroom-parity links headroom-core directly and
|
|
# never crosses into the interpreter, so the venv + maturin + `pip
|
|
# install -e .` setup this job used to do was pure overhead.
|
|
- name: Run parity harness
|
|
run: make test-parity
|