Skip to content
Closed
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
23 changes: 15 additions & 8 deletions docs/homebrew-publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -1578,8 +1578,9 @@ only per `(tap, formula)`, so unrelated Formulae retain parallel throughput:
repository's scoped `github.token` to an isolated ORAS transport. This
includes bounded tar
structure, link safety, receipt identity, local-build-root absence across all
regular members, and every Wasm member's ABI, memory width, object kind, and
fork instrumentation. The credentialed step cannot evaluate
regular members, and every Wasm member's role-appropriate ABI/import
contract, memory width, object kind, and fork instrumentation. The
credentialed step cannot evaluate
Formula Ruby or construct OCI metadata. GHCR returns the same anonymous
authorization failure for a missing package namespace and an existing private
reference. At that boundary, write mode uses the isolated credentials to fetch
Expand Down Expand Up @@ -1704,12 +1705,18 @@ only per `(tap, formula)`, so unrelated Formulae retain parallel throughput:
archive inspector independently derives the keg file inventory, executable
links, target receipt dependencies, archived Formula digest, and
fork-instrumentation state from the selected bottle bytes. Every regular
member beginning with Wasm magic is treated as a Kandelo process module,
independent of filename, mode, or wrapper layout. It must carry the exact
release ABI, one memory matching the bottle architecture, no relocatable
object marker, and complete fork exports when needed. A future bottle that
ships plugin or browser Wasm as data needs an explicit typed payload contract;
modes and paths are not trusted exemptions. The static
member beginning with Wasm magic is decoded independent of its filename or
mode. A module with exactly one leading `dylink.0` section is a dynamic-link
side module: it must import the one architecture-matching process memory,
use only the namespaces supplied by Kandelo's dynamic linker, avoid direct
process-only kernel imports, and carry complete side-module fork
instrumentation when needed. Every other Wasm member is a process
executable and must carry the exact release ABI in addition to the common
memory, object-kind, and fork checks. Homebrew `bin/` and `sbin/` links
declare process entrypoints, so they may not resolve to a side module.
Neither a `.so` suffix nor a non-executable mode exempts an ordinary Wasm
member from process validation. A future bottle that ships plugin or browser
Wasm as inert data still needs an explicit typed payload contract. The static
Formula declaration parser then cross-checks dependency categories and
directness against the validated build provenance and receipt. The verifier
generates the selected package's candidate sidecars, then validates the
Expand Down
23 changes: 16 additions & 7 deletions scripts/homebrew-inspect-bottle.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ def _set_validator_limits() -> None:
(MAX_WASM_VALIDATOR_OUTPUT_BYTES, MAX_WASM_VALIDATOR_OUTPUT_BYTES),
)

def _validate_wasm(self, wasm_path: str, label: str) -> str:
def _validate_wasm(self, wasm_path: str, label: str, declared_role: str) -> str:
with (
tempfile.NamedTemporaryFile() as stdout_file,
tempfile.NamedTemporaryFile() as stderr_file,
Expand All @@ -512,6 +512,7 @@ def _validate_wasm(self, wasm_path: str, label: str) -> str:
wasm_path,
str(self.expected_abi),
self.expected_arch,
declared_role,
],
stdin=subprocess.DEVNULL,
stdout=stdout_file,
Expand Down Expand Up @@ -552,13 +553,13 @@ def _validate_wasm(self, wasm_path: str, label: str) -> str:
fail(f"Wasm inspection output is not UTF-8 for {label!r}")
stderr = stderr_file.read(2048).decode("utf-8", errors="replace")
if return_code != 0:
fail(f"cannot inspect bottle executable {label!r}: {stderr}")
fail(f"cannot inspect bottle Wasm artifact {label!r}: {stderr}")
result = stdout.strip()
if result not in {"required", "not-required"}:
fail(f"Wasm inspection returned an invalid result for {label!r}")
return result

def _inspect_wasm(self, entry: ArchiveEntry) -> str:
def _inspect_wasm(self, entry: ArchiveEntry, declared_role: str) -> str:
if entry.size > MAX_WASM_BYTES:
fail(f"bottle Wasm module {entry.path!r} exceeds {MAX_WASM_BYTES} bytes")
with self._extract_regular(entry) as source, tempfile.NamedTemporaryFile(
Expand All @@ -574,7 +575,7 @@ def _inspect_wasm(self, entry: ArchiveEntry) -> str:
break
wasm_file.write(chunk)
wasm_file.flush()
return self._validate_wasm(wasm_file.name, entry.path)
return self._validate_wasm(wasm_file.name, entry.path, declared_role)

def _result(self) -> dict[str, object]:
formula_rel = f".brew/{self.formula}.rb"
Expand All @@ -593,15 +594,23 @@ def _result(self) -> dict[str, object]:
if path.startswith(f"{self.payload_root}/")
and entry.kind in {"regular", "symlink", "hardlink"}
)
path_exec_files, _resolved_execs = self._path_executables(all_files)
path_exec_files, resolved_execs = self._path_executables(all_files)
wasm_entries = {
entry.path: entry
for entry in self.entries.values()
if entry.path.startswith(f"{self.payload_root}/") and entry.is_wasm
}
fork_instrumentation = "not-required"
for entry in sorted(wasm_entries.values(), key=lambda value: value.path):
result = self._inspect_wasm(entry)
# WHY: Homebrew exposes executable entrypoints through bin/sbin
# links, while Wasm side modules advertise their distinct loading
# contract structurally through a leading dylink.0 section. Pass
# the PATH role into the validator so a mislabeled side module can
# never evade the process ABI contract.
declared_role = (
"path-executable" if entry.path in resolved_execs else "payload"
)
result = self._inspect_wasm(entry, declared_role)
if result == "required":
fork_instrumentation = "required"

Expand Down Expand Up @@ -640,7 +649,7 @@ def parse_args() -> argparse.Namespace:
)
parser.add_argument(
"--wasm-validator",
default=str(pathlib.Path(__file__).with_name("homebrew-validate-wasm-executable.sh")),
default=str(pathlib.Path(__file__).with_name("homebrew-validate-wasm-artifact.sh")),
)
parser.add_argument(
"--wasm-timeout-seconds",
Expand Down
159 changes: 159 additions & 0 deletions scripts/homebrew-validate-wasm-artifact.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env bash
set -euo pipefail

if [ "$#" -ne 4 ]; then
echo "usage: homebrew-validate-wasm-artifact.sh <wasm> <expected-abi> <wasm32|wasm64> <payload|executable|path-executable>" >&2
exit 2
fi

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
# shellcheck source=wasm-artifact-guards.sh
source "$SCRIPT_DIR/wasm-artifact-guards.sh"

wasm_path="$1"
expected_abi="$2"
expected_arch="$3"
declared_role="$4"
if ! [[ "$expected_abi" =~ ^[1-9][0-9]*$ ]] || [ "$expected_abi" -gt 4294967295 ]; then
echo "homebrew-validate-wasm-artifact.sh: invalid expected ABI: $expected_abi" >&2
exit 2
fi
case "$expected_arch" in
wasm32|wasm64) ;;
*)
echo "homebrew-validate-wasm-artifact.sh: invalid expected architecture: $expected_arch" >&2
exit 2
;;
esac
case "$declared_role" in
payload|executable|path-executable) ;;
*)
echo "homebrew-validate-wasm-artifact.sh: invalid declared artifact role: $declared_role" >&2
exit 2
;;
esac
if ! wasm_is_binary "$wasm_path"; then
echo "homebrew-validate-wasm-artifact.sh: input is not a Wasm binary: $wasm_path" >&2
exit 1
fi

wasm_require_no_legacy_asyncify "$wasm_path"

relocatable_status=0
wasm_is_relocatable_object "$wasm_path" || relocatable_status=$?
case "$relocatable_status" in
0)
echo "homebrew-validate-wasm-artifact.sh: artifact is a relocatable Wasm object: $wasm_path" >&2
exit 1
;;
1) ;;
*)
echo "homebrew-validate-wasm-artifact.sh: cannot inspect Wasm object kind: $wasm_path" >&2
exit 1
;;
esac

artifact_role=""
role_status=0
artifact_role="$(wasm_artifact_role "$wasm_path")" || role_status=$?
if [ "$role_status" -ne 0 ]; then
echo "homebrew-validate-wasm-artifact.sh: malformed or misplaced dylink.0 artifact role: $wasm_path" >&2
exit 1
fi

# WHY: a mode bit or `.so` suffix is not enough to decide how Wasm is loaded.
# PATH files are declared launch entrypoints and must remain process modules;
# other payload Wasm may opt into the runtime's structural `dylink.0` contract.
# This prevents a side module from bypassing executable ABI checks merely by
# moving under `bin/`, while allowing real shared modules to omit executable-
# only exports that the dynamic linker never consumes.
if [ "$declared_role" != "payload" ] && [ "$artifact_role" != "executable" ]; then
echo "homebrew-validate-wasm-artifact.sh: declared executable is a dylink.0 side module: $wasm_path" >&2
exit 1
fi
if [ "$declared_role" = "executable" ]; then
artifact_role="executable"
fi

artifact_arch=""
arch_status=0
if [ "$artifact_role" = "side-module" ]; then
artifact_arch="$(wasm_validate_side_module_imports "$wasm_path")" || arch_status=$?
if [ "$arch_status" -ne 0 ]; then
echo "homebrew-validate-wasm-artifact.sh: side module has an unsupported memory or import contract: $wasm_path" >&2
exit 1
fi
else
artifact_arch="$(wasm_memory_arch "$wasm_path")" || arch_status=$?
if [ "$arch_status" -ne 0 ]; then
echo "homebrew-validate-wasm-artifact.sh: executable must define or import exactly one inspectable memory: $wasm_path" >&2
exit 1
fi
fi
if [ "$artifact_arch" != "$expected_arch" ]; then
echo "homebrew-validate-wasm-artifact.sh: $artifact_role architecture $artifact_arch does not match expected architecture $expected_arch: $wasm_path" >&2
exit 1
fi

if [ "$artifact_role" = "executable" ]; then
if wasm_imports_side_module_fork "$wasm_path"; then
echo "homebrew-validate-wasm-artifact.sh: executable imports side-module-only env.fork: $wasm_path" >&2
exit 1
fi

artifact_abi=""
abi_status=0
artifact_abi="$(wasm_extract_abi_version "$wasm_path")" || abi_status=$?
case "$abi_status" in
0) ;;
1)
echo "homebrew-validate-wasm-artifact.sh: executable lacks __abi_version: $wasm_path" >&2
exit 1
;;
*)
echo "homebrew-validate-wasm-artifact.sh: cannot validate __abi_version: $wasm_path" >&2
exit 1
;;
esac
if [ "$artifact_abi" != "$expected_abi" ]; then
echo "homebrew-validate-wasm-artifact.sh: executable ABI $artifact_abi does not match expected ABI $expected_abi: $wasm_path" >&2
exit 1
fi
elif wasm_imports_kernel_fork "$wasm_path"; then
echo "homebrew-validate-wasm-artifact.sh: side module imports executable-only kernel.kernel_fork: $wasm_path" >&2
exit 1
fi

wasm_require_fork_instrumentation_if_needed "$wasm_path"

fork_required=0
predicate_status=0
if [ "$artifact_role" = "side-module" ]; then
wasm_imports_side_module_fork "$wasm_path" || predicate_status=$?
else
wasm_imports_kernel_fork "$wasm_path" || predicate_status=$?
fi
case "$predicate_status" in
0) fork_required=1 ;;
1) ;;
*)
echo "homebrew-validate-wasm-artifact.sh: cannot inspect $artifact_role fork import: $wasm_path" >&2
exit 1
;;
esac
predicate_status=0
wasm_has_any_wpk_fork_export "$wasm_path" || predicate_status=$?
case "$predicate_status" in
0) fork_required=1 ;;
1) ;;
*)
echo "homebrew-validate-wasm-artifact.sh: cannot inspect fork exports: $wasm_path" >&2
exit 1
;;
esac

if [ "$fork_required" -eq 1 ]; then
printf 'required\n'
else
printf 'not-required\n'
fi
99 changes: 1 addition & 98 deletions scripts/homebrew-validate-wasm-executable.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,101 +7,4 @@ if [ "$#" -ne 3 ]; then
fi

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
# shellcheck source=wasm-artifact-guards.sh
source "$SCRIPT_DIR/wasm-artifact-guards.sh"

wasm_path="$1"
expected_abi="$2"
expected_arch="$3"
if ! [[ "$expected_abi" =~ ^[1-9][0-9]*$ ]] || [ "$expected_abi" -gt 4294967295 ]; then
echo "homebrew-validate-wasm-executable.sh: invalid expected ABI: $expected_abi" >&2
exit 2
fi
case "$expected_arch" in
wasm32|wasm64) ;;
*)
echo "homebrew-validate-wasm-executable.sh: invalid expected architecture: $expected_arch" >&2
exit 2
;;
esac
if ! wasm_is_binary "$wasm_path"; then
echo "homebrew-validate-wasm-executable.sh: input is not a Wasm binary: $wasm_path" >&2
exit 1
fi

wasm_require_no_legacy_asyncify "$wasm_path"

relocatable_status=0
wasm_is_relocatable_object "$wasm_path" || relocatable_status=$?
case "$relocatable_status" in
0)
echo "homebrew-validate-wasm-executable.sh: executable is a relocatable Wasm object: $wasm_path" >&2
exit 1
;;
1) ;;
*)
echo "homebrew-validate-wasm-executable.sh: cannot inspect Wasm object kind: $wasm_path" >&2
exit 1
;;
esac

artifact_arch=""
arch_status=0
artifact_arch="$(wasm_memory_arch "$wasm_path")" || arch_status=$?
if [ "$arch_status" -ne 0 ]; then
echo "homebrew-validate-wasm-executable.sh: executable must define or import exactly one inspectable memory: $wasm_path" >&2
exit 1
fi
if [ "$artifact_arch" != "$expected_arch" ]; then
echo "homebrew-validate-wasm-executable.sh: executable architecture $artifact_arch does not match expected architecture $expected_arch: $wasm_path" >&2
exit 1
fi

artifact_abi=""
abi_status=0
artifact_abi="$(wasm_extract_abi_version "$wasm_path")" || abi_status=$?
case "$abi_status" in
0) ;;
1)
echo "homebrew-validate-wasm-executable.sh: executable lacks __abi_version: $wasm_path" >&2
exit 1
;;
*)
echo "homebrew-validate-wasm-executable.sh: cannot validate __abi_version: $wasm_path" >&2
exit 1
;;
esac
if [ "$artifact_abi" != "$expected_abi" ]; then
echo "homebrew-validate-wasm-executable.sh: executable ABI $artifact_abi does not match expected ABI $expected_abi: $wasm_path" >&2
exit 1
fi

wasm_require_fork_instrumentation_if_needed "$wasm_path"

fork_required=0
predicate_status=0
wasm_imports_kernel_fork "$wasm_path" || predicate_status=$?
case "$predicate_status" in
0) fork_required=1 ;;
1) ;;
*)
echo "homebrew-validate-wasm-executable.sh: cannot inspect kernel fork import: $wasm_path" >&2
exit 1
;;
esac
predicate_status=0
wasm_has_any_wpk_fork_export "$wasm_path" || predicate_status=$?
case "$predicate_status" in
0) fork_required=1 ;;
1) ;;
*)
echo "homebrew-validate-wasm-executable.sh: cannot inspect fork exports: $wasm_path" >&2
exit 1
;;
esac

if [ "$fork_required" -eq 1 ]; then
printf 'required\n'
else
printf 'not-required\n'
fi
exec bash "$SCRIPT_DIR/homebrew-validate-wasm-artifact.sh" "$@" executable
Loading
Loading