#!/usr/bin/env python3 """check-wasmtime-pin.py: every workflow that runs wasmtime runs the one pinned release, with both of its determinism settings. WAI's WebAssembly legs hold a module's output to pinned bits. A stock wasmtime leaves NaN canonicalisation off and lets each relaxed SIMD instruction take the host's behaviour, and two versions of it can lower one instruction two ways, so a leg's bits would be the engine's choice. The install steps' comment says every workflow pins one version and passes both settings; this makes that true of every workflow, not only of the ones that comment sits in. In every `.github/workflows/*.yml`, with continued lines (`\\` at the end) joined, this requires that 1. every download of a wasmtime release names version PIN (the `v=` the step sets, or the version in the URL), and the SHA-256 it checks is the digest of that archive in DIGESTS; 2. wasmtime is installed no other way (`cargo install wasmtime-cli`, the install script, or an action); 3. every `wasmtime run`, `wasmtime serve` or `wasmtime compile`, every `wasmtime` given a module path, and every `CARGO_TARGET__RUNNER` that is wasmtime, passes `-W nan-canonicalization=y` and `-W relaxed-simd-deterministic=y`; 4. at least one download and one run were found, so a check that looked in the wrong place fails rather than passing on nothing. check-wasmtime-pin.py [--root DIR] Exit 0 when all four hold, 1 with one line a finding otherwise. Standard library only. """ import os import re import sys PIN = "50.0.1" # The release archives a workflow may download, by name, with the SHA-256 the release lists. DIGESTS = { "wasmtime-v50.0.1-x86_64-linux.tar.xz": "685466455dd42a14774a86c36c8f2e45cbb9bd6d92a850139315df60a75498a5", } FLAGS = ("nan-canonicalization=y", "relaxed-simd-deterministic=y") DOWNLOAD = re.compile(r"bytecodealliance/wasmtime/releases/download/") URL_VERSION = re.compile(r"releases/download/v(\d[^/\"'\s]*)/") SET_VERSION = re.compile(r"^\s*v=[\"']?([^\"'\s;]+)") ARCHIVE = re.compile(r"wasmtime-v(\$v|\$\{v\}|\d[^-\"'\s]*)-([a-z0-9_]+-[a-z]+)\.tar\.xz") DIGEST = re.compile(r"\b([0-9a-f]{64})\b.*sha256sum -c") OTHER_INSTALL = re.compile( r"cargo\s+(?:\S+\s+)*install\s+(?:\S+\s+)*wasmtime-cli|wasmtime\.dev/install|uses:\s*\S*wasmtime" ) # wasmtime as a command: run, serve or compile, or given a module path (`wasmtime x.wasm`). INVOKE = re.compile( r"(?:^|[\s;&|(`/\"'=])wasmtime\s+(?:(?:run|serve|compile)\b|[^\s-]\S*\.(?:c?wasm|wat)\b)" ) RUNNER = re.compile(r"CARGO_TARGET_\w+_RUNNER\s*[:=]\s*[\"']?wasmtime\b") def logical_lines(text): """(first line number, text) with backslash-continued lines joined.""" out, buf, start = [], "", 0 for n, line in enumerate(text.splitlines(), 1): if not buf: start = n if line.rstrip().endswith("\\"): buf += line.rstrip()[:-1] + " " continue out.append((start, buf + line)) buf = "" if buf: out.append((start, buf)) return out def check_file(path, rel, findings): lines = logical_lines(open(path, encoding="utf-8").read()) downloads = runs = 0 for i, (n, line) in enumerate(lines): code = line.lstrip() if code.startswith("#"): continue if OTHER_INSTALL.search(line): findings.append(f"{rel}:{n}: installs wasmtime some other way; download the pinned release ({PIN}) by version and SHA-256") if DOWNLOAD.search(line): downloads += 1 m = URL_VERSION.search(line) ver = m.group(1) if m else None if ver is None: # the URL takes the version from the step's `v=` for _, prev in reversed(lines[max(0, i - 12) : i]): s = SET_VERSION.match(prev) if s: ver = s.group(1) break if ver != PIN: findings.append(f"{rel}:{n}: downloads wasmtime {ver or '(version not found)'}; every workflow pins {PIN}") near = lines[max(0, i - 12) : i + 5] arch = None for _, other in near: a = ARCHIVE.search(other) if a: arch = a.group(2) break name = f"wasmtime-v{ver}-{arch}.tar.xz" if arch else None digests = [d.group(1) for _, other in lines[i : i + 5] for d in [DIGEST.search(other)] if d] if name not in DIGESTS: findings.append(f"{rel}:{n}: downloads {name or 'an archive whose name was not found'}, which has no pinned digest here") elif not digests: findings.append(f"{rel}:{n}: downloads {name} and checks no SHA-256 within four lines") elif digests[0] != DIGESTS[name]: findings.append(f"{rel}:{n}: checks {name} against {digests[0][:12]}..., not its pinned digest {DIGESTS[name][:12]}...") for rx in (INVOKE, RUNNER): m = rx.search(line) if m: runs += 1 rest = line[m.start() :] missing = [f for f in FLAGS if f not in rest] if missing: findings.append(f"{rel}:{n}: runs wasmtime without {' and '.join('-W ' + f for f in missing)}") break return downloads, runs def main(argv): root = "." if len(argv) == 3 and argv[1] == "--root": root = argv[2] elif len(argv) != 1: sys.exit("usage: check-wasmtime-pin.py [--root DIR]") wf = os.path.join(root, ".github", "workflows") findings, downloads, runs = [], 0, 0 for name in sorted(os.listdir(wf)) if os.path.isdir(wf) else []: if name.endswith((".yml", ".yaml")): d, r = check_file(os.path.join(wf, name), f".github/workflows/{name}", findings) downloads += d runs += r if downloads == 0 or runs == 0: findings.append(f"found {downloads} wasmtime download(s) and {runs} run(s) under {wf}: this check looked in the wrong place") for f in findings: print(f"::error::{f}") if findings: return 1 print(f"check-wasmtime-pin: {downloads} downloads of wasmtime {PIN}, each checked against its digest; {runs} runs, each with both settings") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))