#!/bin/sh # Download the public catalog as a normal user and run its guided workflow. set -eu umask 077 fail() { printf '%s\n' "cve-patch: $*" >&2 exit 2 } help() { cat <<'USAGE' Usage: sh run.sh --patch CVE-YYYY-NNNN [--work-dir PATH] [--source PATH] [--abi NUMBER] [--jobs NUMBER] [--plan] sh run.sh --verify CVE-YYYY-NNNN [--work-dir PATH] sh run.sh --help Example: curl -fsSL https://raw.githubusercontent.com/ashhart/CVE-patch/main/run.sh | sh -s -- --patch CVE-2026-98432 Requires Linux, a normal user, Python 3.10 or newer, and Git. The bootstrap downloads the public ashhart/CVE-patch main branch over HTTPS. It retains a clean commit-addressed checkout under $XDG_DATA_HOME/cve-patch/checkouts/ or $HOME/.local/share/cve-patch/checkouts/. An existing checkout is checked and never reset or overwritten. Patching requires a controlling terminal for the guided workflow's prompts. --plan and --verify can run without a terminal. The bootstrap does not install dependencies or reboot the machine. Read run.sh before running downloaded code; --help performs no download. USAGE } if [ "$#" -eq 1 ] && [ "$1" = "--help" ]; then help exit 0 fi action= identifier= plan=0 build_options=0 positive_number() { case "$2" in ''|*[!0-9]*) fail "$1 requires a positive integer" ;; esac [ "${#2}" -le 9 ] || fail "$1 is too large" [ "$2" -gt 0 ] || fail "$1 requires a positive integer" } # A function has its own positional parameters, preserving the caller's argv. validate_args() { seen_work=0 seen_source=0 seen_abi=0 seen_jobs=0 while [ "$#" -gt 0 ]; do case "$1" in --patch|--verify) [ -z "$action" ] || fail "choose exactly one of --patch and --verify" [ "$#" -ge 2 ] || fail "$1 requires a CVE ID" action=$1 identifier=$2 shift 2 ;; --work-dir|--source) flag=$1 [ "$#" -ge 2 ] || fail "$1 requires a path" [ -n "$2" ] || fail "$1 requires a nonempty path" case "$2" in --*) fail "$1 requires a path, not an option" ;; esac if [ "$flag" = "--work-dir" ]; then [ "$seen_work" -eq 0 ] || fail "--work-dir was specified twice" seen_work=1 else [ "$seen_source" -eq 0 ] || fail "--source was specified twice" seen_source=1 build_options=1 fi shift 2 ;; --abi|--jobs) [ "$#" -ge 2 ] || fail "$1 requires a positive integer" positive_number "$1" "$2" if [ "$1" = "--abi" ]; then [ "$seen_abi" -eq 0 ] || fail "--abi was specified twice" case "$2" in 0*) fail "--abi must be between 100 and 999999 without leading zeros" ;; esac [ "$2" -ge 100 ] && [ "$2" -le 999999 ] || fail "--abi must be between 100 and 999999" seen_abi=1 else [ "$seen_jobs" -eq 0 ] || fail "--jobs was specified twice" [ "$2" -le 256 ] || fail "--jobs must be between 1 and 256" seen_jobs=1 fi build_options=1 shift 2 ;; --plan) [ "$plan" -eq 0 ] || fail "--plan was specified twice" plan=1 shift ;; *) fail "unknown argument: $1; use --help" ;; esac done [ -n "$action" ] || fail "choose --patch CVE-ID or --verify CVE-ID; use --help" case "$identifier" in CVE-[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]*) ;; *) fail "invalid CVE ID; expected CVE-YYYY-NNNN with at least four final digits" ;; esac suffix=${identifier#CVE-????-} case "$suffix" in *[!0-9]*) fail "invalid CVE ID" ;; esac if [ "$action" = "--verify" ]; then [ "$build_options" -eq 0 ] && [ "$plan" -eq 0 ] || fail "--verify accepts only --work-dir" fi } validate_args "$@" [ "$(uname -s)" = "Linux" ] || fail "run this bootstrap on the actual Linux target" [ "$(id -u)" -ne 0 ] || fail "run as your normal user, without sudo" python_bin=$(command -v python3) || fail "Python 3.10 or newer is required" git_bin=$(command -v git) || fail "Git is required; install it using your distribution's instructions" "$python_bin" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)' || fail "Python 3.10 or newer is required" if [ "$action" = "--patch" ] && [ "$plan" -eq 0 ]; then if ! ( : /dev/null; then fail "patching needs a controlling terminal for confirmations; run from an interactive terminal, or use --plan" fi fi if [ -n "${XDG_DATA_HOME:-}" ]; then data_home=$XDG_DATA_HOME else [ -n "${HOME:-}" ] || fail "HOME or XDG_DATA_HOME must be set" data_home=$HOME/.local/share fi case "$data_home" in /*) ;; *) fail "XDG_DATA_HOME or HOME must name an absolute path" ;; esac cache_root=$data_home/cve-patch/checkouts # Check ownership and permissions before placing executable code in the cache. "$python_bin" - "$data_home" <<'PY' || fail "cannot prepare a private persistent checkout directory" import os from pathlib import Path import stat import sys base = Path(sys.argv[1]) for directory in (base, base / "cve-patch", base / "cve-patch" / "checkouts"): try: directory.mkdir(mode=0o700, parents=True, exist_ok=True) info = directory.lstat() if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o022: raise ValueError("must be a user-owned directory without group or other write access: " + str(directory)) except (OSError, ValueError) as exc: print(str(exc), file=sys.stderr) raise SystemExit(2) PY lock=$cache_root/.bootstrap-lock mkdir "$lock" 2>/dev/null || fail "another bootstrap holds the cache lock; inspect $lock if no bootstrap is running" download= cleanup() { if [ -n "$download" ] && [ -d "$download" ]; then rm -rf "$download" fi rmdir "$lock" 2>/dev/null || : } trap cleanup 0 trap 'exit 130' INT trap 'exit 143' TERM download=$(mktemp -d "$cache_root/.download.XXXXXXXX") || fail "cannot create a private download directory" # Ignore inherited Git redirection and hooks; no user or remote settings change. unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_CONFIG GIT_CONFIG_PARAMETERS GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_COUNT=0 GIT_TERMINAL_PROMPT=0 GIT_NO_REPLACE_OBJECTS=1 PYTHONDONTWRITEBYTECODE=1 export GIT_CONFIG_NOSYSTEM GIT_CONFIG_GLOBAL GIT_CONFIG_COUNT GIT_TERMINAL_PROMPT GIT_NO_REPLACE_OBJECTS PYTHONDONTWRITEBYTECODE git_command() { "$git_bin" -c core.hooksPath=/dev/null -c core.fsmonitor=false -c protocol.allow=never -c protocol.https.allow=always -c credential.helper= "$@" } repository=https://github.com/ashhart/CVE-patch.git printf '%s\n' "Downloading $repository main as the current user..." >&2 git_command clone --quiet --depth 1 --branch main -- "$repository" "$download/repo" || fail "download failed; no cached checkout was modified" commit=$(git_command -C "$download/repo" rev-parse --verify 'HEAD^{commit}') || fail "cannot identify the downloaded commit" case "$commit" in ''|*[!0-9a-f]*) fail "download did not produce an expected Git commit ID" ;; esac [ "${#commit}" -eq 40 ] || fail "download did not produce a 40-character Git commit ID" checkout=$cache_root/$commit check_checkout() { [ -d "$1" ] && [ ! -L "$1" ] || fail "checkout must be a real directory: $1" [ -d "$1/.git" ] && [ ! -L "$1/.git" ] || fail "checkout metadata must be a real directory: $1/.git" head=$(git_command -C "$1" rev-parse --verify 'HEAD^{commit}') || fail "cannot read cached checkout identity" [ "$head" = "$commit" ] || fail "cached checkout identity changed; inspect $1" origin=$(git_command -C "$1" config --get remote.origin.url) || fail "cached checkout has no expected origin" [ "$origin" = "$repository" ] || fail "cached checkout origin changed; inspect $1" status=$(git_command -C "$1" status --porcelain=v1 --untracked-files=all --ignored=matching) || fail "cannot inspect cached checkout state" [ -z "$status" ] || fail "cached checkout has local or ignored changes; preserve and inspect $1" } check_checkout "$download/repo" existing=0 comparison=$download/repo if [ -e "$checkout" ] || [ -L "$checkout" ]; then check_checkout "$checkout" existing=1 comparison=$checkout fi # Inspect fresh files and compare cached bytes, including assume-unchanged files. "$python_bin" - "$download/repo" "$comparison" <<'PY' || fail "cached checkout differs from the downloaded commit; preserve and inspect $checkout" import hashlib import os from pathlib import Path import stat import sys def snapshot(root): info = root.lstat() if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o022: raise ValueError("unexpected checkout ownership or write permissions: " + str(root)) found = {} for base, directories, files in os.walk(root, followlinks=False): relative = Path(base).relative_to(root) if relative == Path("."): directories[:] = [name for name in directories if name != ".git"] for name in directories + files: path = Path(base) / name info = path.lstat() if info.st_uid != os.geteuid() or info.st_mode & 0o022: raise ValueError("unexpected ownership or write permissions: " + str(path)) if stat.S_ISDIR(info.st_mode): found[str(path.relative_to(root))] = ("directory",) elif stat.S_ISREG(info.st_mode): digest = hashlib.sha256() with path.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) found[str(path.relative_to(root))] = ("file", bool(info.st_mode & 0o111), digest.hexdigest()) else: raise ValueError("unexpected nonregular cache entry: " + str(path)) return found try: if snapshot(Path(sys.argv[1])) != snapshot(Path(sys.argv[2])): raise ValueError("checkout files differ") except (OSError, ValueError) as exc: print(str(exc), file=sys.stderr) raise SystemExit(2) PY if [ "$existing" -eq 0 ]; then mv "$download/repo" "$checkout" || fail "cannot retain downloaded checkout" fi printf '%s\n' "Using persistent checkout $checkout" >&2 cleanup download= trap - 0 INT TERM if [ "$action" = "--patch" ] && [ "$plan" -eq 0 ]; then exec "$python_bin" -B "$checkout/cve-patch" "$@"