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
110 changes: 106 additions & 4 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@

name: "Audit (cargo)"

# Every tracked Cargo.lock is checked against the RustSec advisory database.
# The weekly trigger is what makes this a gate rather than a formality: a new
# advisory can land against a lock file nobody has touched, and without the
# schedule the audit would only run when a lock changes.
# Every tracked Cargo.lock is checked against the RustSec advisory database,
# and every pkg/contrib source-tarball pin is checked against OSV. The weekly
# trigger is what makes this a gate rather than a formality: a new advisory can
# land against a file nobody has touched, and without the schedule the audit
# would only run when a lock or a pin changes.
on:
pull_request:
paths:
- '**/Cargo.lock'
- '**/Cargo.toml'
# Only wasmtime: both scripts hardcode that pin, so firing this job
# for an njs or wasi-sysroot change would report a green that covers
# nothing. The Makefile is here because its build flags decide which
# crates the audit gate treats as linked.
- 'pkg/contrib/src/wasmtime/version'
- 'pkg/contrib/src/wasmtime/SHA512SUMS'
- 'pkg/contrib/src/wasmtime/Makefile'
- 'pkg/contrib/check-advisories.py'
- 'pkg/contrib/check-lock-audit.py'
Comment thread
andypost marked this conversation as resolved.
- 'pkg/contrib/test_check_lock_audit.py'
- 'pkg/contrib/wasmtime-lock-audit.allow.json'
- 'pkg/eol.json'
# A change to this file must run this file.
- .github/workflows/audit.yml
push:
Expand All @@ -18,6 +31,18 @@ on:
paths:
- '**/Cargo.lock'
- '**/Cargo.toml'
# Only wasmtime: both scripts hardcode that pin, so firing this job
# for an njs or wasi-sysroot change would report a green that covers
# nothing. The Makefile is here because its build flags decide which
# crates the audit gate treats as linked.
- 'pkg/contrib/src/wasmtime/version'
- 'pkg/contrib/src/wasmtime/SHA512SUMS'
- 'pkg/contrib/src/wasmtime/Makefile'
- 'pkg/contrib/check-advisories.py'
- 'pkg/contrib/check-lock-audit.py'
- 'pkg/contrib/test_check_lock_audit.py'
- 'pkg/contrib/wasmtime-lock-audit.allow.json'
- 'pkg/eol.json'
- .github/workflows/audit.yml
schedule:
- cron: "0 6 * * 1"
Expand Down Expand Up @@ -80,3 +105,80 @@ jobs:
# open.
- name: Audit ${{ matrix.lock }}
run: cargo audit --deny warnings --file '${{ matrix.lock }}'

pins:
# A dependency pinned as a source tarball has a lock file inside the
# archive, but it is not tracked in this repository. Check the direct
# source-package pins against OSV, then audit the archived lock. That
# lock covers the whole Wasmtime workspace while Unit links one C API
# library, so check-lock-audit.py fails only on a finding against a crate
# that library actually links, and only when the allowlist does not carry
# a reason for that exact version.
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5

- run: rustup update stable

- name: Check source-tarball pins
run: python3 pkg/contrib/check-advisories.py

- name: Test the lock audit gate
run: |
pip install --disable-pip-version-check --quiet pytest
python3 -m pytest pkg/contrib/test_check_lock_audit.py

- name: Install cargo-audit
uses: taiki-e/install-action@v2
with:
tool: cargo-audit

- name: Resolve the Wasmtime pin
id: pin
run: |
version="$(sed -n -E 's/^WASMTIME_VERSION := ([0-9.]+)$/\1/p' \
pkg/contrib/src/wasmtime/version)"
test -n "$version"
echo "archive=wasmtime-v${version}-src.tar.gz" >> "$GITHUB_OUTPUT"

# The archive is immutable and its digest is committed, so this is one
# download per pin instead of 190 MB on every run and every Monday. The
# digest is checked below either way, so a poisoned entry still fails.
- name: Cache the Wasmtime source archive
uses: actions/cache@v4
with:
path: ~/wasmtime-archive
key: >-
wasmtime-src-${{ hashFiles('pkg/contrib/src/wasmtime/SHA512SUMS') }}

- name: Unpack the Wasmtime source archive
run: |
archive="${{ steps.pin.outputs.archive }}"
mkdir -p ~/wasmtime-archive
# --retry because on a scheduled gate a mirror blip and an advisory
# must not both show up as a red job.
test -f ~/"wasmtime-archive/$archive" || \
curl --fail --location --show-error --retry 3 --retry-delay 5 \
--output ~/"wasmtime-archive/$archive" \
"https://packages.freeunit.org/wasmtime/$archive"
digest="$(awk -v archive="$archive" '$2 == archive { print $1 }' \
pkg/contrib/src/wasmtime/SHA512SUMS)"
test -n "$digest"
printf '%s %s\n' "$digest" ~/"wasmtime-archive/$archive" \
| sha512sum --check
mkdir "$RUNNER_TEMP/wasmtime"
tar -xzf ~/"wasmtime-archive/$archive" --strip-components=1 \
--directory "$RUNNER_TEMP/wasmtime"

- name: Audit the Wasmtime source lock
run: |
# cargo audit exits non-zero on any finding, and the archived lock
# covers the whole Wasmtime workspace, so the report is the input to
# the gate rather than the verdict. An empty report is a failure:
# check-lock-audit.py rejects anything that is not a cargo audit run.
cargo audit --json --file "$RUNNER_TEMP/wasmtime/Cargo.lock" \
> "$RUNNER_TEMP/audit.json" || true
python3 pkg/contrib/check-lock-audit.py \
--report "$RUNNER_TEMP/audit.json" \
--tree "$RUNNER_TEMP/wasmtime"
64 changes: 56 additions & 8 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ jobs:
test:
needs: [prepare, core]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
# 45, not 30: the wasm leg now runs the suite on a cold contrib wasmtime
# build (the cache key moves with the pin) and builds the guest toolchain.
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -629,7 +631,13 @@ jobs:
uses: actions/cache@v6
with:
path: pkg/contrib/wasmtime/artifacts
key: wasmtime-${{ steps.metadata.outputs.wasmtime_version }}-${{ runner.os }}-${{ runner.arch }}
# Every file that decides what gets downloaded, verified and
# built, not just the version. The Makefile matters most: its cmake
# flags decide which features the library carries, so without it a
# feature change with no version bump would restore the old library
# and skip the rebuild, leaving the tests and the audit gate
# disagreeing about what was linked.
key: wasmtime-${{ steps.metadata.outputs.wasmtime_version }}-${{ hashFiles('pkg/contrib/src/wasmtime/*') }}-${{ runner.os }}-${{ runner.arch }}

- name: Make wasmtime
run: |
Expand All @@ -638,14 +646,40 @@ jobs:

- name: Configure wasm
run: |
./configure wasm --include-path=pkg/contrib/wasmtime/artifacts/include --lib-path=pkg/contrib/wasmtime/artifacts/lib
./configure wasm --rpath --include-path=pkg/contrib/wasmtime/artifacts/include --lib-path=pkg/contrib/wasmtime/artifacts/lib
if: steps.metadata.outputs.module == 'wasm'

- name: Make wasm
run: |
make wasm
if: steps.metadata.outputs.module == 'wasm'

# The guest fixture is compiled at test time from pkg/contrib sources:
# wasi-sysroot for wasi-libc and libunit-wasm for the request ABI.
- name: Cache wasi-sysroot
id: cache-wasi-sysroot
if: steps.metadata.outputs.module == 'wasm'
uses: actions/cache@v6
with:
path: |
pkg/contrib/wasi-sysroot
pkg/contrib/libunit-wasm
# Every file that decides what gets downloaded, verified and built,
# not just the version: otherwise a checksum or build-rule change
# restores the old tree and skips the make below.
key: wasi-sysroot-${{ hashFiles('pkg/contrib/src/wasi-sysroot/*', 'pkg/contrib/src/libunit-wasm/*') }}-${{ runner.os }}-${{ runner.arch }}

- name: Make wasi-sysroot
run: |
make -C pkg/contrib .wasi-sysroot libunit-wasm
if: steps.metadata.outputs.module == 'wasm' && steps.cache-wasi-sysroot.outputs.cache-hit != 'true'

- name: Install wasm guest toolchain
run: |
sudo apt install -y clang lld
sudo apt install -y "libclang-rt-$(clang -dumpversion | cut -d. -f1)-dev-wasm32"
if: steps.metadata.outputs.module == 'wasm'

##
## wasm-wasi-component
##
Expand Down Expand Up @@ -725,8 +759,6 @@ jobs:
run: |
sudo apt install -y python3-pytest

if: steps.metadata.outputs.module != 'wasm'

- name: Run ${{ steps.metadata.outputs.module }} tests
# GitHub runners are UTC, where localtime() and gmtime() agree, so a
# UTC-only run cannot tell a correct HTTP-date from one rendered in
Expand All @@ -743,12 +775,28 @@ jobs:
if [ "${{ steps.metadata.outputs.module }}" == "python" ]; then
export UNIT_PYTHONHOME="${UNIT_PYTHON_PREFIX}"
fi
if [ "${{ matrix.build }}" == "wasm-wasi-component" ]; then
pytest-3 --print-log ${{ steps.metadata.outputs.testpath }}
# The wasm legs build their guest fixture as the runner user, so
# they run unprivileged to keep the result reachable by pytest; the
# others install applications as root.
if [ "${{ matrix.build }}" == "wasm-wasi-component" ] || [ "${{ matrix.build }}" == "wasm" ]; then
set -o pipefail
pytest-3 --print-log ${{ steps.metadata.outputs.testpath }} 2>&1 | tee "$RUNNER_TEMP/pytest.out"
else
sudo -E pytest-3 --print-log ${{ steps.metadata.outputs.testpath }}
fi
if: steps.metadata.outputs.module != 'wasm'

# check_prerequisites() skips the whole file when the clang/wasi-sysroot
# fixture cannot be built, so an incomplete toolchain would leave this
# leg green while it covers nothing. At least one smoke test must have
# actually passed.
- name: Fail if the wasm smoke test skipped
if: ${{ !cancelled() && steps.metadata.outputs.module == 'wasm' }}
run: |
grep -qE 'test_wasm\.py::.*PASSED' "$RUNNER_TEMP/pytest.out" || {
echo "no test_wasm.py test passed; the guest toolchain skipped it:" >&2
grep -E 'SKIPPED|feature' "$RUNNER_TEMP/pytest.out" >&2 || true
exit 1
}

# test_capget_fallback.py needs a unitd that actually calls capget(),
# which a root one never does, so every leg above skips it -- the file
Expand Down
2 changes: 1 addition & 1 deletion EOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ needs an upgrade or replacement decision instead.
| Eclipse Jetty | `jetty-util` / `jetty-server` / `jetty-http` jars bundled by the Java module | 9.4.58.v20250814 | **Aug 2025 (EOL)** † | Jetty 9.4 lost community support on 2025-08-14 and the bundled build is its last release. Needs an upgrade-or-replace decision — Jetty 10/11 are EOL too; 12.x is the supported line. |
| Eclipse ECJ (JDT batch compiler) | JSP compilation jar bundled by the Java module | 3.26.0 | none published | Tracks Eclipse releases; endoflife.date has no product for it. Pinned build is from Jun 2021; current is 3.42.0 (Jun 2025). |
| ClassGraph | classpath scanning jar bundled by the Java module | latest | — | No upstream EOL schedule; pinned and bumped as needed. |
| Wasmtime (C API) | WebAssembly runtime built by `pkg/contrib` and linked by the `wasm` module | 43.0.1 | none published | Fetched as a source tarball (`pkg/contrib/src/wasmtime/version`), so `cargo audit` never sees it. Four majors behind the Rust pin below, and affected by RUSTSEC-2026-0269 and -0222 — see #400. |
| Wasmtime (C API) | WebAssembly runtime built by `pkg/contrib` and linked by the `wasm` module | 47.0.4 | none published | Fetched as a source tarball (`pkg/contrib/src/wasmtime/version`), so `cargo audit` never sees it; the `Audit (cargo)` pins job queries OSV for the pinned version instead. Same 47.0.4 as the Rust pin below; the 43.0.1 bump closed RUSTSEC-2026-0269, -0222, -0114 and three wasmtime-wasi path/permission advisories — see #400. |
| Wasmtime (Rust crate) | WebAssembly runtime used by the `wasm-wasi-component` module | 47.0.4 | none published | `src/wasm-wasi-component/Cargo.lock`, audited by the `Audit (cargo)` workflow on every lock. Bumped with the advisories it closes. |

## Rules
Expand Down
108 changes: 108 additions & 0 deletions pkg/contrib/check-advisories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Check tarball-pinned dependencies against OSV advisories.

`cargo audit` reads Cargo.lock files only, so a dependency pinned as a source
tarball under pkg/contrib is invisible to it. This script reads the version
pin and asks OSV, which mirrors the RustSec advisory database and also carries
the GHSA/CVE aliases.

It also checks that the three places naming the pin agree:
- pkg/contrib/src/wasmtime/version
- pkg/contrib/src/wasmtime/SHA512SUMS
- pkg/eol.json (dependencies.wasmtime_c_api)

Exit codes: 0 clean, 1 advisories found, 2 configuration or query failure.
"""
import json
import re
import sys
import urllib.error
import urllib.request
from pathlib import Path

REPO = Path(__file__).resolve().parents[2]
VERSION_FILE = REPO / "pkg/contrib/src/wasmtime/version"
SUMS_FILE = REPO / "pkg/contrib/src/wasmtime/SHA512SUMS"
EOL_FILE = REPO / "pkg/eol.json"
OSV_URL = "https://api.osv.dev/v1/query"

# The contrib tarball is the wasmtime workspace; pinning it pins both crates
# that the C API links.
CRATES = ("wasmtime", "wasmtime-wasi")
Comment thread
andypost marked this conversation as resolved.


def read_version() -> str:
match = re.search(r"WASMTIME_VERSION\s*:=\s*(\S+)", VERSION_FILE.read_text())
if not match:
fail(f"no WASMTIME_VERSION in {VERSION_FILE.relative_to(REPO)}")
return match.group(1)


def fail(message: str) -> None:
print(f"error: {message}", file=sys.stderr)
sys.exit(2)


def check_sums(version: str) -> None:
tarball = f"wasmtime-v{version}-src.tar.gz"
pattern = re.compile(rf"\s{tarball}$", re.MULTILINE)
if not pattern.search(SUMS_FILE.read_text()):
fail(f"{SUMS_FILE.relative_to(REPO)} has no digest for {tarball}")


def check_eol_json(version: str) -> None:
config = json.loads(EOL_FILE.read_text())
pinned = config["dependencies"]["wasmtime_c_api"]["version"]
if pinned != version:
fail(
f"{EOL_FILE.relative_to(REPO)} says wasmtime_c_api {pinned}, but the "
f"pin is {version}"
)


def query(crate: str, version: str) -> list:
body = json.dumps(
{"package": {"name": crate, "ecosystem": "crates.io"}, "version": version}
).encode()
request = urllib.request.Request(
OSV_URL, data=body, headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response).get("vulns", [])
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as error:
fail(f"OSV query for {crate} {version} failed: {error}")


def main() -> int:
version = read_version()
check_sums(version)
check_eol_json(version)

found = False
seen = set()
for crate in CRATES:
for vuln in query(crate, version):
aliases = vuln.get("aliases", [])
# OSV carries the GHSA and RUSTSEC records of one advisory as two
# entries that alias each other; report each advisory once.
key = tuple(sorted({vuln["id"], *aliases}))
if key in seen:
continue
seen.add(key)
found = True
alias_text = ", ".join(aliases)
suffix = f" ({alias_text})" if alias_text else ""
print(f"{crate} {version}: {vuln['id']}{suffix}")
if vuln.get("summary"):
print(f" {vuln['summary']}")

if found:
return 1

print(f"{', '.join(CRATES)} {version}: no advisories")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading