Skip to content

[Homebrew/Publisher] Publish bottles containing Wasm side modules safely - #1119

Closed
brandonpayton wants to merge 1 commit into
packaging/schema3-stacked-a0-qk044from
homebrew/wasm-side-module-publisher-qk044
Closed

[Homebrew/Publisher] Publish bottles containing Wasm side modules safely#1119
brandonpayton wants to merge 1 commit into
packaging/schema3-stacked-a0-qk044from
homebrew/wasm-side-module-publisher-qk044

Conversation

@brandonpayton

@brandonpayton brandonpayton commented Jul 27, 2026

Copy link
Copy Markdown
Member

Why

A Homebrew bottle can contain two different kinds of WebAssembly (Wasm)
modules:

  • a process executable, such as php, which the kernel launches; and
  • a dynamic-link side module, such as PHP's curl.so, which an already
    running process loads through dlopen.

The bottle inspector previously treated every Wasm member as a process
executable. That was safe for bottles containing only programs, but it rejected
valid side modules because they intentionally do not export the executable-only
__abi_version function.

This change makes the distinction from the artifact's real structure. It does
not exempt .so filenames or weaken executable checks. A file exposed through
Homebrew's bin/ or sbin/ paths must still be a process executable.

What changed

  • Add one shared Homebrew Wasm artifact validator.
  • Recognize a side module only when it has exactly one leading dylink.0
    section, which is the same structural contract used by Kandelo's dynamic
    linker.
  • Keep process executables bound to the exact expected Kandelo ABI through
    __abi_version.
  • Validate that side modules:
    • share exactly one memory of the expected architecture;
    • import only from namespaces supplied by Kandelo's dynamic linker;
    • do not import the process-only kernel.kernel_fork entrypoint; and
    • carry complete side-module continuation instrumentation if they import
      env.fork.
  • Resolve Homebrew bin/ and sbin/ links before Wasm validation and reject a
    side module exposed as a launchable entrypoint.
  • Keep the existing executable-only validator as a compatibility wrapper over
    the shared validator.
  • Document the two artifact roles and their separate ABI and fork contracts.

PHP evidence

The PHP bottle that exposed this gap now classifies according to its actual
runtime behavior:

Artifact Structural role Fork instrumentation
bin/php process executable required
curl.so, intl.so, phar.so, zend_test.so, zip.so side module not required
opcache.so side module required

opcache.so remains a side module and does not need the executable-only
__abi_version export. Its fork status is required for a separate reason: it
imports the side-module env.fork entrypoint and carries the complete
wpk_fork_* continuation surface.

Validation

Run in the repository-declared development shell:

  • bash scripts/test-homebrew-inspect-bottle.sh
  • bash scripts/test-wasm-artifact-guards.sh
  • ruby scripts/check-homebrew-publish-workflow-trust.rb
  • bash scripts/test-homebrew-publish-workflow.sh

On the current stacked head, the three focused checks passed. The complete
publisher workflow passed through the real Homebrew lifecycle, OCI layout,
launcher isolation, Formula closure, bottle inspection, runtime evidence,
immutable-release recovery, and staging-release tests. Its first fresh-clone
run then stopped because root node_modules had not been installed. After the
declared npm ci --ignore-scripts preparation, the remaining VFS-release and
main-shell closure suites passed, including all 21 image-contract cases. The
pull-request workflow will repeat the complete prepared run.

The focused inspector fixtures cover:

  • a valid side module without an executable ABI export;
  • a side module incorrectly exposed through bin/;
  • a misplaced dylink.0 section;
  • an unsupported side-module import namespace;
  • an uninstrumented env.fork side module;
  • a .so-named ordinary Wasm executable missing __abi_version; and
  • the existing stale ABI, malformed ABI, memory-width, relocatable-object, and
    fork-instrumentation failures.

Additional checks:

  • ShellCheck for the touched shell scripts;
  • Python syntax parsing for the bottle inspector; and
  • git diff --check.

Stacking and scope

This draft is based on the exact current head of
Automattic/kandelo#1112, which is itself based on the exact current
Automattic/kandelo#1116 shell-cutover head. That gives the migration one
ordered chain: mostly-lazy shell, sealed schema-3 recipe publisher, then Wasm
side-module publication. This PR's only commit is the artifact-role classifier.
It does not change the shell image, package inventory, release locks,
publication pins, kernel, or ABI. As the two prerequisite PRs land, this PR
will be retargeted down the chain before merge.

Homebrew bottles may contain both process executables and dylink.0 side modules. Validate their distinct loading contracts without treating a filename or mode as an exemption from executable ABI checks.

Resolve bin/sbin entrypoints before inspection, reject side modules exposed as process entrypoints, validate side-module memory/import/fork structure, and keep executable ABI-version validation unchanged. Cover valid and malformed roles, unsupported imports, and uninstrumented side-module fork imports.
@brandonpayton
brandonpayton force-pushed the homebrew/wasm-side-module-publisher-qk044 branch from b03688e to 7253144 Compare July 27, 2026 22:05
@brandonpayton
brandonpayton changed the base branch from homebrew/mostly-lazy-shell-cutover-qk044 to packaging/schema3-stacked-a0-qk044 July 27, 2026 22:05
@brandonpayton

Copy link
Copy Markdown
Member Author

The current staging failure is a stacked-branch artifact-state mismatch, not a classifier failure. This PR intentionally changes only the bottle classifier relative to its #1112 base, so change-scope correctly sets package staging to false. However, the current main durable index predates the six image cache keys inherited through #1116, so aggregate materialization rejects shell, LAMP, Nginx, Node, and WordPress before any split suite runs. #1116 staging built all six successfully. After #1116 lands and its exact artifacts are published, retargeting/re-running this stack against current main will give the classifier a current canonical baseline. No classifier code or package scope is being weakened to hide the mismatch.

@brandonpayton

Copy link
Copy Markdown
Member Author

Superseded by #1121. Its exact patch is preserved there as commit 8915cb7 (stable patch ID 14979496a25c634a759db1b879a0ba4664304fed), so keeping this draft open would duplicate review and CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant