From d3805721b887a19382ef1c96b576fc27badc0951 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Fri, 24 Jul 2026 08:38:17 -0400 Subject: [PATCH] [Homebrew/Security] Recognize canonical native Requirement references Treat exact static Requirement declarations as references into the already-bound Formula support tree instead of rejecting every additional support-module token. Keep authority over Requirement classes, tags, AST placement, and support definitions in the Ripper parser, and cover the exact Asa shape plus dynamic-reference rejection in the publication contract. --- docs/homebrew-publishing.md | 6 ++ scripts/homebrew-oci-layout.py | 23 +++++++- ...omebrew-validate-formula-source-closure.sh | 7 ++- scripts/test-homebrew-oci-layout.sh | 16 ++++++ scripts/test-homebrew-publish-workflow.sh | 55 +++++++++++++++++++ 5 files changed, 102 insertions(+), 5 deletions(-) diff --git a/docs/homebrew-publishing.md b/docs/homebrew-publishing.md index 575e1f122f..a40d248ac8 100644 --- a/docs/homebrew-publishing.md +++ b/docs/homebrew-publishing.md @@ -443,6 +443,12 @@ those classes only through the canonical support require and a literal changed metadata or predicates, and `:test`-only native Requirements fail closed. +The bottle source-closure layer recognizes those literal Requirement lines as +references to the already-bound Formula support tree; it does not treat every +additional `KandeloFormulaSupport` token as a second source loader. Its line +allowlist rejects other module references, while the Ripper-based Formula +parser remains authoritative for the closed class and tag allowlists. + The static Formula parser recognizes that exact source shape without evaluating Formula Ruby. Schema 4 of the protected host-dependency plan binds the Requirement class, native Formula identity, sentinel executable, and diff --git a/scripts/homebrew-oci-layout.py b/scripts/homebrew-oci-layout.py index 06e8bfd60f..d99ed0edcd 100755 --- a/scripts/homebrew-oci-layout.py +++ b/scripts/homebrew-oci-layout.py @@ -35,6 +35,13 @@ COMMIT = re.compile(r"^[0-9a-f]{40}$") TAP_REPOSITORY = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") CANONICAL_UINT = re.compile(r"^(0|[1-9][0-9]*)$") +# WHY: source closure needs to recognize references into the already-bound +# support tree without deciding which Requirements are trusted. The Ripper +# parser remains authoritative for AST placement, class names, and tags. +NATIVE_REQUIREMENT_REFERENCE = re.compile( + r"^ depends_on KandeloFormulaSupport::[A-Z][A-Za-z0-9]*Requirement" + r" => (:[a-z]+|\[(?::[a-z]+)(?:, :[a-z]+)*\])$" +) OCI_REMOTE = re.compile( r"^ghcr\.io/[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*$" ) @@ -442,15 +449,25 @@ def source_closure( marker = "Kandelo/formula_support/kandelo_formula_support" entries: list[dict[str, Any]] = [] if marker in formula_source: - if formula_source.splitlines().count(require_line) != 1: + formula_lines = formula_source.splitlines() + if formula_lines.count(require_line) != 1: fail("Formula support require is not canonical") + support_reference_lines = [ + line for line in formula_lines if "KandeloFormulaSupport" in line + ] + allowed_support_references = [ + line + for line in support_reference_lines + if line == " include KandeloFormulaSupport" + or NATIVE_REQUIREMENT_REFERENCE.fullmatch(line) is not None + ] if ( formula_source.count("Tap.fetch") != 1 - or formula_source.count("KandeloFormulaSupport") != 1 or "require_relative" in formula_source + or allowed_support_references != support_reference_lines ): fail("Formula support reference is not a bounded canonical closure") - if formula_source.splitlines().count(" include KandeloFormulaSupport") != 1: + if formula_lines.count(" include KandeloFormulaSupport") != 1: fail("Formula support include is not canonical") support_root = real_directory( tap_root / "Kandelo/formula_support", "Formula support source root" diff --git a/scripts/homebrew-validate-formula-source-closure.sh b/scripts/homebrew-validate-formula-source-closure.sh index 30384b1e43..7979df8e5f 100755 --- a/scripts/homebrew-validate-formula-source-closure.sh +++ b/scripts/homebrew-validate-formula-source-closure.sh @@ -150,10 +150,13 @@ if grep -Fq "$support_marker" "$BASE_FORMULA"; then exit 1 fi tap_fetch_count="$({ grep -Fo 'Tap.fetch' "$BASE_FORMULA" || true; } | wc -l | tr -d '[:space:]')" - support_constant_count="$({ grep -Fo 'KandeloFormulaSupport' "$BASE_FORMULA" || true; } | wc -l | tr -d '[:space:]')" + # WHY: canonical publisher-only Requirement declarations also name + # KandeloFormulaSupport. The source-closure generator below owns the exact + # line allowlist, and the Ripper-based runtime-closure parser owns the + # Requirement class and tag allowlist. Counting the module token here would + # reject those reviewed static declarations without adding source authority. if [ "$tap_fetch_count" != "1" ] || \ [ "$(grep -Fxc ' include KandeloFormulaSupport' "$BASE_FORMULA" || true)" != "1" ] || \ - [ "$support_constant_count" != "1" ] || \ grep -Fq 'require_relative' "$BASE_FORMULA"; then echo "homebrew-validate-formula-source-closure.sh: Formula has an unsupported local source reference" >&2 exit 1 diff --git a/scripts/test-homebrew-oci-layout.sh b/scripts/test-homebrew-oci-layout.sh index abc9af556d..c364f5329e 100755 --- a/scripts/test-homebrew-oci-layout.sh +++ b/scripts/test-homebrew-oci-layout.sh @@ -38,6 +38,8 @@ make_fixture() { require (Tap.fetch("$(printf '%s' "$tap_owner" | tr '[:upper:]' '[:lower:]')", "$(printf '%s' "$tap_short_name" | tr '[:upper:]' '[:lower:]')").path/"Kandelo/formula_support/kandelo_formula_support").to_s class Hello < Formula + depends_on KandeloFormulaSupport::BinaryenRequirement => :build + depends_on KandeloFormulaSupport::WabtRequirement => [:build, :test] include KandeloFormulaSupport desc "OCI fixture" end @@ -329,6 +331,20 @@ source_closure_args=( ) python3 "$TOOL" "${source_closure_args[@]}" \ --out "$TMP_ROOT/source-closure-baseline.json" +cp "$source_closure_root/Formula/hello.rb" \ + "$TMP_ROOT/source-closure-canonical-requirement.rb" +sed -i.bak \ + 's/KandeloFormulaSupport::WabtRequirement/KandeloFormulaSupport.const_get("WabtRequirement")/' \ + "$source_closure_root/Formula/hello.rb" +rm "$source_closure_root/Formula/hello.rb.bak" +if python3 "$TOOL" "${source_closure_args[@]}" \ + --out "$TMP_ROOT/source-closure-with-dynamic-requirement.json" \ + >/dev/null 2>&1; then + echo "Formula support source closure accepted a dynamic Requirement reference" >&2 + exit 1 +fi +mv "$TMP_ROOT/source-closure-canonical-requirement.rb" \ + "$source_closure_root/Formula/hello.rb" mkdir -p "$source_closure_root/Kandelo/formula_support/test" printf 'test-only-v1\n' \ >"$source_closure_root/Kandelo/formula_support/test/support_test.rb" diff --git a/scripts/test-homebrew-publish-workflow.sh b/scripts/test-homebrew-publish-workflow.sh index 940710d5cf..ce2158577f 100755 --- a/scripts/test-homebrew-publish-workflow.sh +++ b/scripts/test-homebrew-publish-workflow.sh @@ -6603,9 +6603,40 @@ class Escape < Formula require_relative "../Kandelo/other" end end +EOF + cat >"$tap/Formula/native-support.rb" <<'EOF' +require (Tap.fetch("kandelo-dev", "tap-core").path/"Kandelo/formula_support/kandelo_formula_support").to_s + +class NativeSupport < Formula + depends_on KandeloFormulaSupport::BinaryenRequirement => :build + depends_on KandeloFormulaSupport::WabtRequirement => [:build, :test] + include KandeloFormulaSupport +end +EOF + cat >"$tap/Formula/dynamic-support.rb" <<'EOF' +require (Tap.fetch("kandelo-dev", "tap-core").path/"Kandelo/formula_support/kandelo_formula_support").to_s + +class DynamicSupport < Formula + depends_on KandeloFormulaSupport.const_get("WabtRequirement") => [:build, :test] + include KandeloFormulaSupport +end EOF write_canonical_formula_support \ "$tap/Kandelo/formula_support/kandelo_formula_support.rb" <<'EOF' + class BinaryenRequirement < Requirement + KANDELO_NATIVE_FORMULA = "binaryen" + KANDELO_NATIVE_SENTINEL = "wasm-opt" + fatal true + satisfy(build_env: false) { which("wasm-opt") } + end + + class WabtRequirement < Requirement + KANDELO_NATIVE_FORMULA = "wabt" + KANDELO_NATIVE_SENTINEL = "wasm-validate" + fatal true + satisfy(build_env: false) { which("wasm-validate") } + end + def kandelo_runner_command runner = Pathname(__dir__)/"run-network-wasm.ts" command = +"" @@ -6659,6 +6690,30 @@ EOF --base-ref "$base" \ --reviewed-tap-root "$reviewed" >/dev/null + # Canonical native Requirement declarations name the support module without + # loading another local source. Both closure validators must accept that + # static reference while the Formula parser retains semantic authority. + bash "$REPO_ROOT/scripts/homebrew-validate-formula-source-closure.sh" \ + --tap-root "$tap" \ + --tap-repository kandelo-dev/homebrew-tap-core \ + --formula native-support \ + --base-ref "$base" >/dev/null + bash "$REPO_ROOT/scripts/homebrew-validate-formula-source-closure.sh" \ + --tap-root "$tap" \ + --tap-repository kandelo-dev/homebrew-tap-core \ + --formula native-support \ + --base-ref "$base" \ + --reviewed-tap-root "$reviewed" >/dev/null + if bash "$REPO_ROOT/scripts/homebrew-validate-formula-source-closure.sh" \ + --tap-root "$tap" \ + --tap-repository kandelo-dev/homebrew-tap-core \ + --formula dynamic-support \ + --base-ref "$base" >/dev/null 2>"$err"; then + fail "Formula source-closure validator accepted a dynamic Requirement reference" + fi + grep -F "Formula support reference is not a bounded canonical closure" "$err" >/dev/null || + fail "Formula source-closure validator did not explain the dynamic Requirement reference" + printf 'test-only-v2\n' \ >"$tap/Kandelo/formula_support/test/kandelo_formula_support_test.rb" git -C "$tap" add Kandelo/formula_support/test/kandelo_formula_support_test.rb