diff --git a/.github/scripts/verify_glib_provenance.py b/.github/scripts/verify_glib_provenance.py new file mode 100755 index 0000000..bb238fe --- /dev/null +++ b/.github/scripts/verify_glib_provenance.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Verify the vendored glib tree against the published crate (issue #71). + +`vendor/glib-0.18.5-patched/` compiles into every Linux desktop build, and its +PROVENANCE.md claims the directory is the published `glib` 0.18.5 source apart +from a small set of documented deltas. This script enforces that claim: + +1. Parse the pinned crate checksum and delta list from PROVENANCE.md (the + fenced "Pinned deltas" block — the one place a reviewer reads them). +2. Download the `.crate` from crates.io (or read `--crate `) and verify + it against the pinned checksum. +3. Diff the extracted source against the vendored tree. Fail unless every + difference is a documented delta whose file matches its pinned SHA-256, + and every documented delta is an actual difference. + +Editing a delta file therefore fails CI unless PROVENANCE.md is updated in the +same change; any other edit, addition, or removal fails outright. + +Usage: verify_glib_provenance.py [--crate ] + +Removal gate: delete this script and its workflow together with the vendored +directory when Tauri's Linux stack uses glib >= 0.20. +""" + +import argparse +import hashlib +import io +import os +import re +import sys +import tarfile +import urllib.request + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +VENDOR_DIR = os.path.join(REPO_ROOT, "vendor", "glib-0.18.5-patched") +MANIFEST = "PROVENANCE.md" +CRATE_NAME, CRATE_VERSION = "glib", "0.18.5" +CRATE_URL = ( + f"https://static.crates.io/crates/{CRATE_NAME}/" + f"{CRATE_NAME}-{CRATE_VERSION}.crate" +) + +BLOCK_RE = re.compile(r"```provenance\n(.*?)```", re.S) +CRATE_LINE_RE = re.compile(r"^crate-sha256: ([0-9a-f]{64})$") +DELTA_LINE_RE = re.compile(r"^delta: (\S+) sha256=([0-9a-f]{64})$") + + +def sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def parse_manifest(text: str) -> tuple[str, dict[str, str]]: + """Return (pinned crate checksum, {relative path: pinned sha256}).""" + blocks = BLOCK_RE.findall(text) + if len(blocks) != 1: + sys.exit(f"{MANIFEST}: expected exactly one ```provenance block, found {len(blocks)}") + crate_sum, deltas = None, {} + for line in blocks[0].splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if m := CRATE_LINE_RE.match(line): + if crate_sum is not None: + sys.exit(f"{MANIFEST}: duplicate crate-sha256 line") + crate_sum = m.group(1) + elif m := DELTA_LINE_RE.match(line): + path, digest = m.groups() + if path == MANIFEST or path in deltas: + sys.exit(f"{MANIFEST}: invalid or duplicate delta {path!r}") + deltas[path] = digest + else: + sys.exit(f"{MANIFEST}: unrecognised provenance line {line!r}") + if crate_sum is None: + sys.exit(f"{MANIFEST}: missing crate-sha256 line") + return crate_sum, deltas + + +def upstream_files(crate_bytes: bytes) -> dict[str, bytes]: + """Map relative path -> content for every regular file in the .crate.""" + prefix = f"{CRATE_NAME}-{CRATE_VERSION}/" + files = {} + with tarfile.open(fileobj=io.BytesIO(crate_bytes), mode="r:gz") as tar: + for member in tar.getmembers(): + if member.isdir(): + continue + if not member.isfile() or not member.name.startswith(prefix): + sys.exit(f"unexpected entry in published crate: {member.name!r}") + files[member.name[len(prefix):]] = tar.extractfile(member).read() + return files + + +def vendored_files() -> dict[str, bytes]: + """Map relative path -> content for every file under the vendored tree.""" + files = {} + for root, dirs, names in os.walk(VENDOR_DIR, followlinks=False): + for name in dirs + names: + full = os.path.join(root, name) + rel = os.path.relpath(full, VENDOR_DIR).replace(os.sep, "/") + if os.path.islink(full): + sys.exit(f"vendored tree contains a symlink: {rel}") + if name in names: + with open(full, "rb") as f: + files[rel] = f.read() + return files + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--crate", help="read the .crate from this path instead of crates.io") + args = parser.parse_args() + + vendored = vendored_files() + if MANIFEST not in vendored: + sys.exit(f"{MANIFEST} missing from {VENDOR_DIR}") + crate_sum, deltas = parse_manifest(vendored.pop(MANIFEST).decode()) + + if args.crate: + with open(args.crate, "rb") as f: + crate_bytes = f.read() + else: + with urllib.request.urlopen(CRATE_URL, timeout=60) as resp: + crate_bytes = resp.read() + actual_sum = sha256(crate_bytes) + if actual_sum != crate_sum: + sys.exit(f"crate checksum mismatch: pinned {crate_sum}, downloaded {actual_sum}") + upstream = upstream_files(crate_bytes) + + errors = [] + for path in sorted(set(upstream) | set(vendored)): + if path not in vendored: + errors.append(f"removed from vendored tree: {path}") + elif path not in upstream: + errors.append(f"added to vendored tree (undocumented): {path}") + elif upstream[path] == vendored[path]: + if path in deltas: + errors.append(f"documented delta is identical to upstream: {path}") + elif path not in deltas: + errors.append(f"undocumented change: {path}") + elif sha256(vendored[path]) != deltas[path]: + errors.append( + f"delta {path} changed without updating {MANIFEST} " + f"(now sha256={sha256(vendored[path])})" + ) + for path in sorted(set(deltas) - set(upstream)): + errors.append(f"documented delta is not a published file: {path}") + + if errors: + print("vendored glib provenance check FAILED:", file=sys.stderr) + for e in errors: + print(f" - {e}", file=sys.stderr) + sys.exit(1) + print( + f"vendored glib {CRATE_VERSION} matches the published crate " + f"({crate_sum[:12]}…) apart from {len(deltas)} documented delta(s) " + f"and {MANIFEST}" + ) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1972b5f..ec1a222 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -33,13 +33,13 @@ jobs: build-mode: none steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + - uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} # Path and rule exclusions, each with its reason, live in the config # file so they are reviewable in one place. config-file: ./.github/codeql/codeql-config.yml - - uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + - uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/vendor-provenance.yml b/.github/workflows/vendor-provenance.yml new file mode 100644 index 0000000..0909bf4 --- /dev/null +++ b/.github/workflows/vendor-provenance.yml @@ -0,0 +1,31 @@ +name: vendor-provenance + +# Guards vendor/glib-0.18.5-patched/ against drift (issue #71): the vendored +# crate is product code, and its PROVENANCE.md claim — published source plus a +# pinned set of deltas — is enforced here rather than by review alone. Remove +# together with the vendored directory (glib >= 0.20 removal gate). +on: + push: + branches: [main] + paths: + - "vendor/glib-0.18.5-patched/**" + - ".github/scripts/verify_glib_provenance.py" + - ".github/workflows/vendor-provenance.yml" + pull_request: + paths: + - "vendor/glib-0.18.5-patched/**" + - ".github/scripts/verify_glib_provenance.py" + - ".github/workflows/vendor-provenance.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + glib: + name: vendored glib matches published crate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: verify vendored glib against crates.io + run: python3 .github/scripts/verify_glib_provenance.py diff --git a/vendor/glib-0.18.5-patched/.cargo-ok b/vendor/glib-0.18.5-patched/.cargo-ok deleted file mode 100644 index 5f8b795..0000000 --- a/vendor/glib-0.18.5-patched/.cargo-ok +++ /dev/null @@ -1 +0,0 @@ -{"v":1} \ No newline at end of file diff --git a/vendor/glib-0.18.5-patched/PROVENANCE.md b/vendor/glib-0.18.5-patched/PROVENANCE.md index 22d8b9e..0fca757 100644 --- a/vendor/glib-0.18.5-patched/PROVENANCE.md +++ b/vendor/glib-0.18.5-patched/PROVENANCE.md @@ -17,5 +17,22 @@ copy is a path dependency; it does not change runtime behavior. `cargo audit` matches by package name and version, so the workflow must continue ignoring RUSTSEC-2024-0429 even though the runtime code is patched. +## Pinned deltas (machine-checked) + +CI (`.github/workflows/vendor-provenance.yml`, running +`.github/scripts/verify_glib_provenance.py`) downloads the published crate, +verifies it against `crate-sha256`, and diffs it against this directory. It +fails on any difference not listed below, and on any listed file whose content +no longer matches its pinned hash — so changing a delta requires updating its +hash here in the same change. This file itself is the only addition. + +```provenance +crate-sha256: 233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5 +# RUSTSEC-2024-0429 backport (gtk-rs-core#1343) +delta: src/variant_iter.rs sha256=a0f5ee8acb8faa089bcdfbc9a57372609fce7654026ccef7d9a224d05a654ccc +# crate-level allow(warnings), reproducing Cargo's --cap-lints for registry deps +delta: src/lib.rs sha256=f118b6507cf8c7176a70963ec6ad890f5020559cf4e5a87131eddae61e4fcb3a +``` + Remove this directory, the workspace exclusion, the `[patch.crates-io]` entry, and the audit exception when Tauri's Linux stack uses `glib` 0.20 or newer.