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
160 changes: 160 additions & 0 deletions .github/scripts/verify_glib_provenance.py
Original file line number Diff line number Diff line change
@@ -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 <path>`) 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 <glib-0.18.5.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()
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
31 changes: 31 additions & 0 deletions .github/workflows/vendor-provenance.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion vendor/glib-0.18.5-patched/.cargo-ok

This file was deleted.

17 changes: 17 additions & 0 deletions vendor/glib-0.18.5-patched/PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading