From c3ef08996bfb3750056d48f0d4a860ea272cef2e Mon Sep 17 00:00:00 2001 From: m4ndolore Date: Sun, 23 Aug 2026 13:38:42 -1000 Subject: [PATCH] ci: block the proprietary vendor library from ever being committed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit quill/ is a clean-room MIT implementation that links against reMarkable's libqsgepaper.so, but that library is theirs. Users extract it from a device they own (quill/build.sh fetches it over SSH); we never redistribute it. .gitignore covers quill/vendor/ and quill/build/, but that protection does not survive `git add -f`, a renamed blob, or a downstream repository that copies quill/ without the ignore rules. This adds the backstop: - scripts/ci/check-no-vendor-blob.sh — filename, path, and ELF content scan over tracked and staged files - scripts/ci/install-hooks.sh — pre-commit hook, so a leak is stopped before it enters history rather than after - .github/workflows/no-vendor-blob.yml — the same check on every push and PR, plus a full-history scan for the library by name and size - scripts/ci/README.md — what it catches and what to do when it fires Documentation under quill/vendor/ stays allowed; that directory legitimately holds a README explaining how to fetch the library. The library is matched by name and content, not by location. Verified: passes a clean tree and a vendor README; fails the blob committed by name, renamed elsewhere in the tree, and quill/build/ output; the hook blocks the commit; full history is clean. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/no-vendor-blob.yml | 55 +++++++++++++++++ scripts/ci/README.md | 66 +++++++++++++++++++++ scripts/ci/check-no-vendor-blob.sh | 89 ++++++++++++++++++++++++++++ scripts/ci/install-hooks.sh | 28 +++++++++ 4 files changed, 238 insertions(+) create mode 100644 .github/workflows/no-vendor-blob.yml create mode 100644 scripts/ci/README.md create mode 100755 scripts/ci/check-no-vendor-blob.sh create mode 100755 scripts/ci/install-hooks.sh diff --git a/.github/workflows/no-vendor-blob.yml b/.github/workflows/no-vendor-blob.yml new file mode 100644 index 0000000..dfc41e7 --- /dev/null +++ b/.github/workflows/no-vendor-blob.yml @@ -0,0 +1,55 @@ +name: no-vendor-blob + +# reMarkable's libqsgepaper.so must never be redistributed by this repository. +# quill/ links against it, but each user extracts it from their own device. +# This job fails the build if the library — or anything under the ignored +# vendor/build directories — ever becomes tracked. + +on: + push: + branches: ["**"] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + name: No proprietary vendor artifacts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # Full history: a blob added in an earlier commit on this branch is + # still a leak, even if a later commit removed it. + fetch-depth: 0 + + - name: Check working tree + run: ./scripts/ci/check-no-vendor-blob.sh + + - name: Check full history + run: | + # Any object ever named libqsgepaper.so, anywhere in reachable + # history. Documentation under quill/vendor/ is legitimate (it tells + # users how to fetch the library themselves), so match the library + # filename rather than the directory, and confirm by size — the real + # library is ~350KB, a README is under a kilobyte. + fail=0 + while read -r sha path; do + [ -n "${path:-}" ] || continue + size=$(git cat-file -s "$sha" 2>/dev/null || echo 0) + if [ "$size" -ge 102400 ]; then + echo " $path ($size bytes, object $sha)" >&2 + fail=1 + fi + done < <(git rev-list --objects --all \ + | grep -E '(^|/)libqsgepaper\.so(\.[0-9.]+)?$' || true) + + if [ "$fail" -ne 0 ]; then + echo >&2 + echo "ERROR: reMarkable's proprietary library is present in git history." >&2 + echo "History must be rewritten to remove it before this branch can ship." >&2 + exit 1 + fi + echo "OK: history is clean." diff --git a/scripts/ci/README.md b/scripts/ci/README.md new file mode 100644 index 0000000..fbbffb9 --- /dev/null +++ b/scripts/ci/README.md @@ -0,0 +1,66 @@ +# CI guards + +## `check-no-vendor-blob.sh` + +Refuses to let reMarkable's proprietary `libqsgepaper.so` into the repository. + +`quill/` is a clean-room MIT implementation that *links against* that library, +but the library itself is reMarkable's property. Every user extracts it from a +device they own — `quill/build.sh` pulls it over SSH — and we never +redistribute it. See `quill/README.md` and `quill/CLEANROOM.md` for the +clean-room boundary this protects. + +`.gitignore` already covers `quill/vendor/` and `quill/build/`. This guard is +the backstop for the cases `.gitignore` does not cover: + +| Case | Caught by | +|---|---| +| `git add -f quill/vendor/libqsgepaper.so` | filename match | +| Blob renamed and hidden elsewhere in the tree | ELF content scan | +| `quill/build/` output committed | path match | +| Blob already in a previous commit | CI history scan | + +It deliberately **allows** documentation under `quill/vendor/` — that directory +legitimately holds a README telling users how to fetch the library themselves. +The library is matched by filename and by content, not by living in that +directory. + +### Run it + +```sh +./scripts/ci/check-no-vendor-blob.sh +``` + +### Install the pre-commit hook + +```sh +./scripts/ci/install-hooks.sh +``` + +CI catches a leak after it is pushed; the hook catches it before it enters +history — the difference between `git restore --staged` and rewriting a branch. +Run once per clone. The hook installs into the shared `.git/hooks/`, so it +covers every worktree of this repository. + +### In CI + +`.github/workflows/no-vendor-blob.yml` runs the working-tree check on every +push and pull request, then scans full reachable history for any object named +`libqsgepaper.so` that is large enough to be the real library (~350KB). + +### If it fires + +```sh +git restore --staged # not yet committed +``` + +If the blob is already in history, it must be removed with a history rewrite +(`git filter-repo`) before the branch can ship publicly. Verified clean as of +the commit that added this guard. + +## Downstream repositories + +Any repository that vendors `quill/` — including a future g-pad — must carry +this guard and the matching `.gitignore` rules. The ignore rules do not travel +with copied source; this check is what makes the boundary enforceable rather +than a convention. diff --git a/scripts/ci/check-no-vendor-blob.sh b/scripts/ci/check-no-vendor-blob.sh new file mode 100755 index 0000000..f93fd54 --- /dev/null +++ b/scripts/ci/check-no-vendor-blob.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Refuse to let reMarkable's proprietary display library into the repository. +# +# quill/ is a clean-room MIT implementation that LINKS AGAINST libqsgepaper.so, +# but that library is reMarkable's property. Every user extracts it from a +# device they own (quill/build.sh pulls it over SSH); we never redistribute it. +# +# .gitignore already covers quill/vendor/ and quill/build/, but a stray +# `git add -f`, a moved build directory, or a downstream repository that forgets +# the ignore rule would leak it. This check is the backstop that makes the leak +# hard to commit and impossible to push unnoticed. +# +# Run manually: ./scripts/ci/check-no-vendor-blob.sh +# Install hook: ./scripts/ci/install-hooks.sh +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +# Filenames that are always the vendor library, wherever they appear. +FORBIDDEN_NAME='libqsgepaper\.so' +# Build output that must never be tracked. quill/vendor/ is deliberately NOT +# listed: it legitimately holds documentation telling users how to fetch the +# library from their own device. The library itself is caught by name above +# and by the ELF content scan below. +FORBIDDEN_PATH='^quill/build/' +# Smallest plausible size for the real library (~350KB); skip smaller files +# when doing the expensive content scan. +MIN_BLOB_BYTES=102400 + +fail=0 + +report() { + if [ "$fail" -eq 0 ]; then + echo "ERROR: proprietary vendor artifacts must never be committed." >&2 + echo >&2 + fi + fail=1 + echo " $1" >&2 +} + +matches_forbidden() { + grep -E "${FORBIDDEN_PATH}|(^|/)${FORBIDDEN_NAME}(\.[0-9.]+)?$" || true +} + +# 1. Nothing currently tracked may match. +while IFS= read -r path; do + [ -n "$path" ] && report "tracked: $path" +done < <(git ls-files | matches_forbidden) + +# 2. Nothing staged may match. This is the pre-commit path: it catches the +# blob before it ever enters history, where removing it means a rewrite. +if git rev-parse --verify --quiet HEAD >/dev/null 2>&1; then + while IFS= read -r path; do + [ -n "$path" ] && report "staged: $path" + done < <(git diff --cached --name-only --diff-filter=ACMR | matches_forbidden) +fi + +# 3. Content check: a renamed blob still carries the vendor's ELF symbols. +# Catches `cp libqsgepaper.so quill/src/display.bin` and similar. +while IFS= read -r path; do + [ -n "$path" ] || continue + [ -f "$path" ] || continue + size=$(wc -c < "$path" 2>/dev/null || echo 0) + [ "$size" -lt "$MIN_BLOB_BYTES" ] && continue + # ELF magic: 0x7F 'E' 'L' 'F' + [ "$(head -c 4 "$path" | od -An -tx1 | tr -d ' \n')" = "7f454c46" ] || continue + # NOTE: `grep -q` exits on first match, which SIGPIPEs `strings`; under + # `set -o pipefail` that turns a successful match into a failed pipeline + # and the check silently passes. Count matches instead so the whole + # stream is consumed and the exit status reflects the search, not the pipe. + hits=$(strings -a "$path" 2>/dev/null | grep -cE 'qsgepaper|EPFramebuffer' || true) + if [ "${hits:-0}" -gt 0 ]; then + report "vendor ELF content in tracked file: $path" + fi +done < <(git ls-files) + +if [ "$fail" -ne 0 ]; then + cat >&2 <<'MSG' + +quill/vendor/ and quill/build/ are gitignored on purpose. +Users obtain libqsgepaper.so from their own device via quill/build.sh. +See quill/README.md and quill/CLEANROOM.md for the clean-room boundary. + +If a file is already staged, unstage it: git restore --staged +MSG + exit 1 +fi + +echo "OK: no proprietary vendor artifacts tracked or staged." diff --git a/scripts/ci/install-hooks.sh b/scripts/ci/install-hooks.sh new file mode 100755 index 0000000..c372a4c --- /dev/null +++ b/scripts/ci/install-hooks.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Install the local pre-commit guard against committing the vendor library. +# +# CI catches a leak after the fact; the hook catches it before it enters +# history, which is the difference between "unstage the file" and "rewrite the +# branch". Run once per clone. +set -euo pipefail + +root="$(git rev-parse --show-toplevel)" +hook="$(git rev-parse --git-path hooks/pre-commit)" + +if [ -e "$hook" ] && ! grep -q 'check-no-vendor-blob' "$hook" 2>/dev/null; then + echo "A pre-commit hook already exists and does not call the guard:" >&2 + echo " $hook" >&2 + echo "Add this line to it manually:" >&2 + echo ' "$(git rev-parse --show-toplevel)"/scripts/ci/check-no-vendor-blob.sh' >&2 + exit 1 +fi + +mkdir -p "$(dirname "$hook")" +cat > "$hook" <<'HOOK' +#!/usr/bin/env sh +# Block commits that would add reMarkable's proprietary libqsgepaper.so. +"$(git rev-parse --show-toplevel)"/scripts/ci/check-no-vendor-blob.sh +HOOK +chmod +x "$hook" + +echo "Installed pre-commit guard at ${hook#"$root"/}"