diff --git a/.github/actions/detect-change-scope/ci-scope-paths.sh b/.github/actions/detect-change-scope/ci-scope-paths.sh index 1d28af8075..a22d0a5a70 100644 --- a/.github/actions/detect-change-scope/ci-scope-paths.sh +++ b/.github/actions/detect-change-scope/ci-scope-paths.sh @@ -48,7 +48,7 @@ package_publish_flow_changed_files() { grep -E \ -e '^\.github/actions/detect-change-scope/(ci-scope-paths|test-ci-scope-paths)\.sh$' \ -e '^\.github/workflows/(staging-build|prepare-merge|activate-merge-candidate|recover-rejected-merge-candidate|staging-cleanup|force-rebuild|reusable-package-source-publish)\.yml$' \ - -e '^\.github/scripts/(activate-merge-candidate|cleanup-merge-candidates|clone-rejected-merge-candidate|compose-staging-release-snapshots|download-verified-release-asset|fetch-canonical-index|github-api-get|init-merge-candidate|latest-merge-gate-status|mark-merge-candidate-ready|reconcile-merge-candidates|recover-canonical-indexes|require-exact-head-approval|select-package-archive-source|state-lock|test-activate-merge-candidate|test-cleanup-merge-candidates|test-clone-rejected-merge-candidate|test-download-verified-release-asset|test-fetch-canonical-index|test-init-merge-candidate|test-latest-merge-gate-status|test-merge-candidate-workflows|test-reconcile-merge-candidates|test-recover-canonical-indexes|test-require-exact-head-approval|test-select-package-archive-source|test-state-lock|test-validate-staging-release|test-verify-merge-candidate|validate-staging-release|verify-merge-candidate)\.sh$' \ + -e '^\.github/scripts/(activate-merge-candidate|cleanup-merge-candidates|clone-rejected-merge-candidate|compose-staging-release-snapshots|derive-rootfs-staging-scope|download-verified-release-asset|fetch-canonical-index|github-api-get|init-merge-candidate|latest-merge-gate-status|mark-merge-candidate-ready|reconcile-merge-candidates|recover-canonical-indexes|require-exact-head-approval|select-package-archive-source|state-lock|test-activate-merge-candidate|test-cleanup-merge-candidates|test-clone-rejected-merge-candidate|test-derive-rootfs-staging-scope|test-download-verified-release-asset|test-fetch-canonical-index|test-init-merge-candidate|test-latest-merge-gate-status|test-merge-candidate-workflows|test-reconcile-merge-candidates|test-recover-canonical-indexes|test-require-exact-head-approval|test-select-package-archive-source|test-state-lock|test-validate-staging-release|test-verify-merge-candidate|validate-staging-release|verify-merge-candidate)\.sh$' \ -e '^tools/xtask/src/(build_index|bundle_program|index_candidate|index_toml|index_update|package_archive_name|staging_reuse|update_pkg_manifest)\.rs$' \ -e '^scripts/(compose-initial-index|index-has-current-entry|index-update|prepare-sdk-package|publish-package-source|release-index-state|sync-package-source)\.sh$' \ -e '^tests/scripts/(index-update|package-publish-flow|release-index-state)\.sh$' \ @@ -79,7 +79,7 @@ kernel_runtime_changed_files() { ci_control_changed_files() { grep -E \ -e '^\.github/workflows/(staging-build|prepare-merge|activate-merge-candidate|recover-rejected-merge-candidate|staging-cleanup|force-rebuild|reusable-package-source-publish)\.yml$' \ - -e '^\.github/scripts/(activate-merge-candidate|cleanup-merge-candidates|clone-rejected-merge-candidate|compose-staging-release-snapshots|download-verified-release-asset|fetch-canonical-index|github-api-get|init-merge-candidate|latest-merge-gate-status|mark-merge-candidate-ready|reconcile-merge-candidates|recover-canonical-indexes|require-exact-head-approval|select-package-archive-source|state-lock|test-activate-merge-candidate|test-cleanup-merge-candidates|test-clone-rejected-merge-candidate|test-download-verified-release-asset|test-fetch-canonical-index|test-init-merge-candidate|test-latest-merge-gate-status|test-merge-candidate-workflows|test-reconcile-merge-candidates|test-recover-canonical-indexes|test-require-exact-head-approval|test-select-package-archive-source|test-state-lock|test-validate-staging-release|test-verify-merge-candidate|validate-staging-release|verify-merge-candidate)\.sh$' \ + -e '^\.github/scripts/(activate-merge-candidate|cleanup-merge-candidates|clone-rejected-merge-candidate|compose-staging-release-snapshots|derive-rootfs-staging-scope|download-verified-release-asset|fetch-canonical-index|github-api-get|init-merge-candidate|latest-merge-gate-status|mark-merge-candidate-ready|reconcile-merge-candidates|recover-canonical-indexes|require-exact-head-approval|select-package-archive-source|state-lock|test-activate-merge-candidate|test-cleanup-merge-candidates|test-clone-rejected-merge-candidate|test-derive-rootfs-staging-scope|test-download-verified-release-asset|test-fetch-canonical-index|test-init-merge-candidate|test-latest-merge-gate-status|test-merge-candidate-workflows|test-reconcile-merge-candidates|test-recover-canonical-indexes|test-require-exact-head-approval|test-select-package-archive-source|test-state-lock|test-validate-staging-release|test-verify-merge-candidate|validate-staging-release|verify-merge-candidate)\.sh$' \ -e '^scripts/(compose-initial-index|index-update|release-index-state)\.sh$' \ -e '^tests/scripts/(index-update|package-publish-flow|release-index-state)\.sh$' \ -e '^tests/scripts/ci-run-test-suite-groups\.test\.sh$' \ diff --git a/.github/actions/detect-change-scope/test-ci-scope-paths.sh b/.github/actions/detect-change-scope/test-ci-scope-paths.sh index 4e9b23bbc7..845592178c 100755 --- a/.github/actions/detect-change-scope/test-ci-scope-paths.sh +++ b/.github/actions/detect-change-scope/test-ci-scope-paths.sh @@ -216,6 +216,12 @@ assert_matches package_publish_flow_changed_files \ assert_matches package_publish_flow_changed_files \ ".github/scripts/test-validate-staging-release.sh" \ ".github/scripts/test-validate-staging-release.sh" +assert_matches package_publish_flow_changed_files \ + ".github/scripts/derive-rootfs-staging-scope.sh" \ + ".github/scripts/derive-rootfs-staging-scope.sh" +assert_matches package_publish_flow_changed_files \ + ".github/scripts/test-derive-rootfs-staging-scope.sh" \ + ".github/scripts/test-derive-rootfs-staging-scope.sh" assert_matches package_publish_flow_changed_files \ "tools/xtask/src/staging_reuse.rs" \ "tools/xtask/src/staging_reuse.rs" @@ -329,6 +335,12 @@ assert_matches ci_control_changed_files \ assert_matches ci_control_changed_files \ ".github/scripts/test-validate-staging-release.sh" \ ".github/scripts/test-validate-staging-release.sh" +assert_matches ci_control_changed_files \ + ".github/scripts/derive-rootfs-staging-scope.sh" \ + ".github/scripts/derive-rootfs-staging-scope.sh" +assert_matches ci_control_changed_files \ + ".github/scripts/test-derive-rootfs-staging-scope.sh" \ + ".github/scripts/test-derive-rootfs-staging-scope.sh" assert_matches ci_control_changed_files \ ".github/scripts/recover-canonical-indexes.sh" \ ".github/scripts/recover-canonical-indexes.sh" diff --git a/.github/scripts/derive-rootfs-staging-scope.sh b/.github/scripts/derive-rootfs-staging-scope.sh new file mode 100755 index 0000000000..553aee4fbe --- /dev/null +++ b/.github/scripts/derive-rootfs-staging-scope.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# Derive the exact wasm32 package generation needed to build rootfs. +set -euo pipefail + +REPO_ROOT="" +XTASK="" +OUTPUT="" + +while [ "$#" -gt 0 ]; do + case "$1" in + --repo-root) REPO_ROOT="$2"; shift 2 ;; + --xtask) XTASK="$2"; shift 2 ;; + --output) OUTPUT="$2"; shift 2 ;; + *) + echo "derive-rootfs-staging-scope: unknown flag $1" >&2 + exit 2 + ;; + esac +done + +if [ -z "$REPO_ROOT" ] || [ ! -d "$REPO_ROOT" ] || [ -L "$REPO_ROOT" ] || + [ -z "$XTASK" ] || [ ! -f "$XTASK" ] || [ -L "$XTASK" ] || [ ! -x "$XTASK" ] || + [ -z "$OUTPUT" ] || [ "$OUTPUT" = / ]; then + echo "derive-rootfs-staging-scope: regular repo root, xtask, and output are required" >&2 + exit 2 +fi + +REPO_ROOT="$(cd "$REPO_ROOT" && pwd -P)" +REGISTRY="$REPO_ROOT/packages/registry" +PROGRAM_INDEX="$REGISTRY/program-packages.json" +OUTPUT_PARENT="$(dirname "$OUTPUT")" + +if [ ! -d "$REGISTRY" ] || [ -L "$REGISTRY" ] || + [ ! -f "$PROGRAM_INDEX" ] || [ -L "$PROGRAM_INDEX" ] || + [ ! -d "$OUTPUT_PARENT" ] || [ -L "$OUTPUT_PARENT" ] || + [ -e "$OUTPUT" ] || [ -L "$OUTPUT" ]; then + echo "derive-rootfs-staging-scope: registry/index must be regular and output must be new" >&2 + exit 2 +fi + +sha_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +index_sha_before="$(sha_file "$PROGRAM_INDEX")" + +# WHY: this scope is an acceleration boundary, not an alternate source of +# package identity. Refuse a stale committed projection before selecting fewer +# builds, or an omitted generation could be mistaken for a current one. +WASM_POSIX_DEPS_REGISTRY="$REGISTRY" \ + "$XTASK" build-deps program-index-context-check \ + --source-repo-root "$REPO_ROOT" + +index_sha_after="$(sha_file "$PROGRAM_INDEX")" +if [ "$index_sha_before" != "$index_sha_after" ]; then + echo "derive-rootfs-staging-scope: program package index changed during validation" >&2 + exit 1 +fi + +TMP_ROOT="$(mktemp -d "$OUTPUT_PARENT/.rootfs-staging-scope.XXXXXX")" +trap 'rm -rf "$TMP_ROOT"' EXIT +cp "$PROGRAM_INDEX" "$TMP_ROOT/program-packages.json" +if [ "$(sha_file "$TMP_ROOT/program-packages.json")" != "$index_sha_before" ]; then + echo "derive-rootfs-staging-scope: could not snapshot the validated program package index" >&2 + exit 1 +fi + +jq -e ' + def hex256: + type == "string" and test("^[0-9a-f]{64}$"); + def package_name: + type == "string" and test("^[a-z0-9][a-z0-9._-]*$"); + . as $index | + $index.format == "kandelo-program-packages-v2" and + ($index.identities | type == "object") and + ($index.packages | type == "object") and + ($index.packages.rootfs | type == "object") and + ($index.packages.rootfs.arches == ["wasm32"]) and + (($index.packages.rootfs.cacheKeys | keys) == ["wasm32"]) and + (($index.packages.rootfs.dependencyClosures | keys) == ["wasm32"]) and + ($index.packages.rootfs.manifestSha256 | hex256) and + ($index.packages.rootfs.cacheKeys.wasm32 | hex256) and + ($index.packages.rootfs.dependencyClosures.wasm32 | + type == "array" and length > 0) and + ($index.identities.rootfs.manifestSha256 == + $index.packages.rootfs.manifestSha256) and + ($index.identities.rootfs.cacheKeys.wasm32 == + $index.packages.rootfs.cacheKeys.wasm32) and + all($index.packages.rootfs.dependencyClosures.wasm32[]; + . as $dependency | + (($dependency | keys) == + ["cacheKey", "manifestSha256", "packageName"]) and + ($dependency.packageName | package_name) and + ($dependency.packageName != "rootfs") and + ($dependency.manifestSha256 | hex256) and + ($dependency.cacheKey | hex256) and + ($index.packages[$dependency.packageName] | type == "object") and + ($index.packages[$dependency.packageName].arches | index("wasm32") != null) and + ($index.packages[$dependency.packageName].manifestSha256 == + $dependency.manifestSha256) and + ($index.packages[$dependency.packageName].cacheKeys.wasm32 == + $dependency.cacheKey) and + ($index.identities[$dependency.packageName].manifestSha256 == + $dependency.manifestSha256) and + ($index.identities[$dependency.packageName].cacheKeys.wasm32 == + $dependency.cacheKey) + ) and + ( + [$index.packages.rootfs.dependencyClosures.wasm32[].packageName] | + length == (unique | length) + ) +' "$TMP_ROOT/program-packages.json" >/dev/null || { + echo "derive-rootfs-staging-scope: invalid rootfs wasm32 projection" >&2 + exit 1 +} + +jq -S --arg source_index_sha256 "$index_sha_before" ' + { + format: "kandelo-rootfs-staging-scope-v1", + source_index_sha256: $source_index_sha256, + root_package: "rootfs", + arch: "wasm32", + entries: ( + ( + .packages.rootfs.dependencyClosures.wasm32 | + map({ + package: .packageName, + arch: "wasm32", + manifest_sha256: .manifestSha256, + cache_key_sha: .cacheKey + }) + ) + [{ + package: "rootfs", + arch: "wasm32", + manifest_sha256: .packages.rootfs.manifestSha256, + cache_key_sha: .packages.rootfs.cacheKeys.wasm32 + }] | + sort_by(.package, .arch) + ) + } +' "$TMP_ROOT/program-packages.json" >"$TMP_ROOT/scope.json" + +jq -e ' + (keys == [ + "arch", + "entries", + "format", + "root_package", + "source_index_sha256" + ]) and + .format == "kandelo-rootfs-staging-scope-v1" and + .root_package == "rootfs" and + .arch == "wasm32" and + (.source_index_sha256 | + type == "string" and test("^[0-9a-f]{64}$")) and + (.entries | length > 1) and + all(.entries[]; + (.arch == "wasm32") and + (.package | type == "string" and + test("^[a-z0-9][a-z0-9._-]*$")) and + (.manifest_sha256 | type == "string" and + test("^[0-9a-f]{64}$")) and + (.cache_key_sha | type == "string" and + test("^[0-9a-f]{64}$")) + ) and + ([.entries[] | [.package, .arch]] | + length == (unique | length)) +' "$TMP_ROOT/scope.json" >/dev/null || { + echo "derive-rootfs-staging-scope: derived an invalid staging scope" >&2 + exit 1 +} + +mv "$TMP_ROOT/scope.json" "$OUTPUT" +count="$(jq -r '.entries | length' "$OUTPUT")" +echo "derive-rootfs-staging-scope: selected $count wasm32 package generations" >&2 diff --git a/.github/scripts/test-derive-rootfs-staging-scope.sh b/.github/scripts/test-derive-rootfs-staging-scope.sh new file mode 100755 index 0000000000..6b41de4879 --- /dev/null +++ b/.github/scripts/test-derive-rootfs-staging-scope.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DERIVE="$SCRIPT_DIR/derive-rootfs-staging-scope.sh" +TMP_ROOT="$(mktemp -d)" +TMP_ROOT="$(cd "$TMP_ROOT" && pwd -P)" +trap 'rm -rf "$TMP_ROOT"' EXIT + +mkdir -p "$TMP_ROOT/repo/packages/registry" "$TMP_ROOT/bin" "$TMP_ROOT/out" +hex_a="$(printf 'a%.0s' {1..64})" +hex_b="$(printf 'b%.0s' {1..64})" +hex_c="$(printf 'c%.0s' {1..64})" + +write_valid_index() { + jq -nS --arg a "$hex_a" --arg b "$hex_b" --arg c "$hex_c" '{ + format: "kandelo-program-packages-v2", + identities: { + dep: {manifestSha256:$a, cacheKeys:{wasm32:$a, wasm64:$c}}, + rootfs: {manifestSha256:$b, cacheKeys:{wasm32:$b, wasm64:$c}}, + unrelated: {manifestSha256:$c, cacheKeys:{wasm64:$c}} + }, + packages: { + dep: { + manifestSha256:$a, + arches:["wasm32"], + cacheKeys:{wasm32:$a}, + dependencyClosures:{wasm32:[]}, + members:[] + }, + rootfs: { + manifestSha256:$b, + arches:["wasm32"], + cacheKeys:{wasm32:$b}, + dependencyClosures:{wasm32:[{ + packageName:"dep", + manifestSha256:$a, + cacheKey:$a + }]}, + members:[] + }, + unrelated: { + manifestSha256:$c, + arches:["wasm64"], + cacheKeys:{wasm64:$c}, + dependencyClosures:{wasm64:[]}, + members:[] + } + } + }' >"$TMP_ROOT/repo/packages/registry/program-packages.json" +} + +cat >"$TMP_ROOT/bin/xtask" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >"${XTASK_LOG:?}" +[ "$1 $2" = "build-deps program-index-context-check" ] +[ "$3" = "--source-repo-root" ] +[ "$4" = "${EXPECTED_REPO_ROOT:?}" ] +[ "${WASM_POSIX_DEPS_REGISTRY:?}" = "$EXPECTED_REPO_ROOT/packages/registry" ] +if [ "${XTASK_MUTATE_INDEX:-0}" = 1 ]; then + printf '\n' >>"$WASM_POSIX_DEPS_REGISTRY/program-packages.json" +fi +[ "${XTASK_FAIL:-0}" != 1 ] +EOF +chmod +x "$TMP_ROOT/bin/xtask" + +run_derive() { + local output="$1" + env \ + XTASK_LOG="$TMP_ROOT/xtask.log" \ + EXPECTED_REPO_ROOT="$TMP_ROOT/repo" \ + "$DERIVE" \ + --repo-root "$TMP_ROOT/repo" \ + --xtask "$TMP_ROOT/bin/xtask" \ + --output "$output" +} + +expect_failure() { + local description="$1" + shift + if "$@"; then + echo "derive rootfs staging scope test: accepted $description" >&2 + exit 1 + fi +} + +write_valid_index +run_derive "$TMP_ROOT/out/scope.json" +[ "$(jq -r '.format' "$TMP_ROOT/out/scope.json")" = \ + kandelo-rootfs-staging-scope-v1 ] +[ "$(jq -r '.entries | length' "$TMP_ROOT/out/scope.json")" = 2 ] +[ "$(jq -r '[.entries[].package] | join(",")' "$TMP_ROOT/out/scope.json")" = \ + dep,rootfs ] +[ "$(jq -r '[.entries[].arch] | unique | join(",")' "$TMP_ROOT/out/scope.json")" = \ + wasm32 ] +[ "$(jq -r '.entries[] | select(.package == "dep") | .cache_key_sha' \ + "$TMP_ROOT/out/scope.json")" = "$hex_a" ] +grep -Fxq \ + "build-deps program-index-context-check --source-repo-root $TMP_ROOT/repo" \ + "$TMP_ROOT/xtask.log" + +write_valid_index +XTASK_FAIL=1 expect_failure "a stale projection" \ + run_derive "$TMP_ROOT/out/stale.json" +[ ! -e "$TMP_ROOT/out/stale.json" ] + +write_valid_index +XTASK_MUTATE_INDEX=1 expect_failure "an index mutated during validation" \ + run_derive "$TMP_ROOT/out/mutated.json" +[ ! -e "$TMP_ROOT/out/mutated.json" ] + +write_valid_index +jq '.format = "unknown"' \ + "$TMP_ROOT/repo/packages/registry/program-packages.json" \ + >"$TMP_ROOT/bad.json" +mv "$TMP_ROOT/bad.json" \ + "$TMP_ROOT/repo/packages/registry/program-packages.json" +expect_failure "a malformed projection" \ + run_derive "$TMP_ROOT/out/malformed.json" + +write_valid_index +jq 'del(.packages.rootfs)' \ + "$TMP_ROOT/repo/packages/registry/program-packages.json" \ + >"$TMP_ROOT/bad.json" +mv "$TMP_ROOT/bad.json" \ + "$TMP_ROOT/repo/packages/registry/program-packages.json" +expect_failure "a missing rootfs projection" \ + run_derive "$TMP_ROOT/out/missing-rootfs.json" + +write_valid_index +jq '.packages.rootfs.arches += ["wasm64"] | + .packages.rootfs.cacheKeys.wasm64 = .identities.rootfs.cacheKeys.wasm64 | + .packages.rootfs.dependencyClosures.wasm64 = []' \ + "$TMP_ROOT/repo/packages/registry/program-packages.json" \ + >"$TMP_ROOT/bad.json" +mv "$TMP_ROOT/bad.json" \ + "$TMP_ROOT/repo/packages/registry/program-packages.json" +expect_failure "a rootfs projection expanded to wasm64" \ + run_derive "$TMP_ROOT/out/wasm64.json" + +write_valid_index +jq '.packages.rootfs.dependencyClosures.wasm32 += + [.packages.rootfs.dependencyClosures.wasm32[0]]' \ + "$TMP_ROOT/repo/packages/registry/program-packages.json" \ + >"$TMP_ROOT/bad.json" +mv "$TMP_ROOT/bad.json" \ + "$TMP_ROOT/repo/packages/registry/program-packages.json" +expect_failure "duplicate rootfs dependencies" \ + run_derive "$TMP_ROOT/out/duplicate.json" + +write_valid_index +jq --arg c "$hex_c" \ + '.packages.rootfs.dependencyClosures.wasm32[0].cacheKey = $c' \ + "$TMP_ROOT/repo/packages/registry/program-packages.json" \ + >"$TMP_ROOT/bad.json" +mv "$TMP_ROOT/bad.json" \ + "$TMP_ROOT/repo/packages/registry/program-packages.json" +expect_failure "a dependency identity mismatch" \ + run_derive "$TMP_ROOT/out/identity.json" + +write_valid_index +mv "$TMP_ROOT/repo/packages/registry/program-packages.json" \ + "$TMP_ROOT/repo/packages/registry/program-packages.real.json" +ln -s program-packages.real.json \ + "$TMP_ROOT/repo/packages/registry/program-packages.json" +expect_failure "a symlinked projection" \ + run_derive "$TMP_ROOT/out/symlink.json" + +rm "$TMP_ROOT/repo/packages/registry/program-packages.json" +mv "$TMP_ROOT/repo/packages/registry/program-packages.real.json" \ + "$TMP_ROOT/repo/packages/registry/program-packages.json" +write_valid_index +printf 'occupied\n' >"$TMP_ROOT/out/occupied.json" +expect_failure "an existing output target" \ + run_derive "$TMP_ROOT/out/occupied.json" + +echo "derive rootfs staging scope tests passed" diff --git a/.github/scripts/test-merge-candidate-workflows.sh b/.github/scripts/test-merge-candidate-workflows.sh index f730e8c04b..0e3aa903e3 100755 --- a/.github/scripts/test-merge-candidate-workflows.sh +++ b/.github/scripts/test-merge-candidate-workflows.sh @@ -114,6 +114,27 @@ grep -Fq "contains(github.event.pull_request.labels.*.name, 'preserve-head-commi "$PREPARE" || fail "Prepare merge does not select merge-commit verification for preserve-head-commit" grep -Fq 'batched-changes and preserve-head-commit are mutually exclusive' "$PREPARE" || \ fail "Prepare merge does not reject conflicting history-method labels" +grep -Fq 'Remove stage-rootfs-closure-only and complete an ordinary staging run before prepare-merge.' \ + "$PREPARE" || \ + fail "Prepare merge does not reject the limited rootfs staging scope" +grep -Fq 'description="Limited rootfs staging only; remove the scope label before prepare-merge."' \ + "$STAGING_WORKFLOW" || \ + fail "Limited rootfs staging can look like merge authorization" +grep -Fq 'bash .github/scripts/derive-rootfs-staging-scope.sh' "$STAGING_WORKFLOW" || \ + fail "Staging does not derive the rootfs scope from the validated package projection" +if grep -Fq 'rootfs_scope=$(mktemp)' "$STAGING_WORKFLOW"; then + fail "Staging passes an already-created file to the no-clobber rootfs scope helper" +fi +grep -Fq 'rootfs_scope_dir=$(mktemp -d)' "$STAGING_WORKFLOW" || \ + fail "Staging preflight does not reserve a private parent for its new rootfs scope" +grep -Fq 'ROOTFS_SCOPE_DIR=$(mktemp -d "$RUNNER_TEMP/staging-rootfs-scope.XXXXXX")' \ + "$STAGING_WORKFLOW" || \ + fail "Staging finalization does not reserve a private parent for its new rootfs scope" +grep -Fq '[ "$ROOTFS_STAGING_ONLY" != "true" ] &&' "$STAGING_WORKFLOW" || \ + fail "Limited rootfs staging can skip entries already present in the canonical release" +grep -Fq 'limited rootfs staging target changed after preflight; refusing canonical supplementation' \ + "$STAGING_WORKFLOW" || \ + fail "Limited rootfs staging can hide target mutation with canonical supplementation" grep -Fq 'expected_parents="$base_sha $head_sha"' "$VERIFY_SCRIPT" || \ fail "merge-commit activation does not bind the exact prepared base and head parents" diff --git a/.github/workflows/prepare-merge.yml b/.github/workflows/prepare-merge.yml index 69b3fd6b40..0612384e54 100644 --- a/.github/workflows/prepare-merge.yml +++ b/.github/workflows/prepare-merge.yml @@ -40,6 +40,7 @@ on: env: BATCHED_CHANGES: ${{ contains(github.event.pull_request.labels.*.name, 'batched-changes') }} PRESERVE_HEAD_COMMIT: ${{ contains(github.event.pull_request.labels.*.name, 'preserve-head-commit') }} + LIMITED_ROOTFS_STAGING: ${{ contains(github.event.pull_request.labels.*.name, 'stage-rootfs-closure-only') }} PREPARE_MERGE_METHOD: ${{ contains(github.event.pull_request.labels.*.name, 'preserve-head-commit') && 'merge' || contains(github.event.pull_request.labels.*.name, 'batched-changes') && 'rebase' || 'squash' }} GIT_FETCH_RETRY_FUNC: | git_fetch_retry() { @@ -107,6 +108,13 @@ jobs: id: synthesize run: | set -euo pipefail + # WHY: stage-rootfs-closure-only deliberately defers package + # generations and full suites. It may prove prepublication bytes, + # but it must never become authority for a merge. + if [ "$LIMITED_ROOTFS_STAGING" = true ]; then + echo "::error::Remove stage-rootfs-closure-only and complete an ordinary staging run before prepare-merge." + exit 1 + fi # WHY: each label selects a different post-merge history proof. # Silently preferring one would let the operator perform the other # method and leave an otherwise valid candidate terminally rejected. @@ -197,7 +205,12 @@ jobs: if: failure() run: | gh pr edit "$PR_NUMBER" --repo ${{ github.repository }} --remove-label ready-to-ship || true - gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "Cannot prepare merge: prepare-merge could not synthesize and persist a merge of PR head \`$PR_HEAD_SHA\` into the current \`$BASE_REF\` base. Please rebase or merge \`$BASE_REF\` if there are conflicts, then re-apply \`ready-to-ship\`." || true + if [ "$LIMITED_ROOTFS_STAGING" = true ]; then + body="Cannot prepare merge while \`stage-rootfs-closure-only\` is present. That run stages only the rootfs wasm32 closure and deliberately defers the rest. Remove the limited-scope label, let ordinary staging finish, then re-apply \`ready-to-ship\`." + else + body="Cannot prepare merge: prepare-merge could not synthesize and persist a merge of PR head \`$PR_HEAD_SHA\` into the current \`$BASE_REF\` base. Please rebase or merge \`$BASE_REF\` if there are conflicts, then re-apply \`ready-to-ship\`." + fi + gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "$body" || true # --------------------------------------------------------------------- # gate — review state + required-checks + wait-for-staging-build diff --git a/.github/workflows/staging-build.yml b/.github/workflows/staging-build.yml index aec0f988db..db113440e8 100644 --- a/.github/workflows/staging-build.yml +++ b/.github/workflows/staging-build.yml @@ -25,11 +25,11 @@ on: # a new push/label event aborts an in-flight run — fine, the overlay is # content-keyed. # -# `labeled`/`unlabeled` is only active for the skip-staging-tests label. -# Other PR labels, including ready-to-ship, get an isolated concurrency -# group so they cannot cancel a real staging run before the jobs skip. +# `labeled`/`unlabeled` is active only for the two staging-control labels. +# Other PR labels, including ready-to-ship, get an isolated concurrency group +# so they cannot cancel a real staging run before the jobs skip. concurrency: - group: staging-build-pr-${{ github.event.pull_request.number }}-${{ (github.event.action == 'labeled' || github.event.action == 'unlabeled') && github.event.label.name != 'skip-staging-tests' && github.run_id || 'active' }} + group: staging-build-pr-${{ github.event.pull_request.number }}-${{ (github.event.action == 'labeled' || github.event.action == 'unlabeled') && github.event.label.name != 'skip-staging-tests' && github.event.label.name != 'stage-rootfs-closure-only' && github.run_id || 'active' }} cancel-in-progress: true jobs: @@ -58,7 +58,8 @@ jobs: ( github.event.action != 'labeled' && github.event.action != 'unlabeled' || - github.event.label.name == 'skip-staging-tests' + github.event.label.name == 'skip-staging-tests' || + github.event.label.name == 'stage-rootfs-closure-only' ) runs-on: ubuntu-latest timeout-minutes: 20 @@ -72,19 +73,28 @@ jobs: kernel_runtime_changed: ${{ steps.scope.outputs.kernel_runtime_changed }} test_gate_required: ${{ steps.scope.outputs.test_gate_required }} docs_only: ${{ steps.scope.outputs.docs_only }} - package_staging_required: ${{ steps.scope.outputs.package_staging_required }} + package_staging_required: ${{ steps.scope.outputs.package_staging_required == 'true' || steps.controls.outputs.rootfs_staging_only == 'true' }} package_staging_reason: ${{ steps.scope.outputs.package_staging_reason }} - skip_staging_tests: ${{ steps.skip.outputs.skip_staging_tests }} + skip_staging_tests: ${{ steps.controls.outputs.skip_staging_tests }} + rootfs_staging_only: ${{ steps.controls.outputs.rootfs_staging_only }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - - name: Detect staging test skip label - id: skip + - name: Detect staging control labels + id: controls run: | - if [ "${{ contains(github.event.pull_request.labels.*.name, 'skip-staging-tests') }}" = "true" ]; then + rootfs_only="${{ contains(github.event.pull_request.labels.*.name, 'stage-rootfs-closure-only') }}" + echo "rootfs_staging_only=$rootfs_only" >> "$GITHUB_OUTPUT" + if [ "$rootfs_only" = "true" ]; then + # WHY: a rootfs-only run proves package publication, not the full + # runtime. Keeping the expensive suites out also prevents their + # success from making this deliberately partial run look mergeable. + echo "skip_staging_tests=true" >> "$GITHUB_OUTPUT" + echo "::notice::stage-rootfs-closure-only label present; only the fresh rootfs wasm32 closure will be staged. All other generations and full tests are deferred." + elif [ "${{ contains(github.event.pull_request.labels.*.name, 'skip-staging-tests') }}" = "true" ]; then echo "skip_staging_tests=true" >> "$GITHUB_OUTPUT" echo "::notice::skip-staging-tests label present; staging-build will materialize artifacts and defer expensive tests to prepare-merge." else @@ -121,6 +131,8 @@ jobs: target_tag: ${{ steps.compute.outputs.target_tag }} baseline_tag: ${{ steps.compute.outputs.baseline_tag }} reuse_staging: ${{ steps.compute.outputs.reuse_staging }} + limited_scope: ${{ steps.compute.outputs.limited_scope }} + deferred_program_count: ${{ steps.compute.outputs.deferred_program_count }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -176,6 +188,9 @@ jobs: bash scripts/dev-shell.sh bash -c ' set -euo pipefail PACKAGE_STAGING_REQUIRED="${{ needs.change-scope.outputs.package_staging_required }}" + ROOTFS_STAGING_ONLY="${{ needs.change-scope.outputs.rootfs_staging_only }}" + echo "limited_scope=$ROOTFS_STAGING_ONLY" >> "$GITHUB_OUTPUT" + echo "deferred_program_count=0" >> "$GITHUB_OUTPUT" ABI=$(grep -oE "ABI_VERSION: u32 = [0-9]+" crates/shared/src/lib.rs | awk "{print \$4}") echo "abi=$ABI" >> "$GITHUB_OUTPUT" @@ -223,6 +238,7 @@ jobs: HOST_TARGET=$(rustc -vV | awk "/^host/ {print \$2}") library_matrix="[]" program_matrix="[]" + scope_seen="[]" # Temporarily disabled packages — too slow to rebuild in # CI while we have no separate prebuilt-package repo to @@ -232,13 +248,64 @@ jobs: # mirror in prepare-merge.yml) to re-enable rebuilds. disabled_pkgs=" erlang-vfs perl perl-vfs python-vfs redis texlive " - expected_ledger=$(mktemp) XTASK="target/$HOST_TARGET/release/xtask" + rootfs_scope="" + if [ "$ROOTFS_STAGING_ONLY" = "true" ]; then + # WHY: the derivation helper refuses to overwrite an existing + # path, so give it a new filename inside a private temp dir + # instead of passing the file that plain `mktemp` creates. + rootfs_scope_dir=$(mktemp -d) + rootfs_scope="$rootfs_scope_dir/scope.json" + bash .github/scripts/derive-rootfs-staging-scope.sh \ + --repo-root "$PWD" \ + --xtask "$XTASK" \ + --output "$rootfs_scope" + fi + + full_expected_ledger=$(mktemp) "$XTASK" staging-reuse expected \ --registry packages/registry \ --expected-abi "$ABI" \ --exclude "${disabled_pkgs// /,}" \ - --output "$expected_ledger" + --output "$full_expected_ledger" + expected_ledger=$(mktemp) + if [ "$ROOTFS_STAGING_ONLY" = "true" ]; then + jq -S --slurpfile scope "$rootfs_scope" " + .entries |= map(. as \$entry | + select(any(\$scope[0].entries[]; + .package == \$entry.package and .arch == \$entry.arch))) + " "$full_expected_ledger" >"$expected_ledger" + jq -e --slurpfile scope "$rootfs_scope" " + (.entries | length) == (\$scope[0].entries | length) and + all(.entries[]; . as \$entry | + .kind == \"program\" and + any(\$scope[0].entries[]; + .package == \$entry.package and + .arch == \$entry.arch and + .cache_key_sha == \$entry.cache_key_sha)) + " "$expected_ledger" >/dev/null || { + echo "preflight: rootfs scope does not exactly match the expected package ledger" >&2 + exit 1 + } + deferred_program_count=$(jq --slurpfile scope "$rootfs_scope" " + [.entries[] | . as \$entry | + select(.kind == \"program\") | + select(all(\$scope[0].entries[]; + .package != \$entry.package or .arch != \$entry.arch))] | + length + " "$full_expected_ledger") + echo "deferred_program_count=$deferred_program_count" >> "$GITHUB_OUTPUT" + echo "::notice::Limited rootfs proof selected $(jq ".entries | length" "$rootfs_scope") wasm32 generations and deferred $deferred_program_count other program generations." + jq -r --slurpfile scope "$rootfs_scope" " + .entries[] | . as \$entry | + select(.kind == \"program\") | + select(all(\$scope[0].entries[]; + .package != \$entry.package or .arch != \$entry.arch)) | + \"::notice::deferred package generation: \(.package)/\(.arch)\" + " "$full_expected_ledger" + else + cp "$full_expected_ledger" "$expected_ledger" + fi # Reusing individual assets from a partial mutable release is not # safe: an empty matrix would then make test-gate resolve entries @@ -266,6 +333,12 @@ jobs: if [ "$stale_count" -eq 0 ]; then reuse_staging=target echo "preflight: reusing fully current, asset-verified $TARGET_TAG" + elif [ "$ROOTFS_STAGING_ONLY" = "true" ]; then + # WHY: a canonical supplement is sufficient for ordinary test + # materialization, but the prepublication freezer validates + # pr--staging by itself. Rebuild the complete scope so that + # target has all current entries without a second release. + echo "preflight: limited rootfs target has $stale_count stale entries; forcing the complete target generation" else canonical_reuse_dir=$(mktemp -d) rm -rf "$canonical_reuse_dir" @@ -336,11 +409,39 @@ jobs: arches=$(awk -F'"'"'[][]'"'"' '"'"'/^arches *=/ {print $2}'"'"' "$pkg_dir/package.toml" | tr -d '"'"' "'"'"' | tr '"'"','"'"' '"'"' '"'"') arches=${arches:-wasm32} for arch in $arches; do + if [ "$ROOTFS_STAGING_ONLY" = "true" ]; then + if ! jq -e --arg pkg "$pkg" --arg arch "$arch" " + any(.entries[]; + .package == \$pkg and .arch == \$arch) + " "$rootfs_scope" >/dev/null; then + continue + fi + scope_seen=$(jq -nc \ + --argjson seen "$scope_seen" \ + --arg pkg "$pkg" \ + --arg arch "$arch" \ + "\$seen + [{package:\$pkg, arch:\$arch}]") + fi sha=$(cargo run --release -p xtask --target "$HOST_TARGET" --quiet -- \ compute-cache-key-sha --package "$pkg_dir" --arch "$arch" 2>/dev/null) || { + if [ "$ROOTFS_STAGING_ONLY" = "true" ]; then + echo "preflight: selected rootfs generation $pkg/$arch has no cache key" >&2 + exit 1 + fi echo "preflight: skipping $pkg/$arch (compute-cache-key-sha failed)" >&2 continue } + if [ "$ROOTFS_STAGING_ONLY" = "true" ]; then + expected_sha=$(jq -r --arg pkg "$pkg" --arg arch "$arch" " + .entries[] | + select(.package == \$pkg and .arch == \$arch) | + .cache_key_sha + " "$rootfs_scope") + if [ "$sha" != "$expected_sha" ]; then + echo "preflight: selected rootfs generation $pkg/$arch differs from its fresh projection" >&2 + exit 1 + fi + fi short=${sha:0:8} if [ "$reuse_staging" != false ] && jq -e --arg pkg "$pkg" --arg arch "$arch" " @@ -362,7 +463,13 @@ jobs: suffix="-abi${ABI}-${arch}-${short}.tar.zst" existing_archive=$(echo "$existing_assets" | jq -r --arg pre "$prefix" --arg suf "$suffix" \ "first(.[] | select(startswith(\$pre) and endswith(\$suf))) // \"\"") - if [ -n "$existing_archive" ] && scripts/index-has-current-entry.sh "$existing_index" "$pkg" "$arch" "$sha" "$revision"; then + # WHY: the freeze step consumes the PR tag alone. A matching + # canonical archive therefore cannot remove an entry from this + # forced scope; every one of the 15 current generations must + # be written to the target staging release itself. + if [ "$ROOTFS_STAGING_ONLY" != "true" ] && + [ -n "$existing_archive" ] && + scripts/index-has-current-entry.sh "$existing_index" "$pkg" "$arch" "$sha" "$revision"; then echo "preflight: skip $pkg/$arch (sha $short already indexed)" continue fi @@ -377,6 +484,19 @@ jobs: done done + if [ "$ROOTFS_STAGING_ONLY" = "true" ]; then + selected_pairs=$(mktemp) + seen_pairs=$(mktemp) + jq -S "[.entries[] | {package, arch}] | sort_by(.package, .arch)" \ + "$rootfs_scope" >"$selected_pairs" + printf "%s" "$scope_seen" | + jq -S "sort_by(.package, .arch)" >"$seen_pairs" + cmp "$selected_pairs" "$seen_pairs" || { + echo "preflight: registry matrix did not cover the exact rootfs scope" >&2 + exit 1 + } + fi + program_matrix_file=$(mktemp) printf "%s" "$program_matrix" > "$program_matrix_file" program_matrix=$(cargo run --release -p xtask --target "$HOST_TARGET" --quiet -- \ @@ -432,12 +552,17 @@ jobs: echo "merge-gate already success on ${{ github.event.pull_request.head.sha }}; skipping pending post." exit 0 fi + if [ "${{ needs.change-scope.outputs.rootfs_staging_only }}" = "true" ]; then + description="Limited rootfs staging only; remove the scope label before prepare-merge." + else + description="Tests green. Apply the ready-to-ship label to build the durable release and merge." + fi gh api -X POST \ -H "Accept: application/vnd.github+json" \ "/repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }}" \ -f state=pending \ -f context=merge-gate \ - -f description="Tests green. Apply the ready-to-ship label to build the durable release and merge." \ + -f description="$description" \ -f target_url="https://github.com/${{ github.repository }}/blob/main/docs/plans/2026-04-29-pr-package-builds-design.md" package-staging-not-required: @@ -823,6 +948,7 @@ jobs: PACKAGE_BASELINE_TAG: ${{ needs.preflight.outputs.baseline_tag }} PACKAGE_REUSE_STAGING: ${{ needs.preflight.outputs.reuse_staging }} SKIP_STAGING_TESTS: ${{ needs.change-scope.outputs.skip_staging_tests }} + ROOTFS_STAGING_ONLY: ${{ needs.change-scope.outputs.rootfs_staging_only }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -871,17 +997,40 @@ jobs: SNAPSHOT="$SNAPSHOT" \ PACKAGE_TARGET_TAG="$PACKAGE_TARGET_TAG" \ PACKAGE_BASELINE_TAG="$PACKAGE_BASELINE_TAG" \ + ROOTFS_STAGING_ONLY="$ROOTFS_STAGING_ONLY" \ ABI="$ABI" \ bash -c ' set -euo pipefail HOST_TARGET=$(rustc -vV | awk "/^host/ {print \$2}") cargo build --release -p xtask --target "$HOST_TARGET" XTASK="target/$HOST_TARGET/release/xtask" + FULL_EXPECTED="$RUNNER_TEMP/staging-full-expected.json" "$XTASK" staging-reuse expected \ --registry packages/registry \ --expected-abi "$ABI" \ --exclude cpython,erlang,erlang-vfs,perl,perl-vfs,python-vfs,redis,texlive \ - --output "$EXPECTED" + --output "$FULL_EXPECTED" + if [ "$ROOTFS_STAGING_ONLY" = "true" ]; then + # WHY: preserve the helper no-clobber contract even when a + # runner is reused or a job step is retried. + ROOTFS_SCOPE_DIR=$(mktemp -d "$RUNNER_TEMP/staging-rootfs-scope.XXXXXX") + ROOTFS_SCOPE="$ROOTFS_SCOPE_DIR/scope.json" + bash .github/scripts/derive-rootfs-staging-scope.sh \ + --repo-root "$PWD" \ + --xtask "$XTASK" \ + --output "$ROOTFS_SCOPE" + jq -S --slurpfile scope "$ROOTFS_SCOPE" " + .entries |= map(. as \$entry | + select(any(\$scope[0].entries[]; + .package == \$entry.package and + .arch == \$entry.arch and + .cache_key_sha == \$entry.cache_key_sha))) + " "$FULL_EXPECTED" >"$EXPECTED" + [ "$(jq ".entries | length" "$EXPECTED")" = \ + "$(jq ".entries | length" "$ROOTFS_SCOPE")" ] + else + mv "$FULL_EXPECTED" "$EXPECTED" + fi TARGET_SNAPSHOT="$RUNNER_TEMP/fresh-target-snapshot" bash .github/scripts/validate-staging-release.sh \ --tag "$PACKAGE_TARGET_TAG" \ @@ -903,6 +1052,12 @@ jobs: bash .github/scripts/compose-staging-release-snapshots.sh \ --target-dir "$TARGET_SNAPSHOT" \ --output-dir "$SNAPSHOT" + elif [ "$ROOTFS_STAGING_ONLY" = "true" ]; then + # WHY: preflight already proved a self-contained target. If it + # changes before materialization, canonical fallback would hide + # that race and let a non-freezable target look successful. + echo "limited rootfs staging target changed after preflight; refusing canonical supplementation" >&2 + exit 1 else CANONICAL_SNAPSHOT="$RUNNER_TEMP/fresh-canonical-supplement" bash .github/scripts/validate-staging-release.sh \ @@ -931,7 +1086,15 @@ jobs: export WASM_POSIX_BINARY_INDEX_URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/binaries-abi-v${ABI}/index.toml" fi echo "WASM_POSIX_BINARY_INDEX_URL=$WASM_POSIX_BINARY_INDEX_URL" >> "$GITHUB_ENV" - bash scripts/dev-shell.sh bash scripts/fetch-binaries.sh --fetch-only + if [ "$ROOTFS_STAGING_ONLY" = "true" ]; then + # WHY: the limited proof intentionally materializes only the same + # root whose exact closure was staged. A full registry walk would + # demand the generations explicitly deferred by this label. + bash scripts/dev-shell.sh bash scripts/fetch-binaries.sh \ + --fetch-only --package rootfs + else + bash scripts/dev-shell.sh bash scripts/fetch-binaries.sh --fetch-only + fi - name: Skip staging test suites if: env.SKIP_STAGING_TESTS == 'true' diff --git a/crates/fork-instrument/src/instrument.rs b/crates/fork-instrument/src/instrument.rs index e9c430fc72..b78a7ca863 100644 --- a/crates/fork-instrument/src/instrument.rs +++ b/crates/fork-instrument/src/instrument.rs @@ -78,10 +78,10 @@ //! a block from outside). The tool panics with a diagnostic in //! that case; the function must be restructured or the tool //! extended. -//! - **Fork-from-catch-handler remains unsupported** (B1 follow-up). -//! Phase 6c/6d/6e plumbing is retained so ref-typed exnref locals -//! still round-trip cleanly across fork; if a handler contains a -//! fork-path call, the tool panics (same mechanism as nested). +//! - **Fork from modern `try_table` catches is supported.** Plain-catch +//! arm identity and scalar payloads are frame-backed per activation; +//! catch_ref values use the exnref auxiliary table. Legacy `try` +//! catch handlers remain unsupported. //! - **Scalar args only for fork-path calls.** If a fork-path call //! has a ref-typed argument, we'd need to spill it through an aux //! table (not currently wired up). Panic in that case. @@ -96,7 +96,7 @@ //! | 4 | 4 | `call_index` | //! | 8 | 4 | `catch_region_id` | //! | 12 | 4 | `exnref_slot` | -//! | 16.. | var | scalar locals (user + arg spills) | +//! | 16.. | var | scalar locals (user, arg spills, plain-catch state) | //! //! Ref-typed user locals are routed through module-level auxiliary //! tables; their storage is outside the frame. @@ -119,8 +119,8 @@ use walrus::{ ir::{ AtomicWidth, BinaryOp, Binop, Block, Br, BrTable, Call, CallIndirect, Const, GlobalGet, IfElse, Instr, InstrLocId, InstrSeqId, InstrSeqType, LegacyCatch, LoadKind, LocalGet, - LocalSet, LocalTee, Loop, MemArg, RefAsNonNull, RefIsNull, RefNull, Return, StoreKind, - TableGet, TableSet, Throw, ThrowRef, TryTable, TryTableCatch, UnaryOp, Unreachable, Value, + LocalSet, LocalTee, Loop, MemArg, RefAsNonNull, RefNull, Return, StoreKind, TableGet, + TableSet, Throw, ThrowRef, TryTable, TryTableCatch, UnaryOp, Value, }, }; @@ -146,7 +146,7 @@ pub fn instrument_functions( module: &mut Module, runtime: &Runtime, fork_path: &HashSet, - b1_plan: &B1ScratchPlan, + plain_catch_plan: &PlainCatchPlan, ) -> HashSet { let runtime_funcs: HashSet = [ runtime.unwind_begin, @@ -169,7 +169,7 @@ pub fn instrument_functions( let (aux_tables, ref_plan, catch_plans) = plan_and_inject_aux_tables(module, &targets); - let empty_b1_slots: Vec<(InstrSeqId, Vec)> = Vec::new(); + let empty_plain_catches: Vec<(InstrSeqId, Vec)> = Vec::new(); let mut instrumented = HashSet::new(); for (ordinal, id) in targets.iter().enumerate() { @@ -177,7 +177,10 @@ pub fn instrument_functions( let this_plan = ref_plan.get(id).unwrap_or(&empty_plan); let empty_catch_plan: Vec = Vec::new(); let this_catch_plan = catch_plans.get(id).unwrap_or(&empty_catch_plan); - let this_b1_slots = b1_plan.per_function.get(id).unwrap_or(&empty_b1_slots); + let this_plain_catches = plain_catch_plan + .per_function + .get(id) + .unwrap_or(&empty_plain_catches); instrument_one_function( module, *id, @@ -187,7 +190,7 @@ pub fn instrument_functions( &aux_tables, this_plan, this_catch_plan, - this_b1_slots, + this_plain_catches, ); instrumented.insert(*id); } @@ -245,7 +248,7 @@ fn instrument_one_function( aux_tables: &AuxTables, ref_plan: &[RefLocalSlot], catch_plan: &[CatchRegionPlan], - b1_slots: &[(InstrSeqId, Vec)], + plain_catches: &[(InstrSeqId, Vec)], ) { // Choose scheme based on call-site topology. Post-commit-4 // (2026-05-14) there are TWO live schemes (guard-dispatch was @@ -296,7 +299,7 @@ fn instrument_one_function( aux_tables, ref_plan, catch_plan, - b1_slots, + plain_catches, ); return; } @@ -362,7 +365,7 @@ fn instrument_one_function( aux_tables, ref_plan, catch_plan, - b1_slots, + plain_catches, ); return; } @@ -388,7 +391,7 @@ fn instrument_one_function( aux_tables, ref_plan, catch_plan, - b1_slots, + plain_catches, ); } @@ -406,7 +409,7 @@ fn instrument_one_function_switch( aux_tables: &AuxTables, ref_plan: &[RefLocalSlot], catch_plan: &[CatchRegionPlan], - b1_slots: &[(InstrSeqId, Vec)], + plain_catches: &[(InstrSeqId, Vec)], ) { // Pre-existing user locals (args + referenced in body). Scalars // live in the frame; ref-typed locals go through aux tables. @@ -433,7 +436,7 @@ fn instrument_one_function_switch( let n_calls = call_sites.len(); // Allocate per-function synthetic locals. - let catch_state_locals = if catch_plan.is_empty() && b1_slots.is_empty() { + let catch_state_locals = if catch_plan.is_empty() && plain_catches.is_empty() { None } else { Some(CatchStateLocals { @@ -492,6 +495,8 @@ fn instrument_one_function_switch( carryover_spills.push(spills); } + let plain_catch_state = allocate_plain_catch_state(module, plain_catches); + // Combined scalar locals for the frame (user locals first, then // frame-backed per-call arg spills in call order, then per-call // carryover spills in call order — added 2.4c). @@ -511,6 +516,7 @@ fn instrument_one_function_switch( frame_scalars.push((lid, ty)); } } + append_plain_catch_frame_scalars(&mut frame_scalars, &plain_catch_state); let locals_with_offsets = assign_local_offsets(&frame_scalars, LOCALS_START_OFFSET); let frame_size = HEADER_SIZE + user_locals_size(&frame_scalars); @@ -524,7 +530,8 @@ fn instrument_one_function_switch( // Plan catch-handler entry-capture (Phase 6d). We allocate in_catch // and captured_exnref locals now; the IR rewrite is applied later, // after the body has been rebuilt. - let catch_handlers = plan_catch_ref_handlers(module, func_id, catch_plan, aux_tables); + let catch_handlers = + plan_catch_ref_handlers(module, func_id, catch_plan, aux_tables, &plain_catch_state); // Build the new body: preamble-if + Block($unwind_save) + postamble. let memory = first_memory(module); @@ -533,7 +540,7 @@ fn instrument_one_function_switch( // Phase 6c rewind-throw stubs: prepended to each fork-path // try_table body. Phase 6 covers catch_ref / catch_all_ref. // B1 Stage 2 (Task 2.3) extends the same stub with a plain-catch - // dispatch when `b1_slots` lists arms for the region. + // dispatch when `plain_catches` lists arms for the region. if !catch_plan.is_empty() && aux_tables.exnref.is_some() { let catch_state = catch_state_locals.expect("exnref catch plan requires catch-state locals"); @@ -544,7 +551,7 @@ fn instrument_one_function_switch( catch_state.catch_region_id, aux_tables, catch_plan, - b1_slots, + &plain_catch_state, ); // The stub injection appended to the try_tables' own body // seqs. Those seqs are reachable from instructions inside @@ -704,14 +711,13 @@ fn instrument_one_function_switch( apply_plain_catch_handlers( module, func_id, - runtime, catch_state.catch_region_id, - b1_slots, + &plain_catch_state, catch_plan, &catch_handlers, ); } else { - debug_assert!(b1_slots.is_empty()); + debug_assert!(plain_catches.is_empty()); } } @@ -3449,13 +3455,6 @@ fn natural_align(ty: ValType) -> u32 { scalar_size(ty) } -/// Round `x` up to the nearest 8-byte boundary. Used by B1 scratch -/// planning and (Task 1.3) save-buffer reservation. A near-duplicate -/// lives in `runtime.rs`; consolidate when extracting `crate::layout`. -fn align_up_8(x: u32) -> u32 { - (x + 7) & !7u32 -} - fn default_for_type(ty: ValType) -> Option { Some(match ty { ValType::I32 => Instr::Const(Const { @@ -3838,8 +3837,8 @@ fn visit_try_tables(f: &LocalFunction, seq: InstrSeqId, out: &mut Vec)>>, + pub per_function: std::collections::HashMap)>>, /// Stage 2 (B1): functions whose plain-catch arms include /// unsupported operand types (e.g., ref-typed). For these /// functions, B1 emission tasks fall back to today's behavior @@ -3964,32 +3937,16 @@ pub struct B1ScratchPlan { pub b2_carveout: std::collections::HashSet, } -/// Stage 1 (B1) — assigns scratch offsets for every plain-catch arm -/// across all fork-path functions. The scratch area lives between -/// `saved_globals` and `frame data` in the save buffer; its base is -/// `Runtime.b1_scratch_base` (set in Task 1.3) and its total size -/// is `B1ScratchPlan.total_bytes`. +/// Discover supported plain-catch arms across all fork-path functions. /// -/// Tuple layout per arm (Stage 2 will read/write this): -/// ```text -/// +0 4 arm_id (i32) -/// +4 var operand_0 ... operand_N-1, naturally aligned -/// ``` -/// Tuples are 8-byte-aligned within the scratch area so that -/// i64/f64 operands at offset +4 (the first operand position) -/// land on a properly aligned address relative to the scratch base. -/// -/// Operand types are restricted to scalars (i32/i64/f32/f64/v128) -/// at this stage. Ref-typed operands (externref/funcref/exnref/GC -/// refs) will require aux-table spilling — a future B2 carve-out. +/// Operand types are restricted to scalars (i32/i64/f32/f64/v128). +/// Ref-typed operands (externref/funcref/exnref/GC refs) require +/// auxiliary-table spilling and remain a conservative carve-out. /// /// Stage 2 (Task 2.1) detects ref-typed payloads here and routes the -/// affected function to `B1ScratchPlan.b2_carveout` instead of +/// affected function to `PlainCatchPlan.b2_carveout` instead of /// `per_function`. Stage 2 emission tasks check the carve-out set -/// and skip plain-catch instrumentation for those functions, so -/// `scalar_size`'s ref-type panic is now unreachable from the -/// planner — it survives only as a defense-in-depth assertion for -/// future callers that bypass the carve-out filter. +/// and skip plain-catch instrumentation for those functions. /// /// The carve-out is whole-function: if any arm in any region of a /// function has a ref-typed operand, the entire function's @@ -3997,15 +3954,8 @@ pub struct B1ScratchPlan { /// arms because Task 2.3's rewind dispatcher needs the whole /// region's arm set or none. /// -/// The cursor walks `targets` in iteration order, so per-function -/// offsets depend on the caller's ordering of `targets`. Stage 2's -/// emission code reads `B1ScratchPlan.per_function[fid]` keyed by -/// `FunctionId`, so this ordering is internal to the plan and -/// irrelevant to correctness — but tests that pin specific offset -/// values must use stable target ordering. -pub fn plan_b1_scratch(module: &Module, targets: &[FunctionId]) -> B1ScratchPlan { - let mut plan = B1ScratchPlan::default(); - let mut cursor: u32 = 0; +pub fn plan_plain_catches(module: &Module, targets: &[FunctionId]) -> PlainCatchPlan { + let mut plan = PlainCatchPlan::default(); for &fid in targets { let arms_per_region = discover_plain_catch_arms(module, fid); if arms_per_region.is_empty() { @@ -4013,10 +3963,7 @@ pub fn plan_b1_scratch(module: &Module, targets: &[FunctionId]) -> B1ScratchPlan } // Stage 2 (B1): detect unsupported operand types and carve out // the entire function. We can't selectively drop just the bad - // arms because the rewind-throw stub dispatches by arm_id and - // expects every arm in a region to have a scratch slot — Stage - // 2 keeps things simple by treating the whole function's - // plain-catch as off-limits if any arm has a ref operand. + // arms because replay needs a complete region-wide arm set. let has_unsupported = arms_per_region.iter().any(|(_, arms)| { arms.iter() .any(|arm| arm.operand_tys.iter().any(|t| matches!(t, ValType::Ref(_)))) @@ -4042,39 +3989,72 @@ pub fn plan_b1_scratch(module: &Module, targets: &[FunctionId]) -> B1ScratchPlan plan.b2_carveout.insert(fid); continue; } - let mut per_func: Vec<(InstrSeqId, Vec)> = - Vec::with_capacity(arms_per_region.len()); - for (body_seq, arm_list) in arms_per_region { - let mut slots: Vec = Vec::with_capacity(arm_list.len()); - for arm in arm_list { - debug_assert!( - arm.operand_tys + plan.per_function.insert(fid, arms_per_region); + } + plan +} + +/// Activation-owned plain-catch state for one static arm. +#[derive(Debug, Clone)] +struct PlainCatchArmState { + arm: PlainCatchArm, + operand_locals: Vec, +} + +/// Activation-owned state for one try_table with plain catches. +/// +/// WHY: these locals are added to the ordinary function frame, so recursive +/// calls and later activations cannot alias one module-wide scratch tuple. +#[derive(Debug, Clone)] +struct PlainCatchRegionState { + body_seq: InstrSeqId, + active_arm: LocalId, + arms: Vec, +} + +fn allocate_plain_catch_state( + module: &mut Module, + plain_catches: &[(InstrSeqId, Vec)], +) -> Vec { + plain_catches + .iter() + .map(|(body_seq, arms)| PlainCatchRegionState { + body_seq: *body_seq, + active_arm: module.locals.add(ValType::I32), + arms: arms + .iter() + .cloned() + .map(|arm| { + let operand_locals = arm + .operand_tys .iter() - .all(|t| !matches!(t, ValType::Ref(_))), - "B1 plan_b1_scratch invariant: caller must filter ref-payload arms via Stage 2 \ - b2_carveout (excluded from fork-path) before reaching the planner. Affected \ - function has a tag with a ref-typed operand." - ); - let payload_size: u32 = arm.operand_tys.iter().map(|t| scalar_size(*t)).sum(); - let tuple_size = 4 + payload_size; - // Outer 8-byte alignment for the tuple start. - let aligned = align_up_8(cursor); - slots.push(PlainCatchArmSlot { - arm, - scratch_offset: aligned, - tuple_size, - }); - cursor = aligned + tuple_size; - } - per_func.push((body_seq, slots)); + .map(|&ty| module.locals.add(ty)) + .collect(); + PlainCatchArmState { + arm, + operand_locals, + } + }) + .collect(), + }) + .collect() +} + +fn append_plain_catch_frame_scalars( + frame_scalars: &mut Vec<(LocalId, ValType)>, + regions: &[PlainCatchRegionState], +) { + for region in regions { + frame_scalars.push((region.active_arm, ValType::I32)); + for arm in ®ion.arms { + frame_scalars.extend( + arm.operand_locals + .iter() + .copied() + .zip(arm.arm.operand_tys.iter().copied()), + ); } - plan.per_function.insert(fid, per_func); } - // Final scratch area aligned up to 8 bytes so `frames_start_offset` - // (which sits after the scratch area) lands aligned for the frame - // header writes that follow. - plan.total_bytes = align_up_8(cursor); - plan } // ---------------------------------------------------------------------- @@ -4090,26 +4070,10 @@ pub fn plan_b1_scratch(module: &Module, targets: &[FunctionId]) -> B1ScratchPlan /// exception that was caught pre-fork. The shape depends on what kind /// of catch was originally taken: /// -/// - **catch_ref / catch_all_ref**: Phase 6's `apply_catch_ref_handlers` -/// stashed the exnref in `_wpk_fork_exnref_stash[slot]`. We re-throw -/// it via `throw_ref`. Selected when the stash slot is non-null. -/// -/// - **plain catch (B1 Stage 2)**: the per-arm capture block stored -/// `(arm_id, op_0, ..., op_M-1)` in the B1 scratch tuple at -/// `runtime.b1_scratch_base + slot.scratch_offset`. We load the -/// arm_id, dispatch by value to the matching arm, push the saved -/// operands, and `throw $tag`. Selected when the stash slot is null. -/// -/// The exnref-stash null-check is the sentinel: Phase 6's capture -/// always writes a non-null exnref; B1's plain-catch capture never -/// touches the stash. This naturally disambiguates regions with mixed -/// `catch_ref` and `catch` clauses without Phase 6 needing to know -/// about B1. -/// -/// `b1_slots_lookup` maps `body_seq -> arm slots` for regions that have -/// at least one plain-catch arm in scope (i.e. function not in -/// `b2_carveout`). Regions with no plain arms fall back to today's -/// catch_ref-only stub shape. +/// Plain catches restore a frame-backed active-arm local and operand locals, +/// then throw the matching tag. A catch_ref capture writes active-arm `-1`, +/// so a mixed region falls through to the exnref `throw_ref` path without +/// treating stale auxiliary-table contents as mode state. fn inject_rewind_throw_stubs( module: &mut Module, func_id: FunctionId, @@ -4117,7 +4081,7 @@ fn inject_rewind_throw_stubs( catch_region_id_local: LocalId, aux_tables: &AuxTables, catch_plan: &[CatchRegionPlan], - b1_slots: &[(InstrSeqId, Vec)], + plain_catches: &[PlainCatchRegionState], ) { let exnref_table = match aux_tables.exnref { Some(t) => t, @@ -4127,23 +4091,20 @@ fn inject_rewind_throw_stubs( } }; - let b1_lookup: HashMap = b1_slots + let plain_lookup: HashMap = plain_catches .iter() - .map(|(seq, slots)| (*seq, slots.as_slice())) + .map(|region| (region.body_seq, region)) .collect(); - let memory = first_memory(module); - for plan in catch_plan { let body_seq_id = plan.body_seq; let region_id = plan.catch_region_id; let slot = plan.exnref_slot; - let plain_arms: &[PlainCatchArmSlot] = b1_lookup.get(&body_seq_id).copied().unwrap_or(&[]); + let plain_region = plain_lookup.get(&body_seq_id).copied(); // Build the inner "catch_ref path" sequence (Phase 6's existing - // logic). Always emitted — used either as the only path - // (region has no plain arms) or as the false-branch of the - // B1 sentinel check (region has plain arms). + // logic). Always emitted — used either as the only path or as + // the fallback when no exact plain arm is active. let throw_ref_seq_id = { let local = local_mut(module, func_id); let s = local @@ -4168,48 +4129,9 @@ fn inject_rewind_throw_stubs( s }; - // Build the "rewind dispatch" sequence: either a single - // throw_ref (no plain arms) or a sentinel-gated split between - // plain-catch dispatch and throw_ref. - let dispatch_seq_id = if plain_arms.is_empty() { - throw_ref_seq_id - } else { - // B1 plain-catch dispatch: build first, then wrap in - // an IfElse that selects on `exnref_stash[slot] is null`. - let plain_dispatch_id = - build_plain_catch_dispatch(module, func_id, runtime, memory, plain_arms); - - let local = local_mut(module, func_id); - let outer = local - .builder_mut() - .dangling_instr_seq(InstrSeqType::Simple(None)) - .id(); - let block = &mut local.block_mut(outer).instrs; - // Sentinel: load `_wpk_fork_exnref_stash[slot]` and check if - // it's null. If null → plain-catch dispatch; if non-null → - // catch_ref throw_ref. - push_instr( - block, - Instr::Const(Const { - value: Value::I32(slot as i32), - }), - ); - push_instr( - block, - Instr::TableGet(TableGet { - table: exnref_table, - }), - ); - push_instr(block, Instr::RefIsNull(RefIsNull {})); - push_instr( - block, - Instr::IfElse(IfElse { - consequent: plain_dispatch_id, - alternative: throw_ref_seq_id, - }), - ); - outer - }; + let dispatch_seq_id = plain_region.map_or(throw_ref_seq_id, |region| { + build_plain_catch_dispatch(module, func_id, region, throw_ref_seq_id) + }); // Build the empty else for the outer REWIND-match guard. let else_id = { @@ -4281,54 +4203,21 @@ fn inject_rewind_throw_stubs( } } -/// Build a dangling instr_seq that performs B1 plain-catch rewind -/// dispatch. The sequence reads the saved `arm_id` from the B1 scratch -/// tuple and walks an if-chain over `arm_idx` values, throwing the -/// matching tag with operands loaded from the same tuple. -/// -/// Layout of the scratch tuple (per `emit_capture_save_and_branch`): -/// +0 i32 arm_id -/// +4.. scalar operand_0, operand_1, ... +/// Build a dangling sequence that rethrows one frame-restored plain catch. /// -/// The base address is `*runtime.buf_global + runtime.b1_scratch_base -/// + slot.scratch_offset`. +/// Exact nonnegative arm IDs select plain catches. `-1` deliberately falls +/// through to `throw_ref_fallback`, which is what a catch_ref capture records +/// for a mixed region. fn build_plain_catch_dispatch( module: &mut Module, func_id: FunctionId, - runtime: &Runtime, - memory: MemoryId, - arms: &[PlainCatchArmSlot], + region: &PlainCatchRegionState, + throw_ref_fallback: InstrSeqId, ) -> InstrSeqId { - debug_assert!(!arms.is_empty()); - - // We build the if-chain bottom-up: the "else" of arm[N-1] is a - // single `unreachable` (all arm_ids exhausted); each preceding - // arm wraps the previous chain in `if arm_id == K { throw J } else { - // ...prev... }`. - // - // arm_id is stable across capture+rewind: capture writes - // `slot.arm.arm_idx` at offset +0; we compare against the same - // value here. - - // Innermost else: unreachable (defense; arm_id always matches one - // of the saved arms). - let unreachable_id = { - let local = local_mut(module, func_id); - let s = local - .builder_mut() - .dangling_instr_seq(InstrSeqType::Simple(None)) - .id(); - push_instr( - &mut local.block_mut(s).instrs, - Instr::Unreachable(Unreachable {}), - ); - s - }; + debug_assert!(!region.arms.is_empty()); - let mut chain = unreachable_id; - for slot in arms.iter().rev() { - // Build the "throw arm[J]" body: load operands from scratch, - // then throw the tag. + let mut chain = throw_ref_fallback; + for arm in region.arms.iter().rev() { let throw_id = { let local = local_mut(module, func_id); local @@ -4336,36 +4225,13 @@ fn build_plain_catch_dispatch( .dangling_instr_seq(InstrSeqType::Simple(None)) .id() }; - let scratch_off = (runtime.b1_scratch_base + slot.scratch_offset) as u64; - // Operands start at +4 (after arm_id i32). Walk in declaration - // order so they land on the operand stack in the order the - // tag's params expect. - let mut cur_off: u32 = 4; - let operand_loads: Vec<(Instr, Instr)> = slot - .arm - .operand_tys - .iter() - .map(|&ty| { - let abs_off = scratch_off + cur_off as u64; - cur_off += scalar_size(ty); - ( - Instr::GlobalGet(GlobalGet { - global: runtime.buf_global, - }), - load_scalar(memory, ty, abs_off), - ) - }) - .collect(); - let local = local_mut(module, func_id); let s = &mut local.block_mut(throw_id).instrs; - for (gget, load) in operand_loads { - push_instr(s, gget); - push_instr(s, load); + for &operand in &arm.operand_locals { + push_instr(s, Instr::LocalGet(LocalGet { local: operand })); } - push_instr(s, Instr::Throw(Throw { tag: slot.arm.tag })); + push_instr(s, Instr::Throw(Throw { tag: arm.arm.tag })); - // Wrap: if (load arm_id) == arm_idx { throw_id } else { chain } let outer_id = { let local = local_mut(module, func_id); local @@ -4376,18 +4242,16 @@ fn build_plain_catch_dispatch( { let local = local_mut(module, func_id); let s = &mut local.block_mut(outer_id).instrs; - // Load arm_id from scratch (offset +0 of tuple). push_instr( s, - Instr::GlobalGet(GlobalGet { - global: runtime.buf_global, + Instr::LocalGet(LocalGet { + local: region.active_arm, }), ); - push_instr(s, load_i32(memory, scratch_off)); push_instr( s, Instr::Const(Const { - value: Value::I32(slot.arm.arm_idx as i32), + value: Value::I32(arm.arm.arm_idx as i32), }), ); push_instr( @@ -4422,6 +4286,7 @@ struct CatchHandlerInfo { target_label: InstrSeqId, in_catch_local: LocalId, captured_exnref_local: LocalId, + plain_active_arm: Option, } fn plan_catch_ref_handlers( @@ -4429,6 +4294,7 @@ fn plan_catch_ref_handlers( func_id: FunctionId, catch_plan: &[CatchRegionPlan], aux_tables: &AuxTables, + plain_catches: &[PlainCatchRegionState], ) -> Vec { let mut infos = Vec::new(); if aux_tables.exnref.is_none() { @@ -4482,6 +4348,10 @@ fn plan_catch_ref_handlers( target_label, in_catch_local, captured_exnref_local, + plain_active_arm: plain_catches + .iter() + .find(|region| region.body_seq == plan.body_seq) + .map(|region| region.active_arm), }); } @@ -4574,6 +4444,19 @@ fn apply_catch_ref_handlers( local: info.captured_exnref_local, }), ); + if let Some(active_arm) = info.plain_active_arm { + // WHY: mixed catch regions must restore their capture kind + // from frame-owned state. A negative arm cannot match any + // plain catch, so replay falls through to throw_ref without + // consulting possibly stale exnref-table nullness. + push_instr( + s, + Instr::Const(Const { + value: Value::I32(-1), + }), + ); + push_instr(s, Instr::LocalSet(LocalSet { local: active_arm })); + } push_instr( s, Instr::Const(Const { @@ -4628,24 +4511,6 @@ fn apply_catch_ref_handlers( // Stage 2 (B1) — per-arm capture-block emission for plain catch // ---------------------------------------------------------------------- -/// Per-region emission state for a B1 plain-catch transform: links the -/// region's try_table body to the function-local "in_catch" flag and -/// the region's id (used for setting `catch_region_id_local` at unwind -/// time). -/// -/// Phase 6's `CatchHandlerInfo::in_catch_local` is allocated only for -/// regions with a catch_ref/catch_all_ref clause. For plain-catch-only -/// regions we allocate a fresh local; for mixed regions we reuse Phase -/// 6's local so `maybe_record_catch_state` continues to see a single -/// flag per region. -#[derive(Debug, Clone, Copy)] -#[allow(dead_code)] // Task 2.3 will consume these fields. -struct PlainCatchRegionInfo { - body_seq: InstrSeqId, - catch_region_id: u32, - in_catch_local: LocalId, -} - /// Stage 2 (B1) — emit per-arm capture blocks that intercept plain /// catch dispatch. /// @@ -4669,12 +4534,11 @@ struct PlainCatchRegionInfo { /// ``` /// /// Inside each cap_arm_J body the operands are: -/// 1. spilled to fresh locals (top-of-stack first). -/// 2. saved as a tuple (arm_id, op_0, ..., op_M-1) at scratch slot -/// `runtime.b1_scratch_base + slot.scratch_offset`. -/// 3. used to set `in_catch_local = 1` and `catch_region_id_local = +/// 1. spilled to activation-local, frame-backed locals. +/// 2. used to set the frame-backed active arm plus +/// `in_catch_local = 1` and `catch_region_id_local = /// region_id`. -/// 4. re-pushed (in declaration order) and `br $hJ` executes. +/// 3. re-pushed (in declaration order) and `br $hJ` executes. /// /// CatchAll/CatchRef/CatchAllRef clauses are preserved verbatim. If /// Phase 6 already retargeted CatchRef/CatchAllRef clauses, those @@ -4684,29 +4548,22 @@ struct PlainCatchRegionInfo { /// is reused for any region that overlaps with B1's emission. For /// plain-catch-only regions, a fresh `in_catch_local` is allocated. /// -/// Returns the per-region info Task 2.3 will consume: the region_id -/// + in_catch_local pair lets `inject_rewind_throw_stubs` and -/// `maybe_record_catch_state` extend their dispatch to plain-catch- -/// only regions. Currently the caller discards the return; Task 2.3 -/// will plumb it through. -#[allow(dead_code)] fn apply_plain_catch_handlers( module: &mut Module, func_id: FunctionId, - runtime: &Runtime, catch_region_id_local: LocalId, - b1_slots: &[(InstrSeqId, Vec)], + plain_catches: &[PlainCatchRegionState], catch_plan: &[CatchRegionPlan], catch_handlers: &[CatchHandlerInfo], -) -> Vec { - let mut regions = Vec::new(); - if b1_slots.is_empty() { - return regions; +) { + if plain_catches.is_empty() { + return; } - let memory = first_memory(module); - for (body_seq, arm_slots) in b1_slots { - if arm_slots.is_empty() { + for region in plain_catches { + let body_seq = region.body_seq; + let arm_states = ®ion.arms; + if arm_states.is_empty() { continue; } @@ -4719,19 +4576,19 @@ fn apply_plain_catch_handlers( FunctionKind::Local(l) => l, _ => continue, }; - let (parent, tt) = - match find_try_table_parent_seq(local, local.entry_block(), *body_seq) { - Some(v) => v, - None => continue, - }; - (parent, tt.catches.clone(), local.block(*body_seq).ty) + let (parent, tt) = match find_try_table_parent_seq(local, local.entry_block(), body_seq) + { + Some(v) => v, + None => continue, + }; + (parent, tt.catches.clone(), local.block(body_seq).ty) }; // Look up region_id from catch_plan (every fork-path try_table // gets a catch_region_id assigned in plan_and_inject_aux_tables). let catch_region_id = catch_plan .iter() - .find(|p| p.body_seq == *body_seq) + .find(|p| p.body_seq == body_seq) .map(|p| p.catch_region_id) .unwrap_or(0); @@ -4741,20 +4598,14 @@ fn apply_plain_catch_handlers( // region.) let in_catch_local = catch_handlers .iter() - .find(|h| h.body_seq == *body_seq) + .find(|h| h.body_seq == body_seq) .map(|h| h.in_catch_local) .unwrap_or_else(|| module.locals.add(ValType::I32)); - regions.push(PlainCatchRegionInfo { - body_seq: *body_seq, - catch_region_id, - in_catch_local, - }); - // ---------------------------------------------------------- // Build dangling sequences: outer + N caps. // Caps are ordered outer-to-inner so `cap_seq_ids[J]` is the - // J-th outermost (and corresponds to `arm_slots[J]`). The + // J-th outermost (and corresponds to `arm_states[J]`). The // innermost cap (`cap_seq_ids[N-1]`) holds the inner try_table // and the `br $b1_outer` that handles normal exit. // ---------------------------------------------------------- @@ -4765,43 +4616,31 @@ fn apply_plain_catch_handlers( // Build per-arm InstrSeqType up-front (mutates module.types) // before any &mut LocalFunction borrow is needed. - let cap_types: Vec = arm_slots + let cap_types: Vec = arm_states .iter() - .map(|slot| InstrSeqType::new(&mut module.types, &[], &slot.arm.operand_tys)) + .map(|state| InstrSeqType::new(&mut module.types, &[], &state.arm.operand_tys)) .collect(); - let mut cap_seq_ids: Vec = Vec::with_capacity(arm_slots.len()); + let mut cap_seq_ids: Vec = Vec::with_capacity(arm_states.len()); for cap_ty in &cap_types { let local = local_mut(module, func_id); cap_seq_ids.push(local.builder_mut().dangling_instr_seq(*cap_ty).id()); } - // Per-arm operand spill locals. - let mut arm_spill_locals: Vec> = Vec::with_capacity(arm_slots.len()); - for slot in arm_slots { - let spills: Vec = slot - .arm - .operand_tys - .iter() - .map(|&ty| module.locals.add(ty)) - .collect(); - arm_spill_locals.push(spills); - } - // ---------------------------------------------------------- // Rewrite the inner try_table's catches: each plain Catch // arm now points at its capture block; everything else (incl. // catch_ref/catch_all_ref already retargeted by Phase 6) is // preserved verbatim. // - // We map by arm position within `arm_slots` -- each entry's + // We map by arm position within `arm_states` -- each entry's // `arm.arm_idx` is the arm's index in the original try_table's // catches list. We walk `original_catches` and substitute each // matching plain Catch with its capture target. // ---------------------------------------------------------- let mut new_catches: Vec = original_catches.clone(); - for (j, slot) in arm_slots.iter().enumerate() { - let arm_idx = slot.arm.arm_idx as usize; + for (j, state) in arm_states.iter().enumerate() { + let arm_idx = state.arm.arm_idx as usize; if let Some(c) = new_catches.get_mut(arm_idx) { if let TryTableCatch::Catch { tag, .. } = c { *c = TryTableCatch::Catch { @@ -4819,7 +4658,7 @@ fn apply_plain_catch_handlers( // ---------------------------------------------------------- // // D-06 fix (2026-05-14): emit_capture_save_and_branch's - // "capture tail" (spill payload, save to scratch, set flags, + // "capture tail" (spill payload to frame locals, set flags, // re-push payload, br $hJ) must run AT THE POINT WHERE // CONTROL ARRIVES AFTER THE CATCH — which, per wasm-EH's // br-to-label semantics, is OUTSIDE the cap_seq the catch @@ -4841,14 +4680,14 @@ fn apply_plain_catch_handlers( // never ran in either case, but legacy `try`/`catch` was // forced to guard-dispatch which used a completely // different mechanism. - let n = arm_slots.len(); + let n = arm_states.len(); { let local = local_mut(module, func_id); let s = &mut local.block_mut(cap_seq_ids[n - 1]).instrs; push_instr( s, Instr::TryTable(TryTable { - seq: *body_seq, + seq: body_seq, catches: new_catches, }), ); @@ -4884,11 +4723,9 @@ fn apply_plain_catch_handlers( emit_capture_save_and_branch( module, func_id, - runtime, - memory, cap_seq_ids[j], - &arm_slots[j + 1], - &arm_spill_locals[j + 1], + region.active_arm, + &arm_states[j + 1], in_catch_local, catch_region_id_local, catch_region_id, @@ -4925,11 +4762,9 @@ fn apply_plain_catch_handlers( emit_capture_save_and_branch( module, func_id, - runtime, - memory, outer_seq_id, - &arm_slots[0], - &arm_spill_locals[0], + region.active_arm, + &arm_states[0], in_catch_local, catch_region_id_local, catch_region_id, @@ -4945,33 +4780,28 @@ fn apply_plain_catch_handlers( let parent_instrs = &mut local.block_mut(parent_seq).instrs; let tt_idx = parent_instrs .iter() - .position(|(i, _)| matches!(i, Instr::TryTable(tt) if tt.seq == *body_seq)) + .position(|(i, _)| matches!(i, Instr::TryTable(tt) if tt.seq == body_seq)) .expect("try_table not found in its parent (B1 stage 2 emission)"); parent_instrs[tt_idx].0 = Instr::Block(Block { seq: outer_seq_id }); } } - - regions } /// Emit the capture-block "tail" for a single plain-catch arm: at the /// point where this is invoked, `tag.params()` are on the operand /// stack. The emitted sequence: /// -/// 1. Spill operands to per-arm locals (top-of-stack first). -/// 2. Save tuple (arm_id, op_0, ..., op_M-1) to memory at -/// `b1_scratch_base + slot.scratch_offset`. +/// 1. Spill operands to per-arm frame locals (top-of-stack first). +/// 2. Set the region's frame-backed active arm. /// 3. Set `in_catch_local = 1`, `catch_region_id_local = region_id`. /// 4. Re-push operands (declaration order). -/// 5. `br slot.arm.label` (original handler). +/// 5. `br arm.label` (original handler). fn emit_capture_save_and_branch( module: &mut Module, func_id: FunctionId, - runtime: &Runtime, - memory: MemoryId, cap_seq_id: InstrSeqId, - slot: &PlainCatchArmSlot, - spills: &[LocalId], + active_arm: LocalId, + arm: &PlainCatchArmState, in_catch_local: LocalId, catch_region_id_local: LocalId, catch_region_id: u32, @@ -4982,44 +4812,23 @@ fn emit_capture_save_and_branch( // 1. Spill operands. Operands were declared L-to-R but appear on // the stack with the LAST one on top — so we spill in reverse // declaration order: spills[M-1] first, then [M-2], ..., [0]. - for i in (0..spills.len()).rev() { - push_instr(s, Instr::LocalSet(LocalSet { local: spills[i] })); + for i in (0..arm.operand_locals.len()).rev() { + push_instr( + s, + Instr::LocalSet(LocalSet { + local: arm.operand_locals[i], + }), + ); } - // 2. Save arm_id at offset +0 of this arm's scratch tuple. The - // absolute address is `*(buf_global) + b1_scratch_base + - // scratch_offset`. We fold the constant offset into the - // MemArg.offset and push `global.get $buf_global` as the - // address. - let scratch_off = (runtime.b1_scratch_base + slot.scratch_offset) as u64; - push_instr( - s, - Instr::GlobalGet(GlobalGet { - global: runtime.buf_global, - }), - ); + // 2. Record which arm owns the operand locals for this activation. push_instr( s, Instr::Const(Const { - value: Value::I32(slot.arm.arm_idx as i32), + value: Value::I32(arm.arm.arm_idx as i32), }), ); - push_instr(s, store_i32(memory, scratch_off)); - - // 2b. Save operands at offset +4 + cumulative. - let mut cur_off: u32 = 4; - for (i, &ty) in slot.arm.operand_tys.iter().enumerate() { - let abs_off = scratch_off + cur_off as u64; - push_instr( - s, - Instr::GlobalGet(GlobalGet { - global: runtime.buf_global, - }), - ); - push_instr(s, Instr::LocalGet(LocalGet { local: spills[i] })); - push_instr(s, store_scalar(memory, ty, abs_off)); - cur_off += scalar_size(ty); - } + push_instr(s, Instr::LocalSet(LocalSet { local: active_arm })); // 3. Set flags. push_instr( @@ -5048,15 +4857,15 @@ fn emit_capture_save_and_branch( ); // 4. Re-push operands in declaration order. - for &spill in spills { - push_instr(s, Instr::LocalGet(LocalGet { local: spill })); + for &operand in &arm.operand_locals { + push_instr(s, Instr::LocalGet(LocalGet { local: operand })); } // 5. Branch to original handler. push_instr( s, Instr::Br(Br { - block: slot.arm.label, + block: arm.arm.label, }), ); } @@ -5294,7 +5103,7 @@ fn instrument_one_function_trampoline_dispatch( _aux_tables: &AuxTables, _ref_plan: &[RefLocalSlot], _catch_plan: &[CatchRegionPlan], - _b1_slots: &[(InstrSeqId, Vec)], + _plain_catches: &[(InstrSeqId, Vec)], ) { unimplemented!( "trampoline emission lands in sub-commits 2.4-2.6 of the mega-PR; \ @@ -6094,7 +5903,7 @@ fn instrument_one_function_nested_switch( aux_tables: &AuxTables, ref_plan: &[RefLocalSlot], catch_plan: &[CatchRegionPlan], - b1_slots: &[(InstrSeqId, Vec)], + plain_catches: &[(InstrSeqId, Vec)], ) { // Pre-existing user locals. let all_user_locals = collect_user_locals(module, func_id); @@ -6121,7 +5930,7 @@ fn instrument_one_function_nested_switch( aux_tables, ref_plan, catch_plan, - b1_slots, + plain_catches, ); return; } @@ -6227,6 +6036,8 @@ fn instrument_one_function_nested_switch( carryover_spills.insert(site.call_idx, spills); } + let plain_catch_state = allocate_plain_catch_state(module, plain_catches); + // Combined scalar locals for the function frame: existing user // scalars + frame-backed per-call arg-spill locals (in call_idx // order) + per-call carryover-spill locals (in call_idx order; @@ -6251,9 +6062,10 @@ fn instrument_one_function_nested_switch( frame_scalars.push((lid, ty)); } } + append_plain_catch_frame_scalars(&mut frame_scalars, &plain_catch_state); // Synthetic locals. - let catch_state_locals = if catch_plan.is_empty() && b1_slots.is_empty() { + let catch_state_locals = if catch_plan.is_empty() && plain_catches.is_empty() { None } else { Some(CatchStateLocals { @@ -6425,7 +6237,8 @@ fn instrument_one_function_nested_switch( // Plan catch handlers (Phase 6d). These remain dead code for the // nested transform's MVP (no fork-from-catch), but the plumbing is // preserved for ref-typed exnref locals that still round-trip. - let catch_handlers = plan_catch_ref_handlers(module, func_id, catch_plan, aux_tables); + let catch_handlers = + plan_catch_ref_handlers(module, func_id, catch_plan, aux_tables, &plain_catch_state); let memory = first_memory(module); let ptr_ty = runtime.buf_type; @@ -6433,7 +6246,7 @@ fn instrument_one_function_nested_switch( // Phase 6c rewind-throw stubs (still emitted for try_table bodies // without fork-path calls — preserves the exnref serialization // path). Extended by B1 Stage 2 Task 2.3 with plain-catch arm - // dispatch when `b1_slots` lists arms for the region. + // dispatch when `plain_catches` lists arms for the region. if !catch_plan.is_empty() && aux_tables.exnref.is_some() { let catch_state = catch_state_locals.expect("exnref catch plan requires catch-state locals"); @@ -6444,7 +6257,7 @@ fn instrument_one_function_nested_switch( catch_state.catch_region_id, aux_tables, catch_plan, - b1_slots, + &plain_catch_state, ); } @@ -6660,14 +6473,13 @@ fn instrument_one_function_nested_switch( apply_plain_catch_handlers( module, func_id, - runtime, catch_state.catch_region_id, - b1_slots, + &plain_catch_state, catch_plan, &catch_handlers, ); } else { - debug_assert!(b1_slots.is_empty()); + debug_assert!(plain_catches.is_empty()); } } diff --git a/crates/fork-instrument/src/lib.rs b/crates/fork-instrument/src/lib.rs index a2e43a9ca4..a8014f80f9 100644 --- a/crates/fork-instrument/src/lib.rs +++ b/crates/fork-instrument/src/lib.rs @@ -127,40 +127,30 @@ pub fn instrument(input: &[u8], opts: &Options) -> Result> { // exported ABI is stable regardless of whether any caller was // actually rewritten. // - // Stage 1 (B1): reserve plain-catch scratch space in the save - // buffer. `total_bytes` is 0 when no fork-path function has a - // plain catch — preserves byte-identical behavior to pre-B1 - // for all currently-shipping ports. The plan must be computed - // *before* `inject_runtime` because the resulting size shifts - // `frames_start_offset`, which gets baked into the unwind_begin - // body as a constant. Filter to local functions and sort to - // match the determinism of `instrument_functions`'s target walk - // — Stage 2 reads `B1ScratchPlan.per_function[fid]` and the - // per-function scratch_offset values must be stable across runs - // for byte-reproducible builds. We do NOT also filter - // `runtime_funcs` here because they don't exist yet at this - // point in the pipeline (they're added by `inject_runtime` on - // the next line). + // Discover supported plain-catch regions before injecting the runtime. + // The plan contains only static tag/label/type metadata; activation state + // is allocated later as ordinary frame-backed function locals. Sort the + // targets to keep local allocation and emitted bytes deterministic. let mut fork_path_targets: Vec = fork_path .iter() .copied() .filter(|id| matches!(module.funcs.get(*id).kind, walrus::FunctionKind::Local(_))) .collect(); fork_path_targets.sort(); - let b1_plan = instrument::plan_b1_scratch(&module, &fork_path_targets); + let plain_catch_plan = instrument::plan_plain_catches(&module, &fork_path_targets); // Only modules with the configured fork seed need linked-frame imports. // Runtime exports and metadata remain stable for no-seed modules, but // adding unused host imports would make an otherwise inert side module // impossible to instantiate through the dynamic linker. let runtime = if entry.is_some() { - runtime::inject_linked_runtime(&mut module, b1_plan.total_bytes) + runtime::inject_linked_runtime(&mut module) } else { - runtime::inject_runtime(&mut module, b1_plan.total_bytes) + runtime::inject_runtime(&mut module) }; // Phase 4b: structural wrap of each fork-path function's body. // No-op when `fork_path` is empty (module doesn't use fork). - instrument::instrument_functions(&mut module, &runtime, &fork_path, &b1_plan); + instrument::instrument_functions(&mut module, &runtime, &fork_path, &plain_catch_plan); loop { let existing = module diff --git a/crates/fork-instrument/src/linked_frames.rs b/crates/fork-instrument/src/linked_frames.rs index f60ea58c52..534de03d6d 100644 --- a/crates/fork-instrument/src/linked_frames.rs +++ b/crates/fork-instrument/src/linked_frames.rs @@ -200,7 +200,7 @@ pub struct FrameFormatDescriptor { pub chunk_header_size: u32, pub node_header_size: u32, /// Bytes at the start of the root chunk's payload reserved for the - /// instrumented runtime header, saved globals, and exception scratch. + /// instrumented runtime header, saved globals, and abort selector. pub fixed_prefix_size: u32, } diff --git a/crates/fork-instrument/src/runtime.rs b/crates/fork-instrument/src/runtime.rs index 950ec2e5d7..a9de7bd304 100644 --- a/crates/fork-instrument/src/runtime.rs +++ b/crates/fork-instrument/src/runtime.rs @@ -26,23 +26,19 @@ //! auxiliary tables (Phase 4f); this phase skips them. //! //! Module-prefix layout (all offsets byte-exact; `P` is pointer width — -//! 4 bytes on wasm32, 8 on wasm64; `B` is the B1 plain-catch scratch -//! reservation, 0 when no fork-path function has a plain catch): +//! 4 bytes on wasm32 and 8 on wasm64): //! //! ```text //! +0 P active_frame Current frame payload during save/replay //! +P P reserved Reserved pointer word //! +2P N saved_globals[] Mutable scalar globals, declaration order -//! +2P+N B b1_scratch[] Per-arm scratch tuples (Stage 1 B1) -//! +2P+N+B 16 abort_selector Live-frame call-site selector +//! +2P+N 16 abort_selector Live-frame call-site selector //! ``` //! //! `frames_start_offset` in [`Runtime`] exposes the abort-selector offset -//! `2P + N + B`; `fixed_prefix_size` includes the following 16 bytes. In the +//! `2P + N`; `fixed_prefix_size` includes the following 16 bytes. In the //! linked runtime, frame payloads live after per-node headers in host-managed //! chunks rather than directly after this prefix. -//! `b1_scratch_base` exposes `2P + N` (== `frames_start_offset` when -//! `B == 0`) and `b1_scratch_size` exposes `B` (rounded up to 8). use walrus::{ ConstExpr, FunctionBuilder, FunctionId, GlobalId, InstrSeqBuilder, MemoryId, Module, ValType, @@ -112,9 +108,7 @@ pub struct Runtime { /// and `wpk_fork_rewind_begin` restores. Declaration order. pub saved_globals: Vec, - /// Offset of the linked runtime's abort selector. Includes any space - /// reserved for B1's plain-catch scratch area - /// (see `b1_scratch_base` / `b1_scratch_size`). + /// Offset of the linked runtime's abort selector. /// `wpk_fork_unwind_begin` adds the module-buffer base to this value for /// the initial active-frame word. Linked postambles replace that word with /// the payload returned by the reserve hook before writing any frame data. @@ -124,14 +118,6 @@ pub struct Runtime { /// area after `frames_start_offset` for the still-live activation that /// reverses a failed partial unwind. pub fixed_prefix_size: u32, - - /// Stage 1 (B1): byte offset at which the plain-catch scratch - /// area begins. Equals `2P + N` (header + saved_globals). - pub b1_scratch_base: u32, - /// Stage 1 (B1): bytes reserved for the plain-catch scratch area. - /// Zero when no fork-path function in the module has a plain catch. - /// `b1_scratch_base + b1_scratch_size == frames_start_offset`. - pub b1_scratch_size: u32, } /// Return the pointer type appropriate for the module's primary @@ -175,33 +161,15 @@ fn zero_const(ptr_ty: ValType) -> ConstExpr { /// the module has linear memory — the per-global save/restore /// machinery in `wpk_fork_unwind_begin` / `wpk_fork_rewind_begin`. /// -/// `b1_scratch_size` is the number of bytes B1 (Stage 1 plain-catch -/// scratch area) needs reserved between `saved_globals` and -/// `frame data` in the save buffer. It is zero when no fork-path -/// function in the module has a plain catch — preserving byte-identical -/// behavior to pre-B1 for modules that don't exercise the feature. -/// The value is rounded up to 8-byte alignment internally so frame -/// data starts aligned regardless of saved-globals payload size. -/// -/// **Why a parameter and not a setter:** `frames_start_offset` gets -/// baked into `wpk_fork_unwind_begin`'s body as a constant during this -/// call (see step 3 in [`emit_unwind_begin`]). Shifting the offset -/// after `inject_runtime` returns would silently desync the const and -/// the host-visible offset. Computing the B1 plan first and passing -/// the size in keeps everything consistent. -pub fn inject_runtime(module: &mut Module, b1_scratch_size: u32) -> Runtime { - inject_runtime_with_frame_storage(module, b1_scratch_size, false) +pub fn inject_runtime(module: &mut Module) -> Runtime { + inject_runtime_with_frame_storage(module, false) } -pub fn inject_linked_runtime(module: &mut Module, b1_scratch_size: u32) -> Runtime { - inject_runtime_with_frame_storage(module, b1_scratch_size, true) +pub fn inject_linked_runtime(module: &mut Module) -> Runtime { + inject_runtime_with_frame_storage(module, true) } -fn inject_runtime_with_frame_storage( - module: &mut Module, - b1_scratch_size: u32, - linked_frames: bool, -) -> Runtime { +fn inject_runtime_with_frame_storage(module: &mut Module, linked_frames: bool) -> Runtime { let ptr_ty = ptr_type(module); let memory = module.memories.iter().next().map(|m| m.id()); @@ -241,30 +209,16 @@ fn inject_runtime_with_frame_storage( }); next_off += scalar_size(g.ty); } - // B1 plain-catch scratch area sits between saved_globals and - // frame data. When `b1_scratch_size == 0` (the common case for - // modules without plain-catch fork) this is a no-op and - // `frames_start_offset` is byte-identical to pre-B1. - let b1_scratch_base = next_off; - let aligned_b1_size = align_up_8(b1_scratch_size); - let frames_start_offset = b1_scratch_base + aligned_b1_size; + // Plain-catch payloads are ordinary activation locals and therefore + // serialize inside each function frame. The module prefix owns only + // runtime-global state; no code may use it as NORMAL-state scratch. + let frames_start_offset = next_off; let fixed_prefix_size = frames_start_offset + if linked_frames { ABORT_SELECTOR_SIZE } else { 0 }; - // Invariant: `b1_scratch_base + b1_scratch_size == frames_start_offset` - // holds by construction here — `frames_start_offset` is defined as - // the sum on the previous line, and `b1_scratch_size` is stored as - // `aligned_b1_size`. This is documented in the `Runtime` struct doc - // and the leading buffer-layout comment block; we don't enforce it - // via debug_assert! because any check using the same locals is - // tautological, and a stronger check (e.g., 8-alignment of - // `frames_start_offset`) doesn't hold for all currently-shipping - // modules — the pre-B1 code already permits non-aligned values - // because wasm tolerates unaligned i64 stores. - // --- Runtime globals (state + buf) --- let state_global = module.globals.add_local( ValType::I32, @@ -304,7 +258,7 @@ fn inject_runtime_with_frame_storage( &saved_globals, frames_start_offset, ); - let unwind_end = emit_end_fn(module, state_global); + let unwind_end = emit_end_fn(module, state_global, buf_global, ptr_ty); let rewind_begin = emit_rewind_begin( module, ptr_ty, @@ -314,7 +268,7 @@ fn inject_runtime_with_frame_storage( &saved_globals, STATE_REWINDING, ); - let rewind_end = emit_end_fn(module, state_global); + let rewind_end = emit_end_fn(module, state_global, buf_global, ptr_ty); let abort_begin = emit_rewind_begin( module, ptr_ty, @@ -324,7 +278,7 @@ fn inject_runtime_with_frame_storage( &saved_globals, STATE_ABORT_UNWINDING, ); - let abort_end = emit_end_fn(module, state_global); + let abort_end = emit_end_fn(module, state_global, buf_global, ptr_ty); let state = emit_state_fn(module, state_global); // --- Exports --- @@ -363,8 +317,6 @@ fn inject_runtime_with_frame_storage( saved_globals, frames_start_offset, fixed_prefix_size, - b1_scratch_base, - b1_scratch_size: aligned_b1_size, } } @@ -540,20 +492,31 @@ fn natural_align(ty: ValType) -> u32 { scalar_size(ty) } -/// Round `x` up to the nearest 8-byte boundary. Mirrors the helper -/// in `instrument.rs`; kept private here to avoid widening visibility -/// for a one-line helper. A future cleanup pass can consolidate. -fn align_up_8(x: u32) -> u32 { - (x + 7) & !7u32 -} - -/// Emit a `() -> ()` function that resets state to NORMAL. -fn emit_end_fn(module: &mut Module, state_global: GlobalId) -> FunctionId { +/// Emit a `() -> ()` function that releases active-buffer ownership and +/// resets state to NORMAL. +fn emit_end_fn( + module: &mut Module, + state_global: GlobalId, + buf_global: GlobalId, + ptr_ty: ValType, +) -> FunctionId { let mut builder = FunctionBuilder::new(&mut module.types, &[], &[]); - builder - .func_body() - .i32_const(STATE_NORMAL) - .global_set(state_global); + let mut body = builder.func_body(); + // WHY: a continuation mapping stops belonging to this module at every end + // transition. Clear the alias to released/reusable storage before + // publishing NORMAL; correctness still comes from having no NORMAL-state + // buffer accesses, because linear-memory address zero is itself valid. + match ptr_ty { + ValType::I32 => { + body.i32_const(0); + } + ValType::I64 => { + body.i64_const(0); + } + other => unreachable!("unsupported ptr_ty: {other:?}"), + } + body.global_set(buf_global); + body.i32_const(STATE_NORMAL).global_set(state_global); builder.finish(vec![], &mut module.funcs) } diff --git a/crates/fork-instrument/tests/coverage_wat.rs b/crates/fork-instrument/tests/coverage_wat.rs index 123aabbdad..4d4d45d9da 100644 --- a/crates/fork-instrument/tests/coverage_wat.rs +++ b/crates/fork-instrument/tests/coverage_wat.rs @@ -216,16 +216,14 @@ fn f_04_struct_ref_on_fork_path_rejects_with_diagnostic() { // C-08 / C-09: ref-typed catch operands (A4 territory) // --------------------------------------------------------------------- // -// Per the unsupported-cases review doc, fork-path functions whose -// plain-catch arms carry ref-typed operands (funcref / externref) -// are CARVED OUT of the fork-path set at instrument time (via -// `B1ScratchPlan::b2_carveout`). A future A4 implementation would -// extend per-arm aux-table spilling to support these. The current -// behavior: fork-instrument processes the module without panic; -// the function with the ref-typed catch arm doesn't get -// instrumented (which is correct: it's not actually on the -// fork-call path if the carve-out is right, OR it's a -// surprise-fork-path that the carve-out preserves safety for). +// Per the unsupported-cases review doc, a function whose plain-catch +// arms carry ref-typed operands (funcref / externref) is excluded from +// plain-catch replay support via `PlainCatchPlan::b2_carveout`. The +// function can still be instrumented for other fork sites; a fork reached +// from the affected handler remains explicitly unsupported. A future A4 +// implementation would extend per-arm auxiliary storage to support these +// operands. The current coverage proves the tool accepts the Wasm shape +// without trying to serialize references as scalars. #[test] fn c_08_funcref_catch_operand_does_not_panic() { @@ -248,8 +246,8 @@ fn c_08_funcref_catch_operand_does_not_panic() { "#; match wat::parse_str(wat) { Ok(input) => { - // Should NOT panic. fork-instrument carves out the - // function or accepts it gracefully. + // Should NOT panic. The instrumenter excludes this function + // from plain-catch replay without serializing the reference. let _ = instrument(&input, &Options::default()) .expect("fork-instrument should not error on funcref catch arm"); } diff --git a/crates/fork-instrument/tests/instrument.rs b/crates/fork-instrument/tests/instrument.rs index 328a34f123..182153b96a 100644 --- a/crates/fork-instrument/tests/instrument.rs +++ b/crates/fork-instrument/tests/instrument.rs @@ -20,7 +20,7 @@ use std::collections::HashSet; use fork_instrument::runtime::names as runtime_names; use fork_instrument::{Options, instrument}; use walrus::{ - ExportItem, FunctionId, FunctionKind, LocalFunction, Module, + ExportItem, FunctionId, FunctionKind, LocalFunction, Module, ValType, ir::{self, Instr, InstrSeqId}, }; @@ -528,7 +528,7 @@ fn multivalue_return_wraps_and_validates() { #[test] fn instrument_functions_returns_rewritten_set() { use fork_instrument::call_graph; - use fork_instrument::instrument::{B1ScratchPlan, instrument_functions}; + use fork_instrument::instrument::{PlainCatchPlan, instrument_functions}; use fork_instrument::runtime::inject_runtime; let bytes = wat::parse_str(FIXTURE_TRANSITIVE).unwrap(); @@ -537,8 +537,8 @@ fn instrument_functions_returns_rewritten_set() { let seed = call_graph::find_import_func(&module, "kernel.kernel_fork").expect("seed import present"); let fork_path = call_graph::reaching_closure(&module, seed); - let runtime = inject_runtime(&mut module, 0); - let b1_plan = B1ScratchPlan::default(); + let runtime = inject_runtime(&mut module); + let b1_plan = PlainCatchPlan::default(); let rewritten = instrument_functions(&mut module, &runtime, &fork_path, &b1_plan); let names: HashSet = rewritten @@ -1765,11 +1765,10 @@ fn nested_seqs_in_test(instr: &Instr) -> Vec { } } -// --- B1 Stage 1 Task 1.2 — plan_b1_scratch tests ---------------------- +// --- Plain-catch static planning -------------------------------------- #[test] -fn b1_scratch_plan_empty_targets_is_zero_sized() { - // Self-review: empty target list → empty plan, zero bytes. +fn plain_catch_plan_empty_targets_has_no_functions() { let wat = r#" (module (func $caller (export "caller") (result i32) i32.const 0) @@ -1777,8 +1776,7 @@ fn b1_scratch_plan_empty_targets_is_zero_sized() { "#; let bytes = parse_wat(wat); let module = walrus::Module::from_buffer(&bytes).unwrap(); - let plan = fork_instrument::instrument::plan_b1_scratch(&module, &[]); - assert_eq!(plan.total_bytes, 0, "no targets → zero scratch bytes"); + let plan = fork_instrument::instrument::plan_plain_catches(&module, &[]); assert!( plan.per_function.is_empty(), "no targets → empty per_function map" @@ -1786,8 +1784,7 @@ fn b1_scratch_plan_empty_targets_is_zero_sized() { } #[test] -fn b1_scratch_plan_empty_payload_arm_is_4_bytes() { - // Tag with no payload → tuple_size = 4 (just arm_id). +fn plain_catch_plan_preserves_empty_payload_arm() { let wat = r#" (module (import "kernel" "kernel_fork" (func $fork (result i32))) @@ -1802,7 +1799,7 @@ fn b1_scratch_plan_empty_payload_arm_is_4_bytes() { let bytes = parse_wat(wat); let module = walrus::Module::from_buffer(&bytes).unwrap(); let caller = func_by_name(&module, "caller"); - let plan = fork_instrument::instrument::plan_b1_scratch(&module, &[caller]); + let plan = fork_instrument::instrument::plan_plain_catches(&module, &[caller]); assert_eq!( plan.per_function.len(), 1, @@ -1810,16 +1807,14 @@ fn b1_scratch_plan_empty_payload_arm_is_4_bytes() { ); let per_func = &plan.per_function[&caller]; assert_eq!(per_func.len(), 1, "one try_table with plain-catch arms"); - let (_body_seq, slots) = &per_func[0]; - assert_eq!(slots.len(), 1, "one plain-catch arm"); - assert_eq!(slots[0].tuple_size, 4, "no payload → arm_id only"); - assert_eq!(slots[0].scratch_offset, 0, "first slot at offset 0"); - assert_eq!(plan.total_bytes, 8, "rounded up to 8-byte alignment"); + let (_body_seq, arms) = &per_func[0]; + assert_eq!(arms.len(), 1, "one plain-catch arm"); + assert_eq!(arms[0].arm_idx, 0); + assert!(arms[0].operand_tys.is_empty()); } #[test] -fn b1_scratch_plan_i32_payload_arm_is_8_bytes() { - // Tag with i32 payload → tuple_size = 4 + 4 = 8. +fn plain_catch_plan_preserves_i32_payload_type() { // // Catch label semantics: branching to a `block` carries the // block's RESULT types (forward-branch arity), so a tag with a @@ -1842,20 +1837,14 @@ fn b1_scratch_plan_i32_payload_arm_is_8_bytes() { let bytes = parse_wat(wat); let module = walrus::Module::from_buffer(&bytes).unwrap(); let caller = func_by_name(&module, "caller"); - let plan = fork_instrument::instrument::plan_b1_scratch(&module, &[caller]); + let plan = fork_instrument::instrument::plan_plain_catches(&module, &[caller]); let per_func = &plan.per_function[&caller]; - let (_, slots) = &per_func[0]; - assert_eq!(slots[0].tuple_size, 8, "arm_id (4) + i32 payload (4)"); - assert_eq!(slots[0].scratch_offset, 0); - assert_eq!(plan.total_bytes, 8); + let (_, arms) = &per_func[0]; + assert_eq!(arms[0].operand_tys, vec![ValType::I32]); } #[test] -fn b1_scratch_plan_two_arms_align_to_8_bytes_each() { - // Two arms in one function, payload sizes 0 and 8. - // First arm: tuple_size=4 → aligned start 0, end 4 - // Second arm: aligned to 8 → start 8, end 8 + (4 + 8) = 20 - // total_bytes rounds up to 24. +fn plain_catch_plan_preserves_regions_in_source_order() { let wat = r#" (module (import "kernel" "kernel_fork" (func $fork (result i32))) @@ -1877,30 +1866,17 @@ fn b1_scratch_plan_two_arms_align_to_8_bytes_each() { let bytes = parse_wat(wat); let module = walrus::Module::from_buffer(&bytes).unwrap(); let caller = func_by_name(&module, "caller"); - let plan = fork_instrument::instrument::plan_b1_scratch(&module, &[caller]); + let plan = fork_instrument::instrument::plan_plain_catches(&module, &[caller]); let per_func = &plan.per_function[&caller]; assert_eq!(per_func.len(), 2, "two try_tables"); - let (_, slots_a) = &per_func[0]; - let (_, slots_b) = &per_func[1]; - assert_eq!(slots_a[0].scratch_offset, 0); - assert_eq!(slots_a[0].tuple_size, 4); - assert_eq!( - slots_b[0].scratch_offset, 8, - "second arm aligned to next 8-byte boundary" - ); - assert_eq!(slots_b[0].tuple_size, 12, "arm_id (4) + i64 (8)"); - assert_eq!( - plan.total_bytes, 24, - "rounded up from 20 to next 8-byte boundary" - ); + let (_, arms_a) = &per_func[0]; + let (_, arms_b) = &per_func[1]; + assert!(arms_a[0].operand_tys.is_empty()); + assert_eq!(arms_b[0].operand_tys, vec![ValType::I64]); } #[test] -fn b1_scratch_plan_arm_with_8_aligned_tuple_needs_no_padding() { - // Two arms, both with i32 payload (tuple_size = 8 each). - // Arm 0: aligned=0, tuple=8, cursor=8. - // Arm 1: aligned=align_up_8(8)=8 (no padding), tuple=8, cursor=8+8=16. - // total_bytes = align_up_8(16) = 16 (no spurious padding). +fn plain_catch_plan_preserves_same_typed_arms_across_regions() { let wat = r#" (module (import "kernel" "kernel_fork" (func $fork (result i32))) @@ -1925,25 +1901,17 @@ fn b1_scratch_plan_arm_with_8_aligned_tuple_needs_no_padding() { let bytes = parse_wat(wat); let module = walrus::Module::from_buffer(&bytes).unwrap(); let caller = func_by_name(&module, "caller"); - let plan = fork_instrument::instrument::plan_b1_scratch(&module, &[caller]); + let plan = fork_instrument::instrument::plan_plain_catches(&module, &[caller]); let per_func = &plan.per_function[&caller]; assert_eq!(per_func.len(), 2, "two try_tables"); - let (_, slots_a) = &per_func[0]; - let (_, slots_b) = &per_func[1]; - assert_eq!(slots_a[0].scratch_offset, 0); - assert_eq!(slots_a[0].tuple_size, 8); - assert_eq!( - slots_b[0].scratch_offset, 8, - "8-aligned tuple end means next arm needs no padding" - ); - assert_eq!(slots_b[0].tuple_size, 8); - assert_eq!(plan.total_bytes, 16, "no spurious padding inserted"); + let (_, arms_a) = &per_func[0]; + let (_, arms_b) = &per_func[1]; + assert_eq!(arms_a[0].operand_tys, vec![ValType::I32]); + assert_eq!(arms_b[0].operand_tys, vec![ValType::I32]); } #[test] -fn b1_scratch_plan_handles_f32_f64_operand_sizes() { - // tag with (param f32 f64) → tuple = 4 (arm_id) + 4 (f32) + 8 (f64) = 16. - // Verifies scalar_size correctly maps f32→4, f64→8 through the planner. +fn plain_catch_plan_preserves_f32_f64_operand_types() { // // Catch label semantics (mirroring existing i32-payload test): branching // to a `(block $h (result f32 f64))` carries the block's RESULT types, @@ -1969,21 +1937,16 @@ fn b1_scratch_plan_handles_f32_f64_operand_sizes() { let bytes = parse_wat(wat); let module = walrus::Module::from_buffer(&bytes).unwrap(); let caller = func_by_name(&module, "caller"); - let plan = fork_instrument::instrument::plan_b1_scratch(&module, &[caller]); + let plan = fork_instrument::instrument::plan_plain_catches(&module, &[caller]); let per_func = &plan.per_function[&caller]; - let (_, slots) = &per_func[0]; - assert_eq!( - slots[0].tuple_size, 16, - "arm_id (4) + f32 (4) + f64 (8) = 16" - ); - assert_eq!(slots[0].scratch_offset, 0); - assert_eq!(plan.total_bytes, 16); + let (_, arms) = &per_func[0]; + assert_eq!(arms[0].operand_tys, vec![ValType::F32, ValType::F64]); } // --- B1 Stage 2 Task 2.1 — operand-type carve-out tests ---------------- #[test] -fn b1_scratch_plan_ref_operand_function_is_carved_out() { +fn plain_catch_plan_ref_operand_function_is_carved_out() { // A try_table with a tag whose payload includes externref. // The function should land in b2_carveout, NOT in per_function. // @@ -2008,7 +1971,7 @@ fn b1_scratch_plan_ref_operand_function_is_carved_out() { let bytes = parse_wat(wat); let module = walrus::Module::from_buffer(&bytes).unwrap(); let caller = func_by_name(&module, "caller"); - let plan = fork_instrument::instrument::plan_b1_scratch(&module, &[caller]); + let plan = fork_instrument::instrument::plan_plain_catches(&module, &[caller]); assert!( !plan.per_function.contains_key(&caller), "carved-out function must not appear in per_function" @@ -2017,14 +1980,10 @@ fn b1_scratch_plan_ref_operand_function_is_carved_out() { plan.b2_carveout.contains(&caller), "carved-out function must be in b2_carveout" ); - assert_eq!( - plan.total_bytes, 0, - "no scratch allocated for carved-out function" - ); } #[test] -fn b1_scratch_plan_mixed_ref_and_scalar_arms_carves_whole_function() { +fn plain_catch_plan_mixed_ref_and_scalar_arms_carves_whole_function() { // A function with two try_tables: one with i32 payload (supported), // one with externref (unsupported). The whole function gets carved // out because we don't selectively drop arms — Task 2.3's rewind @@ -2053,7 +2012,7 @@ fn b1_scratch_plan_mixed_ref_and_scalar_arms_carves_whole_function() { let bytes = parse_wat(wat); let module = walrus::Module::from_buffer(&bytes).unwrap(); let caller = func_by_name(&module, "caller"); - let plan = fork_instrument::instrument::plan_b1_scratch(&module, &[caller]); + let plan = fork_instrument::instrument::plan_plain_catches(&module, &[caller]); assert!( plan.b2_carveout.contains(&caller), "carve-out must include functions with mixed scalar+ref arms" @@ -2063,14 +2022,10 @@ fn b1_scratch_plan_mixed_ref_and_scalar_arms_carves_whole_function() { "carved-out function must not appear in per_function even \ though one arm is otherwise supported" ); - assert_eq!( - plan.total_bytes, 0, - "carved-out functions allocate no scratch" - ); } #[test] -fn b1_scratch_plan_scalar_only_function_is_not_carved_out() { +fn plain_catch_plan_scalar_only_function_is_not_carved_out() { // Sanity: the existing scalar-only fixture must NOT be carved out. // Mirrors the scalar tests above to ensure carve-out is gated // strictly on ref-typed operands and doesn't accidentally trip @@ -2089,7 +2044,7 @@ fn b1_scratch_plan_scalar_only_function_is_not_carved_out() { let bytes = parse_wat(wat); let module = walrus::Module::from_buffer(&bytes).unwrap(); let caller = func_by_name(&module, "caller"); - let plan = fork_instrument::instrument::plan_b1_scratch(&module, &[caller]); + let plan = fork_instrument::instrument::plan_plain_catches(&module, &[caller]); assert!( !plan.b2_carveout.contains(&caller), "scalar-only function must not be in b2_carveout" @@ -2101,7 +2056,7 @@ fn b1_scratch_plan_scalar_only_function_is_not_carved_out() { } #[test] -fn b1_scratch_plan_multi_target_plain_catch_carved_out() { +fn plain_catch_plan_multi_target_plain_catch_carved_out() { // Two arms in one try_table, pointing at different labels. // Should be carved out (Task 2.4 conservative guard: multi-target // plain-catch fork has not been verified end-to-end). @@ -2121,7 +2076,7 @@ fn b1_scratch_plan_multi_target_plain_catch_carved_out() { let bytes = parse_wat(wat); let module = walrus::Module::from_buffer(&bytes).unwrap(); let caller = func_by_name(&module, "caller"); - let plan = fork_instrument::instrument::plan_b1_scratch(&module, &[caller]); + let plan = fork_instrument::instrument::plan_plain_catches(&module, &[caller]); assert!( plan.b2_carveout.contains(&caller), "multi-target try_table should be carved out" @@ -2133,7 +2088,7 @@ fn b1_scratch_plan_multi_target_plain_catch_carved_out() { } #[test] -fn b1_scratch_plan_single_target_multi_arm_is_supported() { +fn plain_catch_plan_single_target_multi_arm_is_supported() { // Two arms in one try_table, both pointing at the SAME label. // Should NOT be carved out (this is the supported multi-arm case // — Task 2.4's guard only triggers when arms diverge). @@ -2152,7 +2107,7 @@ fn b1_scratch_plan_single_target_multi_arm_is_supported() { let bytes = parse_wat(wat); let module = walrus::Module::from_buffer(&bytes).unwrap(); let caller = func_by_name(&module, "caller"); - let plan = fork_instrument::instrument::plan_b1_scratch(&module, &[caller]); + let plan = fork_instrument::instrument::plan_plain_catches(&module, &[caller]); assert!( !plan.b2_carveout.contains(&caller), "single-target multi-arm should be supported" @@ -2169,11 +2124,9 @@ fn b1_scratch_plan_single_target_multi_arm_is_supported() { // ====================================================================== #[test] -fn b1_stage_1_module_without_plain_catch_validates_and_b1_size_zero() { - // A fork-using module with no plain catch should be byte-identical - // (instrumentation-wise) to pre-B1 — Stage 1 only adds bookkeeping; - // emission is unchanged. The standard wpk_fork_* exports must - // still be present. +fn plain_catch_plan_module_without_plain_catch_validates() { + // A fork-using module with no plain catch needs no activation-owned + // catch locals. The standard wpk_fork_* exports must still be present. let wat = r#" (module (import "kernel" "kernel_fork" (func $fork (result i32))) @@ -2186,17 +2139,15 @@ fn b1_stage_1_module_without_plain_catch_validates_and_b1_size_zero() { let module = Module::from_buffer(&bytes).unwrap(); assert!( module.exports.iter().any(|e| e.name == "wpk_fork_state"), - "wpk_fork_state export must remain after Stage 1" + "wpk_fork_state export must remain without plain catches" ); } #[test] -fn b1_stage_1_module_with_plain_catch_still_validates() { - // The plain-catch fixture from Tasks 1.1/1.2: B1 plan computes a - // non-zero scratch size, frames_start_offset shifts, but the - // emitted module still validates and exposes the standard - // wpk_fork_* exports. Behavioral assertions (parent forks → child - // resumes inside catch) come in Stage 2. +fn plain_catch_plan_module_validates_without_prefix_scratch() { + // Plain-catch state belongs to frame-backed locals, so discovering this + // catch does not reserve module-prefix scratch. The emitted module still + // validates and exposes the standard wpk_fork_* exports. let wat = r#" (module (import "kernel" "kernel_fork" (func $fork (result i32))) @@ -2213,7 +2164,7 @@ fn b1_stage_1_module_with_plain_catch_still_validates() { let module = Module::from_buffer(&bytes).unwrap(); assert!( module.exports.iter().any(|e| e.name == "wpk_fork_state"), - "wpk_fork_state export must remain after Stage 1" + "wpk_fork_state export must remain with plain catches" ); } @@ -2234,7 +2185,7 @@ fn collect_try_tables(f: &LocalFunction) -> Vec { } #[test] -fn b1_stage_2_plain_catch_arm_retargets_to_capture_block() { +fn b1_stage_2_plain_catch_arm_uses_frame_backed_state() { // After instrumentation, the original try_table's plain Catch // clause should point at an injected capture block, not at the // original handler label `$h`. The capture block contains the @@ -2279,33 +2230,23 @@ fn b1_stage_2_plain_catch_arm_retargets_to_capture_block() { "should be a plain Catch clause" ); - // 2. Byte-level (wasmprinter): the cap block must contain the B1 - // save sequence (i32.store of arm_id followed by local.set of - // in_catch / catch_region_id locals) and an outer `br` of the - // handler. We grep for the i32.store + local.set pattern that - // is unique to B1's emission. (Walrus drops these on re-parse - // because they're after the unconditional `br $b1_outer`.) + // 2. Byte-level (wasmprinter): active-arm is an ordinary scalar + // local and therefore round-trips through the function frame at + // the first offset after its 16-byte header. let printed = wasmprinter::print_bytes(&bytes).expect("wasmprinter"); let caller_section = extract_function_text(&printed, "caller"); assert!( caller_section.contains("try_table"), "caller must still have a try_table:\n{caller_section}" ); - // The save sequence: `i32.store offset=N` is unique to the B1 - // emission. (The frame-IO emissions use offset=0/4/8/12 only.) - // For the no-operand fixture, slot.scratch_offset = 0 within the - // scratch area, and runtime.b1_scratch_base is where the scratch - // starts (at 8 in this module: 2P=8 with no saved globals + B=8 - // rounded). We look for ANY of i32.store with offset >= 16 that - // follows a global.get $_wpk_fork_buf — the unique B1 pattern. - // Easier signature: the literal sequence "i32.const 0\n ... - // i32.store offset=8" stores arm_idx 0 at scratch offset 0 - // (absolute = b1_scratch_base = 8). The exact offset depends on - // saved globals; the only invariant is *some* store happens. assert!( - caller_section.contains("i32.store offset=8"), - "caller must contain i32.store at the B1 scratch arm_id offset \ - (= b1_scratch_base + 0 = 8 for this fixture):\n{caller_section}" + caller_section.contains("i32.store offset=16"), + "active-arm local must be serialized after the frame header:\n\ + {caller_section}" + ); + assert!( + caller_section.contains("i32.load offset=16"), + "active-arm local must be restored from its frame:\n{caller_section}" ); } @@ -2389,12 +2330,9 @@ fn b1_stage_2_b2_carveout_function_is_not_transformed() { (B1 must NOT have transformed it)" ); - // Byte-level: with the function in `b2_carveout`, the plan - // reserves zero scratch bytes for it — `B1ScratchPlan.total_bytes - // == 0` and `runtime.b1_scratch_size == 0`. We confirm via the - // direct planner API: a fresh plan over `[caller]` must list it - // in b2_carveout, not per_function. - let plan = fork_instrument::instrument::plan_b1_scratch(&module, &[caller]); + // The direct planner API must list the function in b2_carveout, + // not per_function. + let plan = fork_instrument::instrument::plan_plain_catches(&module, &[caller]); assert!( plan.b2_carveout.contains(&caller), "carved-out function (ref-typed catch operand) must be in \ @@ -2404,11 +2342,6 @@ fn b1_stage_2_b2_carveout_function_is_not_transformed() { !plan.per_function.contains_key(&caller), "carved-out function must NOT have a per_function entry" ); - assert_eq!( - plan.total_bytes, 0, - "no scratch reservation expected when only function is carved \ - out" - ); } #[test] @@ -2556,13 +2489,59 @@ fn b1_stage_2_rewind_stub_has_plain_catch_dispatch() { "rewind stub must contain a `throw $exn` for the plain-catch \ arm dispatch:\n{caller_section}" ); - // ref.is_null indicates the sentinel check (B1 selects between - // catch_ref and plain-catch paths based on whether the exnref - // stash slot is null). + // The mode decision must be frame-owned. Stale contents in the + // static exnref table cannot decide whether this activation took + // a plain or ref catch. assert!( - caller_section.contains("ref.is_null"), - "rewind stub must use ref.is_null on the exnref stash to \ - choose between catch_ref and plain-catch paths:\n{caller_section}" + !caller_section.contains("ref.is_null"), + "rewind stub must dispatch from frame-backed active_arm, not \ + exnref-table nullness:\n{caller_section}" + ); +} + +#[test] +fn mixed_plain_and_catch_ref_uses_frame_backed_arm_kind() { + let wat = r#" + (module + (import "kernel" "kernel_fork" (func $fork (result i32))) + (tag $plain (param i32)) + (tag $with_ref) + (func $caller (export "caller") (param $take_plain i32) (result i32) + (block $done (result i32) + (block $plain_handler (result i32) + (block $ref_handler (result exnref) + (try_table (result exnref) + (catch $plain $plain_handler) + (catch_ref $with_ref $ref_handler) + call $fork + drop + local.get $take_plain + if + i32.const 41 + throw $plain + else + throw $with_ref + end + unreachable)) + drop + i32.const 42 + br $done) + br $done)) + (memory 1)) + "#; + let bytes = instrument_wat(wat); + validate(&bytes); + let printed = wasmprinter::print_bytes(&bytes).expect("wasmprinter"); + let caller_section = extract_function_text(&printed, "caller"); + assert!( + caller_section.contains("i32.const -1"), + "catch_ref capture must mark the frame-backed active arm as \ + non-plain:\n{caller_section}", + ); + assert!( + !caller_section.contains("ref.is_null"), + "mixed replay must not use static exnref-table contents as \ + capture-kind state:\n{caller_section}", ); } @@ -2610,8 +2589,7 @@ fn b1_stage_2_rewind_stub_dispatches_two_arms() { fn b1_stage_2_carved_out_function_no_b1_dispatch_emitted() { // A function in `b2_carveout` (here: ref-typed catch payload) // must NOT receive B1's plain-catch dispatch — its rewind stub - // should retain Phase 6's pre-B1 form (throw_ref only) and - // contain no `ref.is_null`-based sentinel. + // should retain Phase 6's throw_ref-only form. let wat = r#" (module (import "kernel" "kernel_fork" (func $fork (result i32))) @@ -2631,15 +2609,14 @@ fn b1_stage_2_carved_out_function_no_b1_dispatch_emitted() { let printed = wasmprinter::print_bytes(&bytes).expect("wasmprinter"); let caller_section = extract_function_text(&printed, "caller"); // The carved-out function's rewind stub uses ONLY Phase 6's - // throw_ref path. It must NOT contain B1's `ref.is_null` sentinel - // (which would indicate a B1 plain-catch dispatch was emitted). + // throw_ref path and no plain-catch tag throw. assert!( caller_section.contains("throw_ref"), "carved-out function must retain Phase 6's throw_ref:\n{caller_section}" ); assert!( !caller_section.contains("ref.is_null"), - "carved-out function must NOT have B1's ref.is_null sentinel \ - (no plain-catch dispatch should be emitted):\n{caller_section}" + "carved-out function must not consult exnref nullness as mode \ + state:\n{caller_section}" ); } diff --git a/crates/fork-instrument/tests/runtime.rs b/crates/fork-instrument/tests/runtime.rs index c463af56a4..cd5f3a0ce4 100644 --- a/crates/fork-instrument/tests/runtime.rs +++ b/crates/fork-instrument/tests/runtime.rs @@ -108,6 +108,76 @@ fn linked_runtime_imports_transaction_hooks_and_emits_exact_prefix_metadata() { ); } +#[test] +fn plain_catches_do_not_expand_fixed_prefix_metadata() { + let bytes = instrument_wat( + r#" + (module + (import "kernel" "kernel_fork" (func $fork (result i32))) + (tag $payload (param i32)) + (memory 1) + (func (export "run") (param $value i32) (result i32) + (block $handler (result i32) + (try_table (catch $payload $handler) + local.get $value + throw $payload + unreachable) + unreachable) + drop + call $fork)) + "#, + ); + let descriptors: Vec<_> = Parser::new(0) + .parse_all(&bytes) + .filter_map(|payload| match payload.expect("parse payload") { + Payload::CustomSection(section) if section.name() == LINKED_FRAME_FORMAT_SECTION => { + Some(FrameFormatDescriptor::decode(section.data()).unwrap()) + } + _ => None, + }) + .collect(); + assert_eq!( + descriptors, + vec![FrameFormatDescriptor::current(PointerWidth::Wasm32, 24)], + "plain-catch activation state belongs in function frames, not the module prefix", + ); +} + +#[test] +fn memory64_plain_catches_do_not_expand_fixed_prefix_metadata() { + let bytes = instrument_wat( + r#" + (module + (import "kernel" "kernel_fork" (func $fork (result i32))) + (tag $payload (param i32)) + (memory i64 1) + (func (export "run") (param $value i32) (result i32) + (block $handler (result i32) + (try_table (catch $payload $handler) + local.get $value + throw $payload + unreachable) + unreachable) + drop + call $fork)) + "#, + ); + let descriptors: Vec<_> = Parser::new(0) + .parse_all(&bytes) + .filter_map(|payload| match payload.expect("parse payload") { + Payload::CustomSection(section) if section.name() == LINKED_FRAME_FORMAT_SECTION => { + Some(FrameFormatDescriptor::decode(section.data()).unwrap()) + } + _ => None, + }) + .collect(); + assert_eq!( + descriptors, + vec![FrameFormatDescriptor::current(PointerWidth::Wasm64, 32)], + "memory64 plain-catch state belongs in function frames", + ); +} + #[test] fn module_without_fork_seed_does_not_import_linked_storage_hooks() { let bytes = instrument_wat("(module (memory 1) (func (export \"run\")))"); @@ -379,7 +449,7 @@ fn saved_globals_metadata_reports_declared_order() { // metadata — the high-level `instrument` fn hides it. let bytes = wat::parse_str(MODULE_WITH_EXTRA_GLOBAL).unwrap(); let mut module = Module::from_buffer(&bytes).unwrap(); - let runtime = inject_runtime(&mut module, 0); + let runtime = inject_runtime(&mut module); // Exactly two saved globals, in declaration order: user_stack, then user_tls. assert_eq!(runtime.saved_globals.len(), 2); @@ -395,7 +465,7 @@ fn saved_globals_metadata_reports_declared_order() { fn module_with_no_extra_globals_has_empty_saved_globals() { let bytes = wat::parse_str(EMPTY_MODULE_WITH_FORK).unwrap(); let mut module = Module::from_buffer(&bytes).unwrap(); - let runtime = inject_runtime(&mut module, 0); + let runtime = inject_runtime(&mut module); assert!( runtime.saved_globals.is_empty(), @@ -416,7 +486,7 @@ fn wasm64_saved_globals_use_16_byte_header() { "#; let bytes = wat::parse_str(wat).unwrap(); let mut module = Module::from_buffer(&bytes).unwrap(); - let runtime = inject_runtime(&mut module, 0); + let runtime = inject_runtime(&mut module); // wasm64 → header 2 * 8 = 16 bytes. assert_eq!(runtime.saved_globals.len(), 1); @@ -437,7 +507,7 @@ fn ref_typed_mutable_globals_are_skipped_in_4e() { "#; let bytes = wat::parse_str(wat).unwrap(); let mut module = Module::from_buffer(&bytes).unwrap(); - let runtime = inject_runtime(&mut module, 0); + let runtime = inject_runtime(&mut module); // Only the i32 scalar should have been picked up. assert_eq!(runtime.saved_globals.len(), 1); @@ -604,73 +674,73 @@ fn unwind_begin_writes_absolute_frames_start_wasm64() { } // ====================================================================== -// Stage 1 (B1) Task 1.3 — plain-catch scratch reservation in save buffer +// Runtime buffer ownership // ====================================================================== -#[test] -fn b1_scratch_size_is_zero_for_module_without_plain_catch() { - // A fork-using module with no try_table at all: B1 plan computes - // `total_bytes = 0`, the runtime reserves no scratch space, and - // `frames_start_offset` is byte-identical to pre-B1. - let wat = r#" - (module - (import "kernel" "kernel_fork" (func $fork (result i32))) - (func $caller (export "caller") (result i32) - call $fork) - (memory 1)) - "#; - let bytes = wat::parse_str(wat).unwrap(); +fn assert_end_functions_clear_buffer_before_normal(wat_src: &str, ptr_ty: ValType) { + let bytes = wat::parse_str(wat_src).unwrap(); let mut module = Module::from_buffer(&bytes).unwrap(); - let runtime = inject_runtime(&mut module, 0); - assert_eq!(runtime.b1_scratch_size, 0); - assert_eq!( - runtime.b1_scratch_base, runtime.frames_start_offset, - "with zero scratch, base and frames_start coincide", - ); + let runtime = inject_runtime(&mut module); + + for (name, id) in [ + (names::EXPORT_UNWIND_END, runtime.unwind_end), + (names::EXPORT_REWIND_END, runtime.rewind_end), + (names::EXPORT_ABORT_END, runtime.abort_end), + ] { + let local = match &module.funcs.get(id).kind { + walrus::FunctionKind::Local(local) => local, + _ => panic!("{name} is not local"), + }; + let instrs: Vec<_> = local + .block(local.entry_block()) + .instrs + .iter() + .map(|(instr, _)| instr) + .collect(); + assert_eq!(instrs.len(), 4, "{name} must contain two exact stores"); + assert!( + matches!( + (ptr_ty, instrs[0]), + ( + ValType::I32, + Instr::Const(walrus::ir::Const { + value: walrus::ir::Value::I32(0), + }) + ) | ( + ValType::I64, + Instr::Const(walrus::ir::Const { + value: walrus::ir::Value::I64(0), + }) + ) + ), + "{name} must first materialize the pointer-width zero", + ); + assert!( + matches!(instrs[1], Instr::GlobalSet(set) if set.global == runtime.buf_global), + "{name} must clear _wpk_fork_buf before publishing NORMAL", + ); + assert!( + matches!( + instrs[2], + Instr::Const(walrus::ir::Const { + value: walrus::ir::Value::I32(0), + }) + ), + "{name} must materialize STATE_NORMAL", + ); + assert!( + matches!(instrs[3], Instr::GlobalSet(set) if set.global == runtime.state_global), + "{name} must publish STATE_NORMAL last", + ); + } } #[test] -fn b1_scratch_size_shifts_frames_start_offset() { - // Two identical modules instrumented with different scratch sizes. - // The non-zero one must shift `frames_start_offset` by exactly - // (aligned) scratch size; `b1_scratch_base` must NOT shift — - // it tracks the post-saved-globals cursor only. - let wat = r#" - (module - (memory 1)) - "#; - let bytes = wat::parse_str(wat).unwrap(); - let mut module_a = Module::from_buffer(&bytes).unwrap(); - let runtime_a = inject_runtime(&mut module_a, 0); - - let mut module_b = Module::from_buffer(&bytes).unwrap(); - let runtime_b = inject_runtime(&mut module_b, 16); - - assert_eq!(runtime_b.b1_scratch_size, 16); - assert_eq!( - runtime_b.b1_scratch_base, runtime_a.b1_scratch_base, - "b1_scratch_base sits at end of saved-globals area regardless of scratch size", - ); - assert_eq!( - runtime_b.frames_start_offset, - runtime_a.frames_start_offset + 16, - "frames_start_offset shifts by exactly the (aligned) scratch size", - ); +fn wasm32_end_functions_release_buffer_before_normal() { + assert_end_functions_clear_buffer_before_normal("(module (memory 1))", ValType::I32); } #[test] -fn b1_scratch_size_rounded_up_to_8_alignment() { - // 5 bytes requested → rounded up to 8. The frame data must - // start 8-aligned regardless of payload size so its first - // i64 store lands on an aligned address. - let wat = r#"(module (memory 1))"#; - let bytes = wat::parse_str(wat).unwrap(); - let mut module = Module::from_buffer(&bytes).unwrap(); - let runtime = inject_runtime(&mut module, 5); - assert_eq!(runtime.b1_scratch_size, 8, "5 rounds up to 8"); - assert_eq!( - runtime.frames_start_offset - runtime.b1_scratch_base, - 8, - "frames_start sits exactly aligned-size bytes after scratch base", - ); +fn wasm64_end_functions_release_buffer_before_normal() { + assert_end_functions_clear_buffer_before_normal("(module (memory i64 1))", ValType::I64); } diff --git a/docs/architecture.md b/docs/architecture.md index 580f0fd962..0d1d28a0b2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -534,8 +534,14 @@ caller now take. &pid_out, 0)`. Wire format documented in `docs/plans/2026-05-04-non-forking-posix-spawn-design.md` Section 1. 2. Host (`handleSpawn` in `kernel-worker.ts`) reads the blob from - caller memory, copies it to kernel scratch, and calls - `kernel_spawn_process(parent_pid, caller_tid, blob_ptr, blob_len)`. + caller memory, validates argv + envp against the same 4 MiB `ARG_MAX` + contract as `execve`, copies it to bounded kernel-owned scratch, and calls + `kernel_spawn_process(parent_pid, caller_tid, blob_ptr, blob_len)`. Ordinary + blobs reuse the channel-sized syscall scratch. A blob above that size + lazily allocates one whole-spawn buffer and reuses it for the kernel + lifetime. Keeping both paths kernel-owned prevents a large environment or + file-action list from overwriting adjacent Rust heap state; the explicit + whole-blob ceiling also bounds data that `ARG_MAX` does not count. 3. Kernel parses the blob (`crates/kernel/src/spawn.rs::parse_blob` — the trust boundary; bails with EINVAL on any malformed offset), validates `caller_tid` as a live task belonging to the parent, and calls diff --git a/docs/binary-releases.md b/docs/binary-releases.md index 63c247f799..5f3b26063b 100644 --- a/docs/binary-releases.md +++ b/docs/binary-releases.md @@ -117,6 +117,16 @@ PR's [Phase 10 workflow rewrite](plans/2026-05-13-binary-resolution-via-index-le preflight → toolchain-cache → matrix-build → test-gate → merge-gate ``` +The maintainer-only `stage-rootfs-closure-only` label is a bounded +prepublication accelerator. It first rejects a stale +`program-packages.json`, then mechanically selects `rootfs/wasm32` and that +entry's complete wasm32 dependency closure. It forces every selected +generation into the PR staging release even when the canonical release already +has matching bytes, while recording every other program generation as +deferred. This limited run keeps `merge-gate` pending and skips the full test +surface. `prepare-merge` rejects the label; remove it and complete ordinary +staging before asking the merge workflow to run. + - **preflight** asks `xtask staging-reuse expected` for the complete, cache-keyed package/arch ledger. It may reuse `pr--staging` only directly when the target index has the exact ABI, covers every managed entry diff --git a/docs/fork-instrumentation.md b/docs/fork-instrumentation.md index 0c87674ddb..f0b17173d3 100644 --- a/docs/fork-instrumentation.md +++ b/docs/fork-instrumentation.md @@ -95,7 +95,8 @@ wpk_fork_unwind_begin(buf: ptr) -> () wpk_fork_unwind_end() -> () Precondition: state == UNWINDING and all frames have been drained. - Postcondition: state := NORMAL + Postcondition: _wpk_fork_buf := 0 + state := NORMAL wpk_fork_rewind_begin(buf: ptr) -> () Precondition: state == NORMAL (in a freshly-instantiated child) @@ -105,7 +106,8 @@ wpk_fork_rewind_begin(buf: ptr) -> () wpk_fork_rewind_end() -> () Precondition: state == REWINDING and all frames have been reloaded. - Postcondition: state := NORMAL + Postcondition: _wpk_fork_buf := 0 + state := NORMAL wpk_fork_abort_begin(buf: ptr) -> () Precondition: state == UNWINDING after a typed frame-allocation failure. @@ -116,7 +118,8 @@ wpk_fork_abort_begin(buf: ptr) -> () wpk_fork_abort_end() -> () Precondition: state == ABORT_UNWINDING and all committed inner frames have been reloaded. - Postcondition: state := NORMAL + Postcondition: _wpk_fork_buf := 0 + state := NORMAL wpk_fork_state() -> i32 Returns current state. Exported for host-side assertions. @@ -174,12 +177,19 @@ advance `ABI_VERSION` and regenerate `abi/snapshot.json` atomically. tool picks the pointer width from the module's primary memory — a memory64 memory yields `i64`, anything else yields `i32`. -`wpk_fork_unwind_begin` self-initializes `*(buf + 0)` with the address of the -first byte after the fixed prefix before touching user state. During linked -unwind and rewind, generated code overwrites that word with the payload address -returned by the corresponding host hook. The host allocates the root chunk and -passes `root + chunk_header_size` as `buf`; no caller computes or preallocates a -worst-case frame-data footprint. +`wpk_fork_unwind_begin` self-initializes `*(buf + 0)` with +`buf + frames_start_offset` before touching user state. In the linked format, +that initial address is the 16-byte abort selector inside the fixed prefix. +During linked unwind and rewind, generated code overwrites the word with the +payload address returned by the corresponding host hook. The host allocates +the root chunk and passes `root + chunk_header_size` as `buf`; no caller +computes or preallocates a worst-case frame-data footprint. + +Every end export clears `_wpk_fork_buf` before publishing `NORMAL`. This +removes the module's stale alias when the host releases or reuses continuation +storage. Address zero is ordinary linear memory, so the clear is an ownership +invariant and defense, not a substitute for correct generated code: no +ordinary-execution path may dereference the buffer global. ## Host Threading Contract @@ -311,11 +321,18 @@ chunks contain only a chunk header and nodes. Version-1 chunk headers are: | `+8+4P` | `P` | used | First unused byte | | `+8+5P` | `P` | committed tail | Newest committed node; meaningful on root | -The module prefix retains the runtime's pointer word, reserved pointer word, -saved scalar globals, B1 plain-catch scratch, and a 16-byte abort selector. -`frames_start_offset = 2P + N + B` identifies the selector, while the -host-visible fixed-prefix size is `frames_start_offset + 16`. Frame nodes are -not stored in that prefix. +The module prefix retains the runtime's active-frame pointer word, a reserved +pointer word, saved scalar globals, and a 16-byte abort selector. +`frames_start_offset = 2P + N` identifies the selector, while the host-visible +fixed-prefix size is `frames_start_offset + 16`. Frame nodes and +plain-catch activation state are not stored in that prefix. + +This does not introduce a new linked-frame encoding. `fixed_prefix_size` has +always been a module-specific value in the version-1 descriptor, and each node +already declares its function-specific payload size. Existing artifacts retain +and report their larger historical prefix; newly instrumented artifacts report +the prefix they actually use. Import/export names, descriptor fields, and host +parsing semantics are unchanged. At the first fork call, the host maps one page-rounded root large enough for the chunk header and fixed prefix. Each postamble already knows its own exact @@ -345,10 +362,6 @@ Parent and child independently walk and unmap their copies after rewind. The linked format makes chunk boundaries explicit, but version 1 does not rebase internal pointers or relocate the chain in the child. -The `b1_scratch` region is empty (`B == 0`) when no fork-path function in the -module has a plain (non-`_ref`) catch arm — this is the case for every -shipping port today other than future C++ EH ports. - Ref-typed mutable globals (`funcref` / `externref` / `exnref`) are not stored in the linear-memory header — they would need aux-table spill slots, which is a future extension. The tool currently ignores them when snapshotting globals. @@ -356,8 +369,9 @@ a future extension. The tool currently ignores them when snapshotting globals. ## Frame format Each instrumented function has a statically known payload size. The size -depends on how many scalar user locals the function has, but the payload header -is uniform. Each payload is preceded by a linked-node header: `KFCN` magic, +depends on its scalar user locals and instrumenter-owned frame locals, but the +payload header is uniform. Each payload is preceded by a linked-node header: +`KFCN` magic, format version, transactional state, previous-node pointer, payload size, and total aligned node size. That header costs 24 bytes on wasm32 and 32 bytes on wasm64 before alignment. @@ -367,8 +381,8 @@ wasm64 before alignment. | `+0` | 4 | `func_index` | Ordinal assigned at instrument time | | `+4` | 4 | `call_index` | Which call site within the function | | `+8` | 4 | `catch_region_id` | 0 in normal flow; non-zero for catches | -| `+12` | 4 | `exnref_slot` | Valid when `catch_region_id != 0` | -| `+16` | var | `saved_locals[]` | Scalar user locals, natural-aligned | +| `+12` | 4 | `exnref_slot` | Aux-table slot for `_ref` catch replay | +| `+16` | var | `saved_locals[]` | User and synthetic scalars, aligned | Ref-typed user locals (funcref, externref, exnref) do **not** appear in this frame. They are spilled to auxiliary tables — see [Auxiliary @@ -376,11 +390,17 @@ tables](#auxiliary-tables) below. The frame only records the ordinal identity of the function and its call-site, which together with the ref-table slot assignment is sufficient to restore the ref-typed locals during rewind. +Synthetic frame locals include call-argument and operand-stack carryover +spills. For each supported plain-catch region they also include one +`active_arm` i32 and typed scalar operand locals for every static arm. Capture +therefore belongs to one function activation, and recursive activations +serialize distinct values in distinct linked frames. + `catch_region_id` is zero in the common case (the frame was captured outside any catch handler). When non-zero, it identifies the `try_table` whose catch -handler the frame lives in, and `exnref_slot` points at the `_wpk_fork_exnref_stash` -table slot that holds the caught exnref. The rewind preamble uses both fields -to route control back through the original catch clause — see [Catch-handler +handler the frame lives in. For a `_ref` catch, `exnref_slot` identifies the +auxiliary-table entry. For a plain catch, the restored `active_arm` and operand +locals select and reconstruct the exact arm. See [Catch-handler resume](#catch-handler-resume). ## Dispatch schemes @@ -525,7 +545,8 @@ Numbered callouts: 1. **Preamble (Phase 4d).** Every instrumented function opens with a state test. Under `REWINDING`, the preamble calls `__wpk_fork_frame_next(frame_size)`, stores the returned payload in - `*(buf + 0)`, and deserializes each scalar user local. Dispatch reads + `*(buf + 0)`, and deserializes every frame scalar: user locals, + argument/carryover spills, and plain-catch activation state. Dispatch reads `call_index` directly from that active frame payload. 2. **Body wrapper (Phase 4b/4c).** The original body is wrapped in a `$unwind_save` block. On `REWINDING`, a `br_table` keyed by `frame.call_index` jumps to @@ -539,10 +560,10 @@ Numbered callouts: `frame.call_index` and exits `$unwind_save`. If the callee did not begin unwinding, execution continues normally. 5. **Postamble (Phase 4d).** Emits the remaining frame header fields - (func_index, catch_region_id, exnref_slot), writes each scalar user local, - commits the reserved node, and returns a default value of the function's - result type. Callers see the default on the unwind path but discard it - because their own postamble runs next. + (func_index, catch_region_id, exnref_slot), writes every user and synthetic + frame scalar, commits the reserved node, and returns a default value of the + function's result type. Callers see the default on the unwind path but + discard it because their own postamble runs next. ### (b) Fork from inside a catch handler @@ -920,14 +941,19 @@ continues to the fork call site. ``` `catch_ref` and `catch_all_ref` clauses use the exnref stash + `throw_ref` -flow above. Plain (non-`_ref`) `catch` clauses have no exnref to re-throw, -so B1 stages 1+2 add a parallel scratch-replay path: each fork-path plain-catch -arm stashes its operand tuple to the module-level B1 scratch region on -NORMAL entry and replays the operand tuple on REWIND. The unified rewind-throw -stub uses `ref.is_null` on the exnref slot to discriminate between the two -modes, and an arm-id if-chain to dispatch among multiple arms in the same -try_table. See [Fork-from-plain-catch (B1 stages 1+2)](#fork-from-plain-catch-b1-stages-12) -under "Maintainer notes" for the implementation. +flow above. Plain (non-`_ref`) `catch` clauses have no exnref to re-throw. +For those clauses, normal capture stores the exact nonnegative catch-list arm +index and scalar operand tuple in activation-local frame locals. Rewind +compares the restored arm index, pushes that arm's restored operands, and +throws the original tag back through the same `try_table`. + +In a region that mixes `_ref` and plain catches, `_ref` capture stores +`active_arm = -1`; exact nonnegative IDs select plain arms and every other +value falls through to `throw_ref`. The mode is deliberately frame-owned. +Auxiliary-table nullness is neither activation identity nor reliable handler +kind state because a static table entry can retain an older value. See +[Fork-from-plain-catch](#fork-from-plain-catch) under "Maintainer notes" for +the implementation. ## Call-graph discovery @@ -996,9 +1022,10 @@ K-04, and K-07 cover the current behavior. `wpk_fork_unwind_begin` and restored in `wpk_fork_rewind_begin`. Includes `__stack_pointer`, `__tls_base`, and any program-declared mutable globals. -- **try_table context.** Frames captured inside a fork-path catch handler - carry the active `catch_region_id` and exnref stash slot, and rewind - re-enters the handler via `throw_ref` (Phase 6). +- **try_table context.** Frames captured inside a supported fork-path catch + handler carry the active `catch_region_id`. `_ref` catches replay from their + exnref stash slot; plain catches replay from activation-local arm and scalar + payload state serialized in the same frame. - **Kernel-side-effect calls don't re-fire during REWIND.** Switch-dispatch (the only live scheme post-commit-4) skips the body chunks before the matching `POST_K` entirely on REWIND, so non-fork-path direct calls @@ -1013,16 +1040,26 @@ K-04, and K-07 cover the current behavior. [posix-status.md](posix-status.md) for rationale. - **Functions whose plain-catch arms carry ref-typed operands.** Catch arms whose operand tuple includes an `(ref ...)` value (typically a function or - GC ref) are carved out of the fork-path set at instrument time. Spilling - ref-typed catch operands would require per-arm aux-table slots. The current - PR deliberately keeps the safe `B1ScratchPlan::b2_carveout` behavior and - covers it with WAT-level tests. This is an unanticipated Wasm-level case, + GC ref) are excluded from plain-catch replay support at instrument time. + The function may still be instrumented for other fork sites, but a fork + reached from the affected handler remains unsupported. Spilling ref-typed + catch operands would require per-arm activation-aware auxiliary storage. + The current implementation keeps that explicit + `PlainCatchPlan::b2_carveout` boundary and covers it with WAT-level tests. + This is an unanticipated Wasm-level case, not expected output from ordinary C++ EH lowering: C++ exception payloads live in linear memory / libc++abi state rather than as `funcref` or `externref` plain-catch tag operands. If a future language frontend or hand-written Wasm module needs it, implement per-arm funcref/externref aux-table stashing and promote C-08/C-09 from carve-out validation to full replay tests. +- **Recursive/reentrant ref activation state.** Auxiliary-table slots for + ref-typed user locals and caught exnrefs are assigned statically per + function or `try_table`. Recursive or reentrant activations that keep + distinct ref values live across one fork can therefore alias those slots. + Scalar plain-catch arm/payload state does not have this limitation because + it is serialized per activation. Closing the ref case requires + activation-aware auxiliary storage and executable recursive regressions. - **IfElse with operand-stack carryover.** A fork-bearing `if/else` enclosing a stack value that survives across the branch is rejected by `seq_has_unsupported_carryover` — the cond rewrite via `select` (see @@ -1101,6 +1138,13 @@ encoding. The fixed 60 KiB host-reserved control-region geometry remains in ABI 42, but it is no longer continuation capacity: only its anchor word is used to find the dynamically allocated root chunk. +A function with supported plain catches adds one i32 `active_arm` local per +plain-capable region plus typed scalar operand locals for every supported arm +to each activation's frame payload. This can use more aggregate continuation +bytes than one module-global tuple, but distinct activation storage is required +for recursion and reentrancy correctness and removes normal-execution writes +to continuation-owned memory. + As a narrow size check, instrumenting the P-10 deep-recursion fixture from the same 27,886-byte raw Wasm produced 50,873 bytes with the ABI 41 instrumenter and 52,330 bytes with the ABI 42 linked-frame instrumenter: 1,457 additional @@ -1221,7 +1265,7 @@ the containing switch-dispatch shape skips that opcode on REWIND. Existing examples are the S-01..S-08 host fixtures plus the WAT-level table-operation tests in `crates/fork-instrument/tests/coverage_wat.rs`. -### Fork-from-plain-catch (B1 stages 1+2) +### Fork-from-plain-catch Plain (non-`_ref`) `catch` arms unwrap the thrown exception's operand tuple onto the operand stack at handler entry, but unlike `catch_ref` / @@ -1230,47 +1274,39 @@ reaches the handler by `throw_ref`-ing a saved exnref into the original try_table's catch clause; with a plain catch there is no exnref to save, so some other resume path is needed. -B1 stages 1+2 add that path: - -1. **Discovery (Stage 1, `instrument::discover_plain_catch_arms`).** Walks - each function and collects every plain-catch arm's tag, target label, and - operand-tuple types. Stage 1 also assigns each arm a `B1ScratchPlanSlot` - with a per-arm offset into the module-level scratch region (computed by - `plan_b1_scratch`). Each arm's slot is sized to the natural-aligned - total of its operand tuple's scalar widths. -2. **Capture-block emission (Stage 2 Task 2.2, - `apply_plain_catch_handlers`).** Every plain-catch arm gets a wrapping - `$capture` block injected around its handler body. On entry the block - pops the arm's operand tuple off the stack, stores each value into the - arm's `b1_scratch` slot, sets `$catch_region_id_local` and a - per-handler arm-id, then re-pushes the operands and falls through to the - user handler. On REWIND the rewind-throw stub uses the arm-id to dispatch - into a synthesized re-throw that lands the operands back on the stack - from `b1_scratch` and re-enters the user handler with the same operand - shape it would have seen on the NORMAL pass. -3. **Multi-arm dispatch (Stage 2 Task 2.3, in `inject_rewind_throw_stubs`).** - The pre-existing rewind-throw stub used a single `(if (state == REWINDING - && catch_region_id == K))` check, fine for catch_ref's exnref-stash path. - For plain catches the stub now also discriminates on `arm_id` via an - if-chain so a try_table with multiple plain-catch arms reaches the - correct one. The stub still uses `ref.is_null` on the exnref slot to - select between catch_ref-style throw_ref and plain-catch-style operand - replay. -4. **Carve-out (Stage 2 Task 2.1, in `plan_b1_scratch`).** Functions whose - plain-catch arms carry ref-typed operands (`(ref T)` other than `exnref`) - are removed from the fork-path set rather than instrumented, since the - scratch region only stores scalar tuples. This is the safe path for an - unanticipated Wasm-level case: ordinary C++ EH is not expected to emit - funcref/externref plain-catch payloads, and no current shipping port - needs them. Stage 2 Task 2.4 also detects multi-target plain-catch - try_tables and applies the same carve-out. - -The carve-out is reported via `B1ScratchPlan::b2_carveout`; the scratch -region is sized only for the surviving fork-path arms. C-08/C-09 in +The implementation adds that path without accessing continuation memory during +ordinary catch execution: + +1. **Static discovery (`plan_plain_catches`).** Walk each fork-path function + and collect every supported plain-catch arm's tag, target label, catch-list + index, and operand types. This plan contains no runtime addresses or + activation state. +2. **Activation allocation (`allocate_plain_catch_state`).** Allocate one + `PlainCatchRegionState` per static region: an i32 `active_arm` plus typed + scalar operand locals for every supported arm. +3. **Frame ownership (`append_plain_catch_frame_scalars`).** Add those locals + before frame offsets and size are assigned. Each recursive activation then + saves and restores its own catch values in its own linked frame. +4. **Capture and replay.** `apply_plain_catch_handlers` stores the incoming + operand tuple and exact arm index in those locals, then re-pushes the + operands for the user handler. `inject_rewind_throw_stubs` dispatches on + the restored exact arm index and rethrows its tag with the restored tuple. + Mixed `_ref` capture records `-1`, which selects the `throw_ref` fallback + without consulting stale table nullness. +5. **Carve-out (`PlainCatchPlan::b2_carveout`).** A function whose plain-catch + arms carry ref-typed operands or use an unverified multi-target shape is + excluded from supported plain-catch replay. Other fork sites in that + function may still be instrumented. Scalar frame serialization does not + solve reference ownership; that needs activation-aware auxiliary storage. + +The lifetime boundary is load-bearing: a plain catch can run before any fork +or after a prior continuation has been released. Its normal capture path must +therefore never dereference `_wpk_fork_buf`. + +C-08/C-09 in `crates/fork-instrument/tests/coverage_wat.rs` verify that funcref and -externref catch operands take this safe carve-out path rather than panicking -or producing invalid wasm. For shipping ports the carve-out is empty in the -current matrix, so it is not a merge blocker for PR #307. +externref catch operands retain this explicit unsupported boundary rather than +being serialized as scalars. ## See also diff --git a/docs/posix-status.md b/docs/posix-status.md index e146d9bbe4..bd4f5eb469 100644 --- a/docs/posix-status.md +++ b/docs/posix-status.md @@ -169,7 +169,7 @@ same final-OFD lifetime rules. | `ioperm()` / `iopl()` | Stub | Returns EPERM. No I/O port access. | | `remap_file_pages()` | Stub | Returns ENOSYS. | | `getcontext()` / `setcontext()` / `makecontext()` / `swapcontext()` | Unsupported | Userspace stack-switching primitives, deprecated in POSIX.1-2008, not planned. See the "ucontext API unsupported" row under [Wasm-Inherent gaps](#wasm-inherent--gaps-that-cannot-be-fully-resolved-in-wasm) for rationale. | -| `fork()` called from a C++/Ruby exception catch handler | Full | B1 stages 1+2 + Phase 6 catch-handler resume machinery (per-arm scratch space in the save buffer, multi-arm rewind dispatch, `$capture`-block emission, rewind-throw stub via `_wpk_fork_exnref_stash`) close fork-from-plain-catch under **modern wasm-EH lowering** (`try_table` / `catch_ref` / `throw_ref`). The fierce-wire mega-PR (PR #307) commit 9 + 2026-05-14 followup flipped the SDK + libcxx (revision 4) to modern EH explicitly (LLVM 21's `-wasm-use-legacy-eh` defaults to `true`, so removing the prior `=true` override silently kept legacy lowering — the explicit `=false` is required). Test coverage in `host/test/fork-instrument-coverage.test.ts`: C-02 fork-in-catch, C-03 multi-arm catch, C-04 throw-from-outside, C-05 modern EH single typed catch, C-06 modern multi-target `*_ref`, C-07 modern multi-arm plain, C-10 fork in both try body + handler, C-11 post-catch fork (SpiderMonkey-spike test (b)), S-08 throw-from-outside + fork-in-catch — all 9 PASS. Combined with C-01 (fork-in-try-body), the catch-handler coverage is comprehensive for both legacy-EH-pattern and modern-EH-pattern C++. Funcref/externref catch operands (A4) remain on the not-yet-supported list — see [docs/fork-instrumentation.md §Not guaranteed](fork-instrumentation.md#not-guaranteed-unsupported-patterns). | +| `fork()` called from a C++/Ruby exception catch handler | Partial | Modern wasm-EH `try_table` catch replay is implemented. Plain-catch arm identity and scalar operands are activation-local and serialized in each linked frame, so distinct recursive activations do not share module scratch; multi-arm capture/rethrow restores the exact arm. `_ref` catches use `_wpk_fork_exnref_stash`. Existing C-01 through C-07, C-10, C-11, and S-08 coverage plus `host/test/plain-catch-payload-lifetime.test.ts` exercise the supported surface. Remaining gaps are explicit: ref-typed plain-catch operands are carved out, and static auxiliary-table slots can alias caught exnrefs or ref-typed locals across recursive/reentrant activations. See [docs/fork-instrumentation.md §Not guaranteed](fork-instrumentation.md#not-guaranteed-unsupported-patterns). | ## Signals diff --git a/host/src/kernel-worker.ts b/host/src/kernel-worker.ts index 4d52af8e03..af81b64fc2 100644 --- a/host/src/kernel-worker.ts +++ b/host/src/kernel-worker.ts @@ -213,6 +213,25 @@ const EXEC_METADATA_MAX_BYTES = 4 * 1024 * 1024; const EXEC_PATH_MAX_BYTES = 4096; const PROCESS_METADATA_ARGV = 0; const PROCESS_METADATA_ENVIRONMENT = 1; +const SPAWN_HEADER_BYTES = 40; +const SPAWN_ACTION_RECORD_BYTES = 28; +const SPAWN_MAX_ARGV = 4096; +const SPAWN_MAX_ENVP = 4096; +const SPAWN_MAX_ACTIONS = 1024; + +/** + * Largest complete SYS_SPAWN wire blob accepted by the host. + * + * argv + envp retain the public 4 MiB ARG_MAX contract. The remaining room + * is an aggregate file-action budget equivalent to the parser's 1,024-action + * ceiling with one PATH_MAX-sized path per action. The host needs an explicit + * whole-blob bound because this representation also carries file actions, + * which ARG_MAX itself does not count. + */ +export const SPAWN_BLOB_MAX_BYTES = + EXEC_METADATA_MAX_BYTES + + SPAWN_HEADER_BYTES + + SPAWN_MAX_ACTIONS * (SPAWN_ACTION_RECORD_BYTES + EXEC_PATH_MAX_BYTES); /** Syscall numbers for sleep/delay */ const SYS_NANOSLEEP = ABI_SYSCALLS.Nanosleep; @@ -533,9 +552,7 @@ function parseProcSnapshots(mem: Uint8Array): ProcessSnapshot[] { * Throws on malformed input. Callers should treat the throw as EINVAL. */ function decodeSpawnBlobStrings(blob: Uint8Array): { argv: string[]; envp: string[] } { - const HEADER_LEN = 40; - const ACTION_RECORD_LEN = 28; - if (blob.byteLength < HEADER_LEN) { + if (blob.byteLength < SPAWN_HEADER_BYTES) { throw new Error("blob too short for header"); } const view = new DataView(blob.buffer, blob.byteOffset, blob.byteLength); @@ -544,14 +561,18 @@ function decodeSpawnBlobStrings(blob: Uint8Array): { argv: string[]; envp: strin const nActions = view.getUint32(8, true); // Cap counts to mirror the kernel parser's adversarial-input cap. - if (argc > 4096 || envc > 4096 || nActions > 1024) { + if ( + argc > SPAWN_MAX_ARGV + || envc > SPAWN_MAX_ENVP + || nActions > SPAWN_MAX_ACTIONS + ) { throw new Error("blob count exceeds limit"); } - const argvOffsetsAt = HEADER_LEN; + const argvOffsetsAt = SPAWN_HEADER_BYTES; const envpOffsetsAt = argvOffsetsAt + argc * 4; const actionsAt = envpOffsetsAt + envc * 4; - const stringsAt = actionsAt + nActions * ACTION_RECORD_LEN; + const stringsAt = actionsAt + nActions * SPAWN_ACTION_RECORD_BYTES; if (stringsAt > blob.byteLength) { throw new Error("blob truncated before strings region"); @@ -1068,6 +1089,8 @@ export class CentralizedKernelWorker { /** Pids whose old image committed exec but whose replacement has no channel yet. */ private execHandoffPids = new Set(); private scratchOffset = 0; + /** Kernel-owned transport for spawn blobs larger than one syscall channel. */ + private largeSpawnScratchOffset = 0; private initialized = false; /** * Maps a pthread syscall mailbox to its kernel/libc thread id. @@ -9593,8 +9616,51 @@ export class CentralizedKernelWorker { // ── Read path + blob from caller memory ── const processMem = new Uint8Array(channel.memory.buffer); + if ( + !Number.isSafeInteger(pathLen) || + pathLen < 0 || + !Number.isSafeInteger(blobLen) || + blobLen <= 0 + ) { + this.completeChannel(channel, SYS_SPAWN, origArgs, undefined, -1, EINVAL); + return; + } + if (pathLen >= EXEC_PATH_MAX_BYTES) { + this.completeChannel( + channel, + SYS_SPAWN, + origArgs, + undefined, + -1, + ENAMETOOLONG, + ); + return; + } + if ( + pathLen > 0 && + !isValidMemoryRange(processMem, pathPtr, pathLen) + ) { + this.completeChannel(channel, SYS_SPAWN, origArgs, undefined, -1, EFAULT); + return; + } + if (blobLen > SPAWN_BLOB_MAX_BYTES) { + this.completeChannel(channel, SYS_SPAWN, origArgs, undefined, -1, E2BIG); + return; + } + if (!isValidMemoryRange(processMem, blobPtr, blobLen)) { + this.completeChannel(channel, SYS_SPAWN, origArgs, undefined, -1, EFAULT); + return; + } + if ( + pidOutPtr !== 0 && + !isValidMemoryRange(processMem, pidOutPtr, 4) + ) { + this.completeChannel(channel, SYS_SPAWN, origArgs, undefined, -1, EFAULT); + return; + } + let path = ""; - if (pathPtr !== 0 && pathLen > 0) { + if (pathLen > 0) { path = new TextDecoder().decode(processMem.slice(pathPtr, pathPtr + pathLen)); // Strip trailing NUL if the user copied a C string with the terminator. if (path.endsWith("\0")) path = path.slice(0, -1); @@ -9604,10 +9670,6 @@ export class CentralizedKernelWorker { path = this.resolveExecPathAgainstCwd(parentPid, path); } - if (blobLen <= 0 || (blobPtr === 0 && blobLen > 0)) { - this.completeChannel(channel, SYS_SPAWN, origArgs, undefined, -1, 22); // EINVAL - return; - } // .slice copies into a regular ArrayBuffer (TextDecoder rejects SAB views). const blobBytes = processMem.slice(blobPtr, blobPtr + blobLen); @@ -9625,6 +9687,22 @@ export class CentralizedKernelWorker { this.completeChannel(channel, SYS_SPAWN, origArgs, undefined, -1, 22); // EINVAL return; } + const metadataResult = this.validateExecMetadata( + argv, + envp, + this.getPtrWidth(parentPid), + ); + if (metadataResult < 0) { + this.completeChannel( + channel, + SYS_SPAWN, + origArgs, + undefined, + -1, + -metadataResult, + ); + return; + } // ── PRE-FLIGHT: resolve and compile BEFORE calling the kernel ── // POSIX requires file_actions to run "exactly once." `posix_spawnp`'s @@ -9673,6 +9751,27 @@ export class CentralizedKernelWorker { * validated, compiled program. Now safe to ask the kernel to build the * child (which will apply file_actions exactly once). */ + private scratchOffsetForSpawnBlob(blobLen: number): number { + if (blobLen <= SCRATCH_SIZE) return this.scratchOffset; + if ((this.largeSpawnScratchOffset ?? 0) !== 0) { + // WHY: resolution may be asynchronous, but every completion runs on this + // worker's single event loop and copy + kernel_spawn_process contain no + // await. One shared buffer therefore cannot be observed half-written. + return this.largeSpawnScratchOffset; + } + + // WHY: kernel_alloc_scratch owns its allocation for the kernel lifetime + // and has no matching free operation. Allocate the complete bounded + // transport once, then reuse it, instead of leaking one allocation for + // every large spawn or letting the host grow memory behind Rust's heap. + const allocScratch = this.kernelInstance!.exports.kernel_alloc_scratch as + (size: number) => KernelPointer; + this.largeSpawnScratchOffset = Number( + allocScratch(SPAWN_BLOB_MAX_BYTES), + ); + return this.largeSpawnScratchOffset; + } + private handleSpawnAfterResolve( channel: ChannelInfo, origArgs: number[], @@ -9685,12 +9784,30 @@ export class CentralizedKernelWorker { envp: string[], ): void { // ── Copy blob to kernel scratch ── + if ( + blobLen !== blobBytes.byteLength || + blobLen <= 0 || + blobLen > SPAWN_BLOB_MAX_BYTES + ) { + const errno = blobLen > SPAWN_BLOB_MAX_BYTES ? E2BIG : EINVAL; + this.completeChannel(channel, SYS_SPAWN, origArgs, undefined, -1, errno); + return; + } + const spawnScratchOffset = this.scratchOffsetForSpawnBlob(blobLen); + if (blobLen > SCRATCH_SIZE && spawnScratchOffset === 0) { + this.completeChannel(channel, SYS_SPAWN, origArgs, undefined, -1, ENOMEM); + return; + } const kernelMem = new Uint8Array(this.kernelMemory!.buffer); - if (blobLen > kernelMem.byteLength - this.scratchOffset) { - this.completeChannel(channel, SYS_SPAWN, origArgs, undefined, -1, 22); // EINVAL + if ( + !Number.isSafeInteger(spawnScratchOffset) || + spawnScratchOffset < 0 || + spawnScratchOffset > kernelMem.byteLength - blobLen + ) { + this.completeChannel(channel, SYS_SPAWN, origArgs, undefined, -1, EIO); return; } - kernelMem.set(blobBytes, this.scratchOffset); + kernelMem.set(blobBytes, spawnScratchOffset); // ── Ask the kernel to build the child descriptor ── const kernelSpawn = this.kernelInstance!.exports.kernel_spawn_process as @@ -9703,7 +9820,7 @@ export class CentralizedKernelWorker { const result = kernelSpawn( parentPid, callerTid, - this.toKernelPtr(this.scratchOffset), + this.toKernelPtr(spawnScratchOffset), this.toKernelPtr(blobLen), ); if (result <= 0) { diff --git a/host/test/fork-abort-unwind.test.ts b/host/test/fork-abort-unwind.test.ts index 99a9f86d6e..a2e12fb36b 100644 --- a/host/test/fork-abort-unwind.test.ts +++ b/host/test/fork-abort-unwind.test.ts @@ -145,4 +145,219 @@ describe("instrumented ABORT_UNWINDING", () => { rmSync(dir, { recursive: true, force: true }); } }); + + it("preserves recursive plain-catch ownership across abort recovery", () => { + const dir = mkdtempSync(join(tmpdir(), "kandelo-fork-abort-catch-")); + try { + const rawPath = join(dir, "abort-catch.wasm"); + const instrumentedPath = join(dir, "abort-catch.instrumented.wasm"); + const outerLocalCount = 9_000; + const outerLocalInit = Array.from( + { length: outerLocalCount }, + (_, index) => `i64.const ${index} local.set ${index}`, + ).join("\n"); + const wat = `(module + (import "kernel" "kernel_fork" (func $fork (result i32))) + (import "env" "memory" (memory 8)) + (tag $payload (param i32)) + (func $recurse (param $depth i32) (result i32) + (local $caught i32) + (block $handler (result i32) + (try_table (catch $payload $handler) + local.get $depth + i32.const 100 + i32.add + throw $payload + unreachable) + unreachable) + local.set $caught + local.get $depth + if (result i32) + local.get $depth + i32.const 1 + i32.sub + call $recurse + else + call $fork + end + local.get $caught + i32.add) + (func (export "run") (result i32) + (local ${"i64 ".repeat(outerLocalCount)}) + (local $guard i32) + ${outerLocalInit} + i32.const 7 + local.set $guard + i32.const 2 + call $recurse + local.get $guard + i32.add))`; + const watPath = join(dir, "abort-catch.wat"); + writeFileSync(watPath, wat); + execFileSync("wat2wasm", [ + "--enable-exceptions", + watPath, + "-o", + rawPath, + ]); + execFileSync(fileURLToPath(new URL( + "../../tools/bin/wasm-fork-instrument", + import.meta.url, + )), [ + rawPath, + "-o", + instrumentedPath, + ]); + + const module = new WebAssembly.Module(readFileSync(instrumentedPath)); + const memory = new WebAssembly.Memory({ initial: 8 }); + const view = new DataView(memory.buffer); + let instance: WebAssembly.Instance; + let moduleBuffer = 0; + let failGrowth = true; + let nextAddress = 65_536; + let abortCommits = 0; + let successfulCommits = 0; + let lowMemoryUntouched = false; + let retiredStorageUntouched = false; + const allocationSizes: number[] = []; + const released: Array<{ addr: number; size: number }> = []; + + // WHY: this fixture has no mutable guest globals. In the retired + // module-wide design, the plain-catch arm and payload occupied these + // two words after the linked prefix's pointer words. They let the test + // distinguish a stale-buffer write from merely clearing the buffer + // global and redirecting the same invalid write to address zero. + const scratchArmOffset = 8; + const scratchPayloadOffset = 12; + const sentinel = 0xa5a5a5a5; + const fillScratchWords = (base: number): void => { + view.setUint32(base + scratchArmOffset, sentinel, true); + view.setUint32(base + scratchPayloadOffset, sentinel, true); + }; + const scratchWordsAreUntouched = (base: number): boolean => + view.getUint32(base + scratchArmOffset, true) === sentinel + && view.getUint32(base + scratchPayloadOffset, true) === sentinel; + + const continuation = new LinkedForkContinuation( + memory, + readLinkedFrameFormat(module), + (size) => { + allocationSizes.push(size); + if (failGrowth && nextAddress !== 65_536) { + throw new ContinuationAllocationError(12, size, "injected ENOMEM"); + } + const addr = nextAddress; + nextAddress += size; + return addr; + }, + (addr, size) => released.push({ addr, size }), + "abort-catch-e2e", + ); + + const imports = { + env: { + memory, + __wpk_fork_frame_reserve: (size: number) => { + const frame = continuation.reserveFrame(size); + if (frame === 0) { + (instance.exports.wpk_fork_abort_begin as (addr: number) => void)( + moduleBuffer, + ); + } + return frame; + }, + __wpk_fork_frame_commit: (payload: number) => { + continuation.commitFrame(payload); + if (failGrowth) { + abortCommits++; + } else { + successfulCommits++; + } + }, + __wpk_fork_frame_next: (size: number) => continuation.nextFrame(size), + }, + kernel: { + kernel_fork: () => { + const state = (instance.exports.wpk_fork_state as () => number)(); + if (state === 2) { + (instance.exports.wpk_fork_rewind_end as () => void)(); + continuation.finishReplayAndRelease(); + return 17; + } + if (state === 3) { + const errno = continuation.abortErrno(); + (instance.exports.wpk_fork_abort_end as () => void)(); + continuation.finishAbortReplayAndRelease(); + return -errno; + } + + if (!failGrowth) { + // Check before beginUnwind legitimately reclaims and initializes + // the same mapping. All three recursive catches have run by now. + lowMemoryUntouched = scratchWordsAreUntouched(0); + retiredStorageUntouched = scratchWordsAreUntouched(moduleBuffer); + } + moduleBuffer = Number(continuation.beginUnwind()); + (instance.exports.wpk_fork_unwind_begin as (addr: number) => void)( + moduleBuffer, + ); + return 0; + }, + }, + }; + instance = new WebAssembly.Instance(module, imports); + const run = instance.exports.run as () => number; + const state = instance.exports.wpk_fork_state as () => number; + + // The three recursive activations commit distinct payloads (102, 101, + // 100) into the root chunk. The roughly 72 KiB outer activation then + // needs a second mapping, where allocation failure is injected. + const abortResult = run(); + expect(state()).toBe(0); + expect(continuation.hasActiveContinuation()).toBe(false); + expect(released).toEqual([{ addr: 65_536, size: 65_536 }]); + + // Once abort replay releases the root, later catch capture must own its + // state in activation locals rather than either retired storage or low + // memory. Reuse the same root address for the successful fork so this + // also covers the real allocator-reuse lifecycle. + fillScratchWords(0); + fillScratchWords(moduleBuffer); + failGrowth = false; + nextAddress = 65_536; + const unwindResult = run(); + expect(unwindResult).toBe(0); + expect(state()).toBe(1); + (instance.exports.wpk_fork_unwind_end as () => void)(); + continuation.finishUnwind(); + continuation.beginReplay(); + (instance.exports.wpk_fork_rewind_begin as (addr: number) => void)( + moduleBuffer, + ); + const successfulResult = run(); + + expect({ + abortResult, + abortCommits, + allocationSizes, + lowMemoryUntouched, + retiredStorageUntouched, + successfulCommits, + successfulResult, + }).toEqual({ + abortResult: 298, // -ENOMEM + 100 + 101 + 102 + the outer guard 7 + abortCommits: 3, + allocationSizes: [65_536, 131_072, 65_536, 131_072], + lowMemoryUntouched: true, + retiredStorageUntouched: true, + successfulCommits: 4, + successfulResult: 327, // fork result 17 + payloads + outer guard + }); + expect(state()).toBe(0); + expect(continuation.hasActiveContinuation()).toBe(false); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); }); diff --git a/host/test/plain-catch-payload-lifetime.test.ts b/host/test/plain-catch-payload-lifetime.test.ts new file mode 100644 index 0000000000..666a52b08a --- /dev/null +++ b/host/test/plain-catch-payload-lifetime.test.ts @@ -0,0 +1,402 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { + LinkedForkContinuation, + readLinkedFrameFormat, +} from "../src/fork-continuation"; + +const FIRST_PAYLOAD = 0x1234; +const SECOND_PAYLOAD = 0x5678; +// WHY: this fixture has no pre-existing mutable globals, so the old B1 +// implementation places its arm id and one i32 payload immediately after the +// two pointer words in the module prefix. These offsets name the bytes whose +// ownership is under test; the activation-local replacement must not touch +// them before a continuation exists or after that continuation is released. +const SCRATCH_ARM_OFFSET = 8; +const SCRATCH_PAYLOAD_OFFSET = 12; +const SENTINEL = 0xa5a5a5a5; + +describe("plain-catch payload lifetime", () => { + it("keeps capture state activation-owned before the first fork and after release", () => { + const dir = mkdtempSync(join(tmpdir(), "kandelo-plain-catch-lifetime-")); + try { + const watPath = join(dir, "plain-catch-lifetime.wat"); + const rawPath = join(dir, "plain-catch-lifetime.wasm"); + const instrumentedPath = join(dir, "plain-catch-lifetime.instrumented.wasm"); + writeFileSync(watPath, `(module + (import "kernel" "kernel_fork" (func $fork (result i32))) + (import "env" "memory" (memory 4)) + (tag $payload (param i32)) + (func (export "run") (param $payload i32) (result i32) + (local $caught i32) + (block $handler (result i32) + (try_table (catch $payload $handler) + local.get $payload + throw $payload + unreachable) + unreachable) + local.set $caught + call $fork + local.get $caught + i32.add))`); + execFileSync("wat2wasm", [ + "--enable-exceptions", + watPath, + "-o", + rawPath, + ]); + const instrumenterPath = fileURLToPath( + new URL("../../tools/bin/wasm-fork-instrument", import.meta.url), + ); + execFileSync(instrumenterPath, [rawPath, "-o", instrumentedPath]); + + const module = new WebAssembly.Module(readFileSync(instrumentedPath)); + const memory = new WebAssembly.Memory({ initial: 4 }); + const view = new DataView(memory.buffer); + let instance: WebAssembly.Instance; + let moduleBuffer = 0; + let firstCaptureKeptLowMemory = false; + let secondCaptureKeptLowMemory = false; + let secondCaptureKeptReleasedMemory = false; + let normalForkCalls = 0; + const continuation = new LinkedForkContinuation( + memory, + readLinkedFrameFormat(module), + () => 65_536, + () => {}, + "plain-catch-lifetime", + ); + + const scratchIsSentinel = (base: number): boolean => + view.getUint32(base + SCRATCH_ARM_OFFSET, true) === SENTINEL && + view.getUint32(base + SCRATCH_PAYLOAD_OFFSET, true) === SENTINEL; + const fillScratch = (base: number): void => { + view.setUint32(base + SCRATCH_ARM_OFFSET, SENTINEL, true); + view.setUint32(base + SCRATCH_PAYLOAD_OFFSET, SENTINEL, true); + }; + + instance = new WebAssembly.Instance(module, { + env: { + memory, + __wpk_fork_frame_reserve: (size: number) => + continuation.reserveFrame(size), + __wpk_fork_frame_commit: (payload: number) => + continuation.commitFrame(payload), + __wpk_fork_frame_next: (size: number) => + continuation.nextFrame(size), + }, + kernel: { + kernel_fork: () => { + const state = (instance.exports.wpk_fork_state as () => number)(); + if (state === 2) { + (instance.exports.wpk_fork_rewind_end as () => void)(); + continuation.finishReplayAndRelease(); + return normalForkCalls === 1 ? 7 : 11; + } + + normalForkCalls++; + if (normalForkCalls === 1) { + firstCaptureKeptLowMemory = scratchIsSentinel(0); + } else { + secondCaptureKeptLowMemory = scratchIsSentinel(0); + secondCaptureKeptReleasedMemory = scratchIsSentinel(moduleBuffer); + } + + moduleBuffer = Number(continuation.beginUnwind()); + (instance.exports.wpk_fork_unwind_begin as (addr: number) => void)( + moduleBuffer, + ); + return 0; + }, + }, + }); + + const run = instance.exports.run as (payload: number) => number; + fillScratch(0); + + // The first pass drains frames and returns the function's result default. + expect(run(FIRST_PAYLOAD)).toBe(0); + (instance.exports.wpk_fork_unwind_end as () => void)(); + continuation.finishUnwind(); + continuation.beginReplay(); + (instance.exports.wpk_fork_rewind_begin as (addr: number) => void)( + moduleBuffer, + ); + expect(run(FIRST_PAYLOAD)).toBe(FIRST_PAYLOAD + 7); + + // Once replay releases the continuation, its former bytes are no longer + // storage owned by fork instrumentation. A later plain catch must not + // mutate either that retired mapping or low memory before its fork call. + fillScratch(0); + fillScratch(moduleBuffer); + expect(run(SECOND_PAYLOAD)).toBe(0); + (instance.exports.wpk_fork_unwind_end as () => void)(); + continuation.finishUnwind(); + continuation.beginReplay(); + (instance.exports.wpk_fork_rewind_begin as (addr: number) => void)( + moduleBuffer, + ); + expect(run(SECOND_PAYLOAD)).toBe(SECOND_PAYLOAD + 11); + + expect({ + firstCaptureKeptLowMemory, + secondCaptureKeptLowMemory, + secondCaptureKeptReleasedMemory, + }).toEqual({ + firstCaptureKeptLowMemory: true, + secondCaptureKeptLowMemory: true, + secondCaptureKeptReleasedMemory: true, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("preserves each recursive activation's caught payload", () => { + const dir = mkdtempSync(join(tmpdir(), "kandelo-plain-catch-recursion-")); + try { + const watPath = join(dir, "plain-catch-recursion.wat"); + const rawPath = join(dir, "plain-catch-recursion.wasm"); + const instrumentedPath = join( + dir, + "plain-catch-recursion.instrumented.wasm", + ); + writeFileSync(watPath, `(module + (import "kernel" "kernel_fork" (func $fork (result i32))) + (import "env" "memory" (memory 4)) + (tag $payload (param i32)) + (func $recurse (export "run") (param $depth i32) (result i32) + (local $caught i32) + (local $child_result i32) + (block $handler (result i32) + (try_table (catch $payload $handler) + local.get $depth + i32.const 100 + i32.add + throw $payload + unreachable) + unreachable) + local.set $caught + local.get $depth + if (result i32) + local.get $depth + i32.const 1 + i32.sub + call $recurse + else + call $fork + end + local.set $child_result + i32.const 4096 + local.get $depth + i32.const 2 + i32.shl + i32.add + local.get $caught + i32.store + local.get $child_result + local.get $caught + i32.add))`); + execFileSync("wat2wasm", [ + "--enable-exceptions", + watPath, + "-o", + rawPath, + ]); + const instrumenterPath = fileURLToPath( + new URL("../../tools/bin/wasm-fork-instrument", import.meta.url), + ); + execFileSync(instrumenterPath, [rawPath, "-o", instrumentedPath]); + + const module = new WebAssembly.Module(readFileSync(instrumentedPath)); + const memory = new WebAssembly.Memory({ initial: 4 }); + const view = new DataView(memory.buffer); + let instance: WebAssembly.Instance; + let moduleBuffer = 0; + let normalForkCalls = 0; + let recursiveCaptureKeptLowMemory = false; + const continuation = new LinkedForkContinuation( + memory, + readLinkedFrameFormat(module), + () => 65_536, + () => {}, + "plain-catch-recursion", + ); + + instance = new WebAssembly.Instance(module, { + env: { + memory, + __wpk_fork_frame_reserve: (size: number) => + continuation.reserveFrame(size), + __wpk_fork_frame_commit: (payload: number) => + continuation.commitFrame(payload), + __wpk_fork_frame_next: (size: number) => + continuation.nextFrame(size), + }, + kernel: { + kernel_fork: () => { + const state = (instance.exports.wpk_fork_state as () => number)(); + if (state === 2) { + (instance.exports.wpk_fork_rewind_end as () => void)(); + continuation.finishReplayAndRelease(); + return 7; + } + normalForkCalls++; + recursiveCaptureKeptLowMemory = + view.getUint32(SCRATCH_ARM_OFFSET, true) === SENTINEL && + view.getUint32(SCRATCH_PAYLOAD_OFFSET, true) === SENTINEL; + moduleBuffer = Number(continuation.beginUnwind()); + (instance.exports.wpk_fork_unwind_begin as (addr: number) => void)( + moduleBuffer, + ); + return 0; + }, + }, + }); + + const run = instance.exports.run as (depth: number) => number; + view.setUint32(SCRATCH_ARM_OFFSET, SENTINEL, true); + view.setUint32(SCRATCH_PAYLOAD_OFFSET, SENTINEL, true); + expect(run(2)).toBe(0); + (instance.exports.wpk_fork_unwind_end as () => void)(); + continuation.finishUnwind(); + continuation.beginReplay(); + (instance.exports.wpk_fork_rewind_begin as (addr: number) => void)( + moduleBuffer, + ); + + // WHY: all three calls execute the same static catch arm, but each + // activation owns a distinct payload (102, 101, 100). The result slots + // prove each restored activation retained its own value; the sentinel + // proves capture did not borrow low memory before the continuation + // existed. A module-wide tuple violates that ownership boundary even if + // other frame locals happen to preserve the final arithmetic result. + expect(run(2)).toBe(7 + 100 + 101 + 102); + expect([ + view.getUint32(4096, true), + view.getUint32(4100, true), + view.getUint32(4104, true), + ]).toEqual([100, 101, 102]); + expect({ + normalForkCalls, + recursiveCaptureKeptLowMemory, + }).toEqual({ + normalForkCalls: 1, + recursiveCaptureKeptLowMemory: true, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("distinguishes plain and catch_ref state in either reuse order", () => { + const dir = mkdtempSync(join(tmpdir(), "kandelo-mixed-catch-lifetime-")); + try { + const watPath = join(dir, "mixed-catch-lifetime.wat"); + const rawPath = join(dir, "mixed-catch-lifetime.wasm"); + const instrumentedPath = join(dir, "mixed-catch-lifetime.instrumented.wasm"); + writeFileSync(watPath, `(module + (import "kernel" "kernel_fork" (func $fork (result i32))) + (import "env" "memory" (memory 4)) + (tag $plain (param i32)) + (tag $with_ref) + (func (export "run") (param $take_plain i32) (result i32) + (block $done (result i32) + (block $plain_handler (result i32) + (block $ref_handler (result exnref) + (try_table (result exnref) + (catch $plain $plain_handler) + (catch_ref $with_ref $ref_handler) + local.get $take_plain + if + i32.const 41 + throw $plain + else + throw $with_ref + end + unreachable)) + drop + call $fork + i32.const 42 + i32.add + br $done) + call $fork + i32.add + br $done)))`); + execFileSync("wat2wasm", [ + "--enable-exceptions", + watPath, + "-o", + rawPath, + ]); + const instrumenterPath = fileURLToPath( + new URL("../../tools/bin/wasm-fork-instrument", import.meta.url), + ); + execFileSync(instrumenterPath, [rawPath, "-o", instrumentedPath]); + const module = new WebAssembly.Module(readFileSync(instrumentedPath)); + + const runOrder = (modes: readonly number[]): void => { + const memory = new WebAssembly.Memory({ initial: 4 }); + let instance: WebAssembly.Instance; + let moduleBuffer = 0; + const continuation = new LinkedForkContinuation( + memory, + readLinkedFrameFormat(module), + () => 65_536, + () => {}, + "mixed-catch-lifetime", + ); + instance = new WebAssembly.Instance(module, { + env: { + memory, + __wpk_fork_frame_reserve: (size: number) => + continuation.reserveFrame(size), + __wpk_fork_frame_commit: (payload: number) => + continuation.commitFrame(payload), + __wpk_fork_frame_next: (size: number) => + continuation.nextFrame(size), + }, + kernel: { + kernel_fork: () => { + const state = (instance.exports.wpk_fork_state as () => number)(); + if (state === 2) { + (instance.exports.wpk_fork_rewind_end as () => void)(); + continuation.finishReplayAndRelease(); + return 7; + } + moduleBuffer = Number(continuation.beginUnwind()); + (instance.exports.wpk_fork_unwind_begin as (addr: number) => void)( + moduleBuffer, + ); + return 0; + }, + }, + }); + const run = instance.exports.run as (takePlain: number) => number; + + for (const mode of modes) { + expect(run(mode)).toBe(0); + (instance.exports.wpk_fork_unwind_end as () => void)(); + continuation.finishUnwind(); + continuation.beginReplay(); + (instance.exports.wpk_fork_rewind_begin as (addr: number) => void)( + moduleBuffer, + ); + expect(run(mode)).toBe((mode ? 41 : 42) + 7); + } + }; + + // WHY: the exnref table is intentionally reused. A stale non-null + // entry after catch_ref must not make the next plain activation take + // throw_ref, and a preceding plain catch must not suppress catch_ref. + runOrder([0, 1]); + runOrder([1, 0]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/host/test/spawn-blob-transport.test.ts b/host/test/spawn-blob-transport.test.ts new file mode 100644 index 0000000000..e255b37c91 --- /dev/null +++ b/host/test/spawn-blob-transport.test.ts @@ -0,0 +1,577 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + CentralizedKernelWorker, + SPAWN_BLOB_MAX_BYTES, +} from "../src/kernel-worker"; +import { + CH_DATA_SIZE, + CH_TOTAL_SIZE, + HOST_INTERCEPTED_SYSCALLS, +} from "../src/generated/abi"; + +const E2BIG = 7; +const EFAULT = 14; +const EINVAL = 22; +const EIO = 5; +const ENOMEM = 12; +const ENAMETOOLONG = 36; + +describe("SYS_SPAWN blob transport", () => { + it("uses one reusable kernel-owned buffer for blobs larger than a syscall channel", async () => { + const parentPid = 7; + const childPid = 42; + const path = new TextEncoder().encode("/bin/child"); + const envp = Array.from( + { length: 96 }, + (_, index) => `SPAWN_ENV_${index}=${"x".repeat(1024)}`, + ); + const blob = buildSpawnBlob(["child", "success"], envp); + expect(blob.byteLength).toBeGreaterThan(CH_TOTAL_SIZE); + + const processMemory = sharedMemoryFor(4096 + blob.byteLength); + const processBytes = new Uint8Array(processMemory.buffer); + const pathPtr = 256; + const blobPtr = 4096; + const pidOutPtr = 512; + processBytes.set(path, pathPtr); + processBytes.set(blob, blobPtr); + + const kernelMemory = new WebAssembly.Memory({ initial: 8, maximum: 8 }); + const kernelBytes = new Uint8Array(kernelMemory.buffer); + const generalScratchOffset = 1024; + const largeScratchOffset = 2 * CH_TOTAL_SIZE; + kernelBytes.fill( + 0xa5, + generalScratchOffset, + generalScratchOffset + CH_TOTAL_SIZE, + ); + const allocScratch = vi.fn(() => largeScratchOffset); + const kernelSpawn = vi.fn(( + actualParentPid: number, + actualCallerTid: number, + actualBlobPtr: number, + actualBlobLen: number, + ) => { + expect(actualParentPid).toBe(parentPid); + expect(actualCallerTid).toBe(parentPid); + expect(actualBlobPtr).toBe(largeScratchOffset); + expect(actualBlobLen).toBe(blob.byteLength); + expect( + kernelBytes.slice(actualBlobPtr, actualBlobPtr + actualBlobLen), + ).toEqual(blob); + return childPid; + }); + const onSpawn = vi.fn(() => new Promise(() => {})); + const channel = createChannel(parentPid, processMemory); + const worker = createWorker({ + callbacks: { + onResolveSpawn: vi.fn(async () => resolvedProgram()), + onSpawn, + }, + processes: new Map([[ + parentPid, + { channels: [channel], memory: processMemory, ptrWidth: 4 }, + ]]), + kernelMemory, + scratchOffset: generalScratchOffset, + largeSpawnScratchOffset: 0, + kernelInstance: { + exports: { + kernel_alloc_scratch: allocScratch, + kernel_spawn_process: kernelSpawn, + }, + }, + }); + const args = [ + pathPtr, + path.byteLength, + blobPtr, + blob.byteLength, + pidOutPtr, + 0, + ]; + + worker.handleSpawn(channel, args); + await Promise.resolve(); + await Promise.resolve(); + + expect(allocScratch).toHaveBeenCalledOnce(); + expect(allocScratch).toHaveBeenCalledWith(SPAWN_BLOB_MAX_BYTES); + expect(kernelSpawn).toHaveBeenCalledOnce(); + expect(onSpawn).toHaveBeenCalledWith( + parentPid, + childPid, + expect.any(Object), + envp, + ); + expect( + kernelBytes.slice( + generalScratchOffset, + generalScratchOffset + CH_TOTAL_SIZE, + ), + ).toEqual(new Uint8Array(CH_TOTAL_SIZE).fill(0xa5)); + + worker.handleSpawnAfterResolve( + channel, + args, + parentPid, + parentPid, + pidOutPtr, + blob, + blob.byteLength, + resolvedProgram(), + envp, + ); + expect(allocScratch).toHaveBeenCalledOnce(); + expect(kernelSpawn).toHaveBeenCalledTimes(2); + }); + + it("keeps ordinary spawn blobs in the existing channel-sized scratch", () => { + const blob = buildSpawnBlob(["child"], ["A=B"]); + const kernelMemory = new WebAssembly.Memory({ initial: 2, maximum: 2 }); + const scratchOffset = 1024; + const allocScratch = vi.fn(); + const kernelSpawn = vi.fn(() => 42); + const worker = createWorker({ + callbacks: { onSpawn: vi.fn(() => new Promise(() => {})) }, + kernelMemory, + scratchOffset, + kernelInstance: { + exports: { + kernel_alloc_scratch: allocScratch, + kernel_spawn_process: kernelSpawn, + }, + }, + }); + + worker.handleSpawnAfterResolve( + createChannel(7, sharedMemoryFor(65_536)), + [0, 0, 0, blob.byteLength, 0, 0], + 7, + 7, + 0, + blob, + blob.byteLength, + resolvedProgram(), + ["A=B"], + ); + + expect(allocScratch).not.toHaveBeenCalled(); + expect(kernelSpawn).toHaveBeenCalledWith( + 7, + 7, + scratchOffset, + blob.byteLength, + ); + expect( + new Uint8Array(kernelMemory.buffer).slice( + scratchOffset, + scratchOffset + blob.byteLength, + ), + ).toEqual(blob); + }); + + it.each([ + { + name: "the exact channel-size boundary", + blobLen: CH_TOTAL_SIZE, + expectedOffset: 1024, + expectedAllocations: 0, + }, + { + name: "the first byte above the channel-size boundary", + blobLen: CH_TOTAL_SIZE + 1, + expectedOffset: 2 * CH_TOTAL_SIZE, + expectedAllocations: 1, + }, + ])("selects the bounded transport at $name", ({ + blobLen, + expectedOffset, + expectedAllocations, + }) => { + const blob = new Uint8Array(blobLen).fill(0x5a); + const kernelMemory = new WebAssembly.Memory({ initial: 4, maximum: 4 }); + const allocScratch = vi.fn(() => 2 * CH_TOTAL_SIZE); + const kernelSpawn = vi.fn(() => 42); + const worker = createWorker({ + callbacks: { onSpawn: vi.fn(() => new Promise(() => {})) }, + kernelMemory, + scratchOffset: 1024, + largeSpawnScratchOffset: 0, + kernelInstance: { + exports: { + kernel_alloc_scratch: allocScratch, + kernel_spawn_process: kernelSpawn, + }, + }, + }); + + worker.handleSpawnAfterResolve( + createChannel(7, sharedMemoryFor(65_536)), + [0, 0, 0, blobLen, 0, 0], + 7, + 7, + 0, + blob, + blobLen, + resolvedProgram(), + [], + ); + + expect(allocScratch).toHaveBeenCalledTimes(expectedAllocations); + expect(kernelSpawn).toHaveBeenCalledWith( + 7, + 7, + expectedOffset, + blobLen, + ); + }); + + it("accepts the exact whole-blob transport maximum", () => { + const blob = new Uint8Array(SPAWN_BLOB_MAX_BYTES); + const largeScratchOffset = 1024; + const requiredBytes = largeScratchOffset + blob.byteLength; + const pages = Math.ceil(requiredBytes / 65_536); + const kernelMemory = new WebAssembly.Memory({ + initial: pages, + maximum: pages, + }); + const kernelSpawn = vi.fn(() => 42); + const worker = createWorker({ + callbacks: { onSpawn: vi.fn(() => new Promise(() => {})) }, + kernelMemory, + scratchOffset: 0, + largeSpawnScratchOffset: 0, + kernelInstance: { + exports: { + kernel_alloc_scratch: vi.fn(() => largeScratchOffset), + kernel_spawn_process: kernelSpawn, + }, + }, + }); + + worker.handleSpawnAfterResolve( + createChannel(7, sharedMemoryFor(65_536)), + [0, 0, 0, blob.byteLength, 0, 0], + 7, + 7, + 0, + blob, + blob.byteLength, + resolvedProgram(), + [], + ); + + expect(kernelSpawn).toHaveBeenCalledWith( + 7, + 7, + largeScratchOffset, + SPAWN_BLOB_MAX_BYTES, + ); + }); + + it("returns ENOMEM without touching the kernel when large transport allocation fails", () => { + const blob = new Uint8Array(CH_TOTAL_SIZE + 1); + const completeChannel = vi.fn(); + const kernelSpawn = vi.fn(); + const worker = createWorker({ + callbacks: { onSpawn: vi.fn() }, + completeChannel, + kernelMemory: new WebAssembly.Memory({ initial: 2, maximum: 2 }), + scratchOffset: 1024, + kernelInstance: { + exports: { + kernel_alloc_scratch: vi.fn(() => 0), + kernel_spawn_process: kernelSpawn, + }, + }, + }); + const channel = createChannel(7, sharedMemoryFor(65_536)); + const args = [0, 0, 0, blob.byteLength, 0, 0]; + + worker.handleSpawnAfterResolve( + channel, + args, + 7, + 7, + 0, + blob, + blob.byteLength, + resolvedProgram(), + [], + ); + + expect(kernelSpawn).not.toHaveBeenCalled(); + expect(completeChannel).toHaveBeenCalledWith( + channel, + HOST_INTERCEPTED_SYSCALLS.SYS_SPAWN, + args, + undefined, + -1, + ENOMEM, + ); + }); + + it("rejects a large transport allocation outside kernel memory", () => { + const blob = new Uint8Array(CH_TOTAL_SIZE + 1); + const completeChannel = vi.fn(); + const kernelSpawn = vi.fn(); + const kernelMemory = new WebAssembly.Memory({ initial: 2, maximum: 2 }); + const worker = createWorker({ + callbacks: { onSpawn: vi.fn() }, + completeChannel, + kernelMemory, + scratchOffset: 1024, + kernelInstance: { + exports: { + kernel_alloc_scratch: vi.fn( + () => kernelMemory.buffer.byteLength - blob.byteLength + 1, + ), + kernel_spawn_process: kernelSpawn, + }, + }, + }); + const channel = createChannel(7, sharedMemoryFor(65_536)); + const args = [0, 0, 0, blob.byteLength, 0, 0]; + + worker.handleSpawnAfterResolve( + channel, + args, + 7, + 7, + 0, + blob, + blob.byteLength, + resolvedProgram(), + [], + ); + + expect(kernelSpawn).not.toHaveBeenCalled(); + expect(completeChannel).toHaveBeenCalledWith( + channel, + HOST_INTERCEPTED_SYSCALLS.SYS_SPAWN, + args, + undefined, + -1, + EIO, + ); + }); + + it.each([ + { + name: "path range", + args: (memoryBytes: number) => [ + memoryBytes - 2, + 4, + 256, + 40, + 128, + 0, + ], + errno: EFAULT, + }, + { + name: "blob range", + args: (memoryBytes: number) => [64, 4, memoryBytes - 8, 40, 128, 0], + errno: EFAULT, + }, + { + name: "pid output range", + args: (memoryBytes: number) => [64, 4, 256, 40, memoryBytes - 2, 0], + errno: EFAULT, + }, + { + name: "fractional blob length", + args: (_memoryBytes: number) => [64, 4, 256, 40.5, 128, 0], + errno: EINVAL, + }, + { + name: "empty blob", + args: (_memoryBytes: number) => [64, 4, 256, 0, 128, 0], + errno: EINVAL, + }, + { + name: "truncated blob header", + args: (_memoryBytes: number) => [64, 4, 256, 39, 128, 0], + errno: EINVAL, + }, + { + name: "PATH_MAX-byte path", + args: (_memoryBytes: number) => [64, 4096, 256, 40, 128, 0], + errno: ENAMETOOLONG, + }, + ])("rejects an invalid $name before resolution", ({ args, errno }) => { + const memory = sharedMemoryFor(65_536); + const bytes = new Uint8Array(memory.buffer); + bytes.set(new TextEncoder().encode("/bin"), 64); + bytes.fill(0, 256, 296); + const channel = createChannel(7, memory); + const completeChannel = vi.fn(); + const onResolveSpawn = vi.fn(); + const worker = createWorker({ + callbacks: { onResolveSpawn, onSpawn: vi.fn() }, + processes: new Map([[7, { channels: [channel], memory, ptrWidth: 4 }]]), + completeChannel, + }); + const syscallArgs = args(memory.buffer.byteLength); + + worker.handleSpawn(channel, syscallArgs); + + expect(onResolveSpawn).not.toHaveBeenCalled(); + expect(completeChannel).toHaveBeenCalledWith( + channel, + HOST_INTERCEPTED_SYSCALLS.SYS_SPAWN, + syscallArgs, + undefined, + -1, + errno, + ); + }); + + it("rejects a whole spawn representation above its explicit transport bound", () => { + const memory = sharedMemoryFor(65_536); + const channel = createChannel(7, memory); + const completeChannel = vi.fn(); + const onResolveSpawn = vi.fn(); + const worker = createWorker({ + callbacks: { onResolveSpawn, onSpawn: vi.fn() }, + processes: new Map([[7, { channels: [channel], memory, ptrWidth: 4 }]]), + completeChannel, + }); + const args = [0, 0, 256, SPAWN_BLOB_MAX_BYTES + 1, 0, 0]; + + worker.handleSpawn(channel, args); + + expect(onResolveSpawn).not.toHaveBeenCalled(); + expect(completeChannel).toHaveBeenCalledWith( + channel, + HOST_INTERCEPTED_SYSCALLS.SYS_SPAWN, + args, + undefined, + -1, + E2BIG, + ); + }); + + it("enforces exec's per-entry ARG_MAX transport contract before resolution", () => { + const blob = buildSpawnBlob(["child"], [`A=${"x".repeat(CH_DATA_SIZE)}`]); + const memory = sharedMemoryFor(4096 + blob.byteLength); + const bytes = new Uint8Array(memory.buffer); + const path = new TextEncoder().encode("/bin/child"); + const pathPtr = 256; + const blobPtr = 4096; + bytes.set(path, pathPtr); + bytes.set(blob, blobPtr); + const channel = createChannel(7, memory); + const completeChannel = vi.fn(); + const onResolveSpawn = vi.fn(); + const worker = createWorker({ + callbacks: { onResolveSpawn, onSpawn: vi.fn() }, + processes: new Map([[7, { channels: [channel], memory, ptrWidth: 4 }]]), + completeChannel, + }); + const args = [ + pathPtr, + path.byteLength, + blobPtr, + blob.byteLength, + 0, + 0, + ]; + + worker.handleSpawn(channel, args); + + expect(onResolveSpawn).not.toHaveBeenCalled(); + expect(completeChannel).toHaveBeenCalledWith( + channel, + HOST_INTERCEPTED_SYSCALLS.SYS_SPAWN, + args, + undefined, + -1, + E2BIG, + ); + }); +}); + +function createWorker(overrides: Record): any { + const worker = Object.assign(Object.create(CentralizedKernelWorker.prototype), { + kernel: { toKernelPtr: (value: number | bigint) => Number(value) }, + callbacks: {}, + processes: new Map(), + channelTids: new Map(), + hostReaped: new Set(), + sharedMappings: new Map(), + tcpListenerTargets: new Map(), + tcpListenerRRIndex: new Map(), + tcpListeners: new Map(), + epollInterests: new Map(), + completeChannel: vi.fn(), + ...overrides, + }); + const kernelInstance = worker.kernelInstance ?? { exports: {} }; + worker.kernelInstance = { + ...kernelInstance, + exports: { + kernel_get_process_exit_signal: vi.fn(() => -1), + ...(kernelInstance.exports ?? {}), + }, + }; + return worker; +} + +function createChannel(pid: number, memory: WebAssembly.Memory): any { + return { + pid, + memory, + channelOffset: 0, + i32View: new Int32Array(memory.buffer), + consecutiveSyscalls: 0, + }; +} + +function sharedMemoryFor(requiredBytes: number): WebAssembly.Memory { + const pages = Math.ceil(requiredBytes / 65_536); + return new WebAssembly.Memory({ + initial: pages, + maximum: pages, + shared: true, + }); +} + +function buildSpawnBlob(argv: readonly string[], envp: readonly string[]): Uint8Array { + const encoder = new TextEncoder(); + const argvBytes = argv.map((value) => encoder.encode(`${value}\0`)); + const envpBytes = envp.map((value) => encoder.encode(`${value}\0`)); + const headerBytes = 40; + const offsetsBytes = (argv.length + envp.length) * 4; + const stringsBytes = [...argvBytes, ...envpBytes] + .reduce((total, value) => total + value.byteLength, 0); + const blob = new Uint8Array(headerBytes + offsetsBytes + stringsBytes); + const view = new DataView(blob.buffer); + view.setUint32(0, argv.length, true); + view.setUint32(4, envp.length, true); + + let stringsCursor = 0; + let offsetCursor = headerBytes; + const stringsStart = headerBytes + offsetsBytes; + for (const value of [...argvBytes, ...envpBytes]) { + view.setUint32(offsetCursor, stringsCursor, true); + offsetCursor += 4; + blob.set(value, stringsStart + stringsCursor); + stringsCursor += value.byteLength; + } + return blob; +} + +function resolvedProgram() { + const bytes = Uint8Array.from([ + 0x00, 0x61, 0x73, 0x6d, + 0x01, 0x00, 0x00, 0x00, + ]); + return { + programBytes: bytes.buffer, + programModule: new WebAssembly.Module(bytes), + argv: [], + }; +} diff --git a/packages/registry/program-packages.json b/packages/registry/program-packages.json index 8b803acae0..5f587af03b 100644 --- a/packages/registry/program-packages.json +++ b/packages/registry/program-packages.json @@ -4,127 +4,127 @@ "bash": { "manifestSha256": "6478060f28d430d18a6ebe7c603392d35a41d9bcf6bc74322351d1450b9c5335", "cacheKeys": { - "wasm32": "830ea09a72abe74ae1e3e728d80740f330250e1be2c52a9f1e053f151d15dc3c", - "wasm64": "db84b3df4280116b4206a96bb9b18fc30ab523dbe790439c405258c49000bfe0" + "wasm32": "1d813d7f9db4979fc5c0eef6b37e213cb73a42395757cc92ef8ebeee2e62913d", + "wasm64": "84099d7abfb7265061070b6782d0094b21372afd94058c16d1f82467d9ab8da8" } }, "bc": { "manifestSha256": "a65661463bb7047b91ff00153bd99fd962c96e571934ab4e923f5215fb6ecfbd", "cacheKeys": { - "wasm32": "87d9afde76b4f3d9ba5ca78fe38f55cdf86b312bdce8320a5c17f8b0dbbc1b38", - "wasm64": "c0bfc2c66ecf9c0e0350739120887b53da6023627105323a0ef97ac6748899ec" + "wasm32": "2e7bb9ff2a2cd8dd3b28d46c7d733f873d764820ff023a736a41cc1e16745207", + "wasm64": "42504f670c8c9ad8b6ddfcfbe2a9be6dc2110b4c5d976ff2d5858adeb7c510d5" } }, "bzip2": { "manifestSha256": "59e1e53f5675e7c9148634933ed0f4c7192743727025cae4e843b6924c489abf", "cacheKeys": { - "wasm32": "09122fb04b306e4f803ca3a461e075091be99e9bf17835a19d7013a803322a2f", - "wasm64": "fd73f37ad6d1a7254a402c5fba805abe5c5f01d2cf9e360a3b51ea51926beb2a" + "wasm32": "bd59c26ea552d64a009bebb2d0150b0e2003e227f0bdfe9f0a47a3ae97f9a336", + "wasm64": "ee810512cf9779fa0d7235a25dbe64cb0180b125f1747568cf22cb627eb8f8f8" } }, "coreutils": { "manifestSha256": "b8baabc9af9283434e0d80f7fdc0aaef242ffa31092ab0fc39a0603a12b3cca7", "cacheKeys": { - "wasm32": "72765db88ce28cb67eb7b38a545b40e004215d7ccbb0d4eb02419c509b8d46cf", - "wasm64": "837bdc6360d3481994ccb24c2fb12fd509ce70f445582a1ca32b0b77101dd3ad" + "wasm32": "33ca2b6c08b61b6ba3b013b13271c98b9b63d75f0a5c47d6d265c27c10c50458", + "wasm64": "67ffde57ec940c7107d901231ed2d1c886957edc34de1c60c08094d1260ae797" } }, "cpython": { "manifestSha256": "7dd4f446697a73941ec940c2ccba4d53be73fe6947f1e701031a4d0aa964c4ff", "cacheKeys": { - "wasm32": "314d476303cbf1886be4bf31ee1e21c9012bc2056222766f89ec33d9845f974e", - "wasm64": "31a23842219a401b6708940869b9ba614e35cd69d98f33315e625254074f993f" + "wasm32": "3395181397427732850ece33d0fd3edf875903acb625e59a348577f2613d70f6", + "wasm64": "31b01d999dfbde5bce591d3511418fc84f6ed7042b9d73a1a2e0b4fe8a69920f" } }, "curl": { "manifestSha256": "55523d50261f46dc4aaaff458d1cd87c6f96eaecd687a7540ead35c96906366e", "cacheKeys": { - "wasm32": "63cca65e3571ba910489e52d3c0ec424570a8ebfa08b3b3e24c39747adcd51da", - "wasm64": "bbc392fd90a456565cd3d7e1d38d8b1a3cc5cfe5a9e7619270fd30bf5b94c3f2" + "wasm32": "44aa318c3c067dc7183b20535ac525637df817431351c244f6bc37b029792a84", + "wasm64": "ca4d46d318391dbb76e16b1a75c7eb3a5aa034b3214b936560e1857a6c4f4e2a" } }, "dash": { "manifestSha256": "10ce1bb611fefd78a6c9ec9038f6113e29a7062b1a73e08eccf9b33004cea9a8", "cacheKeys": { - "wasm32": "0b2035e2d9b130f8edc85024720502b612e52db3c71c0c35ed55e26140515a93", - "wasm64": "981f288954ad9124917838d3deaf8514853a168f0f7c88c373b98aeafbebaeb0" + "wasm32": "2ec3ac4e8baa4bfb4ced00ff411feb0ae28e2acf3b5709ac7c042c5a57295aa6", + "wasm64": "55133070386497aa605c431cf06777d717e4c4ececf2983cc507b858c4b529cd" } }, "diffutils": { "manifestSha256": "3a78f0a46bae43ce5ea235c6638c2cbd1d8559b0b62b1fb42443b35968c1aca3", "cacheKeys": { - "wasm32": "5b829e0fee62ddcc82e67563f559482c77be4ad2a794b297d614a9ac0bfb96a2", - "wasm64": "6ae76b9d6a0cea809b7249e9957f8aa0d96a930eb06906a8ea345b3126ff0aa2" + "wasm32": "5fd58502ff1d28c11cc7941a2f21d5ab2d9f40bfbe113f766d274361ab5e0a88", + "wasm64": "f10a466f469db4ac17f9be69e0441b238c649c27f0d2869a6df23d0196d3ba64" } }, "dinit": { "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", "cacheKeys": { - "wasm32": "28167e431807693bad67358e88d98cfb0603fd2d6efd9808bb051544846c5364", - "wasm64": "ce140644a3a60989c426590afbc252ed9b2042e7d810183b207dedb8c9f25738" + "wasm32": "43651e927c2672e0a4f749a5cce47db470dd19d72736ea4c6b6a6b3d55d9f7ab", + "wasm64": "bbcd2d163560d0271c38581dae6f292cc4a119fc8f86af85552310157bef511a" } }, "erlang": { "manifestSha256": "475d6037e73a0f1e2f4381067194b2422751206979ea17209e10efa5a2a93bbb", "cacheKeys": { - "wasm32": "fb022926bb648059ad387c1919207500dbdfd359eed86fb38f671584f2ea97e5", - "wasm64": "b8c4c2cccba15fa5662e0d20b99341c4afd6bea2488d686d48ca7b85eea122c6" + "wasm32": "ff0aaf3f73b5c6186a6b72544d245bf4b4021021266430803ce781c2d3128856", + "wasm64": "ab7b04e55de36ba5515ce2b4cf6d831ed8d6dcedabeb3c52177b3042f08b107b" } }, "erlang-vfs": { "manifestSha256": "15efb74f1825e2b85bedfeb8d98c19fa648c4be39cb9defb65330a8f21eb9141", "cacheKeys": { - "wasm32": "fd19b68ecacbba3881e594e17360a68b1b39800ddb2710b8a9f6ad48b3480faf", - "wasm64": "668be5f4ec2410bfac9f0a307b953dbaad8a44b639f9f206b79503cdb09256f5" + "wasm32": "af8826989737cc530b222a7049f2ec7dcb628adfc5b8bff543afe04b21f9bae7", + "wasm64": "58c34283c08d722d67737f4392c3151aee964cda18c06fe034839daace38feb1" } }, "fbdoom": { "manifestSha256": "a00e0d9c84fcdbb3bd95f296cb3422d60b86dcff4c40734eea1bb0bec4c7d902", "cacheKeys": { - "wasm32": "ad115c1c6aba37e0b0246d728ec5343e931bc0b4b00c1fe8e24bf2e881399ce3", - "wasm64": "a94ea6497362a75f9341ff791856d7e5e7c572ccc7cd1eae2a299033c33e9ec2" + "wasm32": "7ec10e23738b06c8fcd6b1953b252476522a01c5b9cea19c0b16bea88217a712", + "wasm64": "9acd1242e676f6a897861d740aa0b14136e50ac611ba1a40dc9edb63d89c86e1" } }, "file": { "manifestSha256": "7874c1affcbbf2c8ab8c8d4beb087f275af57b5caae6a8947bf5a63a181552b2", "cacheKeys": { - "wasm32": "41f1cbea97d2331099f6ec8f24ad10c15d54d536dca8c5d96354d053685333f1", - "wasm64": "a0e3c4589201e83581916ea7028b90114c42cf05f508d0c3c4ecdba1fd00abdb" + "wasm32": "4c75af58729ffafa2d5ff73b3522aacb97465e1a85e7163274ff0994d30017c3", + "wasm64": "cd5c07e821687325b602fecad1d92eaef7b68a0018c9329bd3c0d753dba83a68" } }, "findutils": { "manifestSha256": "cceedf52aea67fbb0da03cfce6f2e9b1a7b5561fa1656c2762b43c651a625d02", "cacheKeys": { - "wasm32": "9cf821c87b60d798687583dbb8e325ba26d10764f2404b01d15bd296f5630332", - "wasm64": "586f92247cb3b6d4628f8a40b4309e65709b4a140a7b64274f08f25f018d8bb9" + "wasm32": "e9894ce98cd2055fa27987b544eb72df96fc824ce7c9372d1db4294879985ac1", + "wasm64": "69651dede1cf19f3503a8e4a5f3f14c09e1909bd36aab8229795fcd3ddc4a520" } }, "gawk": { "manifestSha256": "a2567b8b0778805e385e1d3a41ac8b5d74aaf9d293b222001b17d09b11e5a0ba", "cacheKeys": { - "wasm32": "f25ff8e149130b153b32c462696ed336e03a217e5bf9098aefb31f62cb96cdb8", - "wasm64": "2b19a7621e1f262e81b925ef4cd366d850a4c61dd574151d7417df8f4b358b08" + "wasm32": "85ef6c0fdac5fa9ac1ab10bb1a057038e6d08015e571b8b36eb2c89505e1917f", + "wasm64": "4e109db7864e9c1b5f5497d1a71d6a34ec921969c5853d2dfbc7a7e38718cab6" } }, "git": { "manifestSha256": "c2bc79e62c9a4e840ae94a16af0f41070460eaf3976a990dc60d2db977bee952", "cacheKeys": { - "wasm32": "956b61b3c170c0be48a6b482941cefd2bbe895cec099f5c748969c71b2e37ee8", - "wasm64": "3271f00c4954641a4d37e10e9cc5bb4edb56caaf8da103a9525deb09b742b569" + "wasm32": "1f940f813db52db5777a114926c9a143eda473ed9d232bcbd3cd196f3e3ff0b4", + "wasm64": "c2596e6e748741ef33ac5ed340be7ebe2bb3c21c419b95ad2dd6e548ef9ce63f" } }, "grep": { "manifestSha256": "59270d3bfb33167b32d13246bf855b49308f8c9c0a66d9635c804f702421fbc6", "cacheKeys": { - "wasm32": "2b95ab13353b8f112cd0eeb7ffcd01f745ec00814bf531f97bb4ec1f85d3a77f", - "wasm64": "0e5b2c50bcbeb21620c8876e305615e0ccf3100c23d72bda28a0383e903a4541" + "wasm32": "ccfc4ba22252a0d57c55ed8ada09dff22883fb35a37a7ea1efa395bcb405a748", + "wasm64": "15300a1a26b74ff6ee98c84a2c64a2797f61fd7b3fd5e5357ddab7ed4a491db3" } }, "gzip": { "manifestSha256": "33853ebe2301caf2979b667830e6c5afc5256f8717137624f08079e6a27f370a", "cacheKeys": { - "wasm32": "c5fea74ec33f362e863e9d99f5d4719cc369ec754884d8183ded6ebce6c346ba", - "wasm64": "fae1b65fef4de74f0afafd78ff5220112e76fe4b8b7f0d3fd907c63b991e6727" + "wasm32": "46f2e656c573c0311dd0e820c58632e59d29f80745bd6bd9a1f28cc082c470df", + "wasm64": "b49e8812bbd6c08fbac02a82c7837d6697eccc2934c25adf5d221bfc40f6bca3" } }, "homebrew-bootstrap": { @@ -144,29 +144,29 @@ "kandelo-sdk": { "manifestSha256": "c081879f1becd855917cff96f17429bcf2b9fd61bf95211eca9ff8fb625bb2eb", "cacheKeys": { - "wasm32": "5ec342f97928be06680f16f6bcb621b5265e3375a4d602f8a0e78b3adb1b79cc", - "wasm64": "69f5a9a871203842c5042886f036d52be61e6ed9c4146b8a8cac1da1e3c1980a" + "wasm32": "c14c5b31bdea4e86f612a88b34102f831be3f09a189faebaf12b0777920ed647", + "wasm64": "71923a448f3c26d321943d9479462e6587b10c47d083c7b36c9cdc528b1094b2" } }, "kernel": { "manifestSha256": "db1d66db8575562ac7b3e720dbaa06d7dd5eef577d80220ea4167dc2d69b863b", "cacheKeys": { - "wasm32": "d0e03c4b0ab4858f28cb0d74e4a38b6d0af49781a6c53e12d14baade637a7ec9", - "wasm64": "f59073d7dec9a33149bc57cbbbc1a7a225b2b9d2295f64c4fea0041f7e2f410f" + "wasm32": "f864c4947733ce7a9c1bd6782038da97680db641c03b7696db690dac065e6436", + "wasm64": "593c2103aafab94813266312952e11752195ab635711e6e94681cd0da90983ec" } }, "lamp": { "manifestSha256": "250b64635d64f8537178188fb5488dbfd2980d79a1c2ffbf346af67008088ba0", "cacheKeys": { - "wasm32": "3b207f25a7c0c143b0e0a5aec0e828bdac1a4125d46bf1d019718db529337d7e", - "wasm64": "f8b7b64c9de2a3034c86a4bcc6542970f62c48b40ca21bd6f31212118645de71" + "wasm32": "b7b5b9e0bb1298d84322990bc7122693b42f07200ba95d289e35f6cf25d35171", + "wasm64": "30c7fb0cd279a07e679e72657428dff187c7cde67f9f0f6ef44df48c0f1c9e20" } }, "less": { "manifestSha256": "996d61545cfe83dcb663a33e8763e0edbd4db4a605fd69a91b243cf79b1f9b17", "cacheKeys": { - "wasm32": "69fd5aea1025f2974ce24964d3f93ba08df5f5082c48fd5e2f5c101df852cc07", - "wasm64": "9d9870cd7b66c39e23dcfbf8998468e431156ca37dec9329bd425b7c5691e0a9" + "wasm32": "9a509fe274611ce618bb65afde230b2e526adfed9f908dd286d63749bf924547", + "wasm64": "a5fe1e95f40aac2c65b24ab6acb1459974888d69d9caf7d61fcb3dbfe8f7422c" } }, "libcurl": { @@ -214,113 +214,113 @@ "lsof": { "manifestSha256": "cfd199bbe082435f7a2e703e2738a368af1eeb5b76b6e93c376054f21ce94419", "cacheKeys": { - "wasm32": "0cbea55dc47e3ce5196d9b07cfa6a22a21fc80b87ba971fcac3f235bcfdfd6d5", - "wasm64": "6047711ddb7300e1bdbb539ab0184099919c05542bc2ca9f94e86f6e0f4489a3" + "wasm32": "3660780917e27528d76a83fb1ae76f3a547fe348cd1ba979bc9f015a03c1213b", + "wasm64": "3cd130d749e8f4010848fada35b7043715e048faa8584a4f11b2b3d14bc83700" } }, "m4": { "manifestSha256": "2c6582d99d6eabfb9da52badf49b899e9fdcba76a728536b25bc3741f3331543", "cacheKeys": { - "wasm32": "746a3cb4942a7375f68e1b8cfdc24e71dae6b1393da3b8bef29386af1af10302", - "wasm64": "99eb4e7a854e414f7473480957a8f785b6ec957e5ace06b8aacbe88c4855c470" + "wasm32": "a7063dcf8ce5bfe288b1520beac1eaf1d19f736fbdd683f344d83d74dcbab727", + "wasm64": "11d165cf45a57702bf92d4e48cf7e3df92b0e68888a345b56a6c5de0302764d5" } }, "make": { "manifestSha256": "f878d2d730f36a4c6dfe1fff1b4ccc704757ea22d8c4c8ccb95b11cd641e5fed", "cacheKeys": { - "wasm32": "8bbaab344db8e332b3763ce4700af83beb4ca8f6aa6670cce47f5ee9bd420899", - "wasm64": "aeeb35e7884e1b1b401454ac5a291ed0baa047354ea1016ffc3586a0b0dd42ce" + "wasm32": "49a0f581bbc70db8db660c144bbaf5683ea30f36f305c434c5e4d772c334723e", + "wasm64": "ba775a7a716319c326836f8eda62ec97b5407ec7fb64b24630070444af6b2aad" } }, "mariadb": { "manifestSha256": "aeb221be233e4b57bccb4b3375020f94c3ec593dac2734631f00e3b1aec9f48c", "cacheKeys": { - "wasm32": "89cb71f508decd03f2b38c57e3f071c00a8d0b12d365f0f98cebabbda13cec79", - "wasm64": "f6668fed09920d9e3526da63be09a9fc9650880341e11e5d87d93c32e9cef494" + "wasm32": "769572acd89a8fa7738058f6c2f48e5270868d1cc1422c4a0610b100cf851f77", + "wasm64": "815139d5343320f5a8ca5aeaff9421abb2ff6e568b13a90c2e5cc341e2ff5aa5" } }, "mariadb-test": { "manifestSha256": "d4ccce161d659a5a87c80fa1117054bbccea870e0d4a46bd8a070d3930ad0e3f", "cacheKeys": { - "wasm32": "6793c1e49769dedf80c692595db76abc462b9e6acfbdc2c37d425616a6dbdfe3", - "wasm64": "225144a55f4bda151a0518cc5ff777e3ff008026aebd8488970196fb83716993" + "wasm32": "b70268f40e5f5e45c040132373266ddee6ad38fab897a60afcc47ea12ad0cf2a", + "wasm64": "e89252da9da6afc71d2f3082719b62725c8d21ab5f307e8f2d83c99054d76fa6" } }, "mariadb-vfs": { "manifestSha256": "26e74ac84d89b2a839ed72783437061a23022ef2f88ad0f52b6aacd0442ee1cf", "cacheKeys": { - "wasm32": "8b7eb8e415bb55058e4d1c2ab8f027aaab704238a94f51e08e753ec359e7e7d7", - "wasm64": "c71c7f72d38ec16f4342158464a0a76556a78e96746e4c633c6b3365ed125b60" + "wasm32": "314fcb39160d1792c099471f02523816ec0f539f2085d425a04b4862b4515d43", + "wasm64": "30f2b70fb885278907dfc2a47d287d5ebe4ef61663f46c9ae6546401bedebe03" } }, "modeset": { "manifestSha256": "36a8683c1c7309701d361c5f77e543a6c1531397b26c72dbb864528c59c42954", "cacheKeys": { - "wasm32": "8a5f567e2f8c33df0d1759f3e7cbeb0e6105044fd4b156855a9ebf79cde6bced", - "wasm64": "2ee657f174d5b6dae244226ea17b4a39b40eda6f284f145a29aa259f1b308610" + "wasm32": "e61f328544530bf45ff0a339e0ed552c7c235bab8fa104e79c297ff9cea0d96e", + "wasm64": "a2a9f75ce10229a41ed2d3ada84a82f3bcd7caba4d5553a7ffb70572a3d12ed6" } }, "msmtpd": { "manifestSha256": "09de04a422ddf631a29a259d816e081f8d317d1077808ede55d8c500baae748f", "cacheKeys": { - "wasm32": "94c0ffb6de71a0cbffd57cbe6979ad7438b16b0ca7b32b46cce86bf65fa1d5a6", - "wasm64": "d2f5fde97e49635bbffc8d7ef44aec792a0ce2bbd39c6f5c806eac2f51f954f1" + "wasm32": "82b32c29b05f3854bebfb414875d603fe66829ab79b0ea02b4a1d00e09fe3a52", + "wasm64": "a4d308062b45985c514b5c87d6d7c682c0ceef539f36436d63bbfc0292ae80c3" } }, "nano": { "manifestSha256": "1ba6d340c95581319982257afd6a3554b333b880a7e69991b8c573da883f86c4", "cacheKeys": { - "wasm32": "48c6e796b2e94c9a4b9e56890b1a04e7df245263464df19e6c19d4676167e7fd", - "wasm64": "65436ed7799f619a181956aa3e54b24f0512253d4aa60206ae10008098b87011" + "wasm32": "ee0b8eacc8659f5d690f5b3ee419584ae24403fe6b8e955e01e5192e6a73b635", + "wasm64": "48c7c4a66eb08ac6b77a31ab124b427b3bc0ffde398c9c05c40357379544ca5e" } }, "ncurses": { "manifestSha256": "0a1c180eeade627e204aa47065dbfebfcdc9359d25e2db436932c7fa3a86fc1e", "cacheKeys": { - "wasm32": "e95f67690426e286783716ea6e0b39edf44bca23e8f7fd225567a45109c13400", - "wasm64": "6e0707760f3fcb68e2250b2e1701b7a5dbc50f7f5091ba6cd7e078c99921678e" + "wasm32": "1d43a87fe208ed807f1e71640e4fcf7c4417699c318780e49a56c58d37e54ebb", + "wasm64": "a9ac8bb376e464cc09e0f916efe67c17d5c6ebba4aa08d8e49c30d26638c02a8" } }, "netcat": { "manifestSha256": "4cf33cd1ab768b3ad0108da8b68c2ff72e98469cd86a0bf2d0da54a7d4f6fa16", "cacheKeys": { - "wasm32": "3d57ef272670ff5652a26adb0dbafc55ba34bdd0726b75e64bb8b3cafb65555b", - "wasm64": "07cf97ce5e5223dbd34e7f5137fea620625d6f211bcd9a0d7053622ca7ef4b0b" + "wasm32": "e1f8dbb4e078921b75b109603c6b67a8035b3301659af937960841e563db2151", + "wasm64": "b013d1080a30b93e86e639c3c8ac0c6d63cc58be72bba3052140ce4e0c0881fa" } }, "nethack": { "manifestSha256": "1a2f12ec2770bd8d40006d6c878a3493b97c71c2bb63ad08c7f6ad99bfd53504", "cacheKeys": { - "wasm32": "35bd109acb9812c033d6d96996f5d00020554b1cee059811eec89b7aba5c1455", - "wasm64": "24102c2e6dbe5911533b42f8fb41ea83a7cc738a0b39d30356774f6767826564" + "wasm32": "0acaa903f47da0a7730a75b9296217ce4723c5d4a53bff772ae18a1f0acc190f", + "wasm64": "2dd42758fee3163011477d852d7b335db98c181744f7fa0db5129846a2bc27f4" } }, "nethack-browser-bundle": { "manifestSha256": "b8294981da7ce4299dfb271d4aecb90ababa97a4d1acc9b716ae05bbe52beb53", "cacheKeys": { - "wasm32": "8185e934440c3ff8fafe718d15028ab5dbd5c345a2f5517a2bc1dade96ca2154", - "wasm64": "76bef8bb896c85ae77da653130b6e9a135fd640badce83ddca4f1f517bcf1580" + "wasm32": "a02b8fc8563203eab00795dfd2474d85769857ece24d9ce0e818c7bd8c56bca5", + "wasm64": "ef4a488fb6c78ebd5b79fbc43f2f208d148c4942e69295372cc99420cb487300" } }, "nginx": { "manifestSha256": "41b0aaf05fdd0d8da42642703f9e60fd7b9545d8029896918b87c771c359147b", "cacheKeys": { - "wasm32": "cc1c9c86ad57a4b5a5ed23636c6c50a354f85e294cfc6addf0c9457cb33defc1", - "wasm64": "60e41f39704edd2c61ff3bb88b77d38913258e26c411fcf72cca8df6426e378e" + "wasm32": "6a92877569b9bf8dabd5333d3f0d5e96388379e28ec00f4d53e0ee675cb58eae", + "wasm64": "f55b140ba6604b5eab3a374514325e46dbd53edd0db3e512ed2255713e06d403" } }, "nginx-php-vfs": { "manifestSha256": "97976410cb02f8ba710d856b4ac904bcf976d677b50eefdee39fb64176070d4b", "cacheKeys": { - "wasm32": "d9f7b81e4a1898ae67934b59515b74d11d2748143eae1cbc0ea9a14a666d0578", - "wasm64": "6e0cfff1f1929963102ba5bb0a9adf4aa5022905a8817b0544a0b3f5bc755cd2" + "wasm32": "772ed9fc96057a9c9d80840c4983aed19e0557a8e40003ba4fe96e5a93808e2e", + "wasm64": "c8220ef3538d7a5439c49752e8b51d55ce7e14f558e02285522b8e81556129c4" } }, "nginx-vfs": { "manifestSha256": "46aa2d3250ac5cd0f102a85c3a36a086c7a50ba1f9e05fa1c2aae3d07ad16f10", "cacheKeys": { - "wasm32": "9964113139d9f9d3ea6812ac15d23b2ecfae0bf253d948b27c6f02d2aeb6ac1a", - "wasm64": "493bb8ec6c5ce410666db6b748d250d269648ca7e5db74a123e09a985bfb2cfc" + "wasm32": "40b4988e6e280e717ba3feb8353f76bbc6992598ce2e8cbf0bf53f14b9cdb4cf", + "wasm64": "87c465fe920c874d628e5bc2e2e39862b32b1bf41955488c7dadbc94086980c3" } }, "node": { @@ -333,8 +333,8 @@ "node-vfs": { "manifestSha256": "33315fb1b3030a4c187ae075eac08f717de8d7ab017b86c6458778ac9070eece", "cacheKeys": { - "wasm32": "4b6288bba28fa58807c061bb810bd296c0989d2ccafa865652efc9d2474da356", - "wasm64": "86019ed8099f6afda9b7e995be9fe7a1f773cfd1d153cfe143402929eafcd321" + "wasm32": "e7b886d3361bfa9a2628c648633cce9960faab811cfcc1829e85ba2a61897adc", + "wasm64": "37cde0a666c0f6caff5f891121d77d1db634aaa0bf8498fdbb89d2fb70d8f7c2" } }, "openssl": { @@ -354,78 +354,78 @@ "perl": { "manifestSha256": "6cdc4dbc54d0e4008cff41f82ec8918c0e44b4aef23903539c1bc0f538fe3ad2", "cacheKeys": { - "wasm32": "6aff0ce4fbc5a621ea551d1451edb0a372cf53444ea4cdb2267b15b457ebc97d", - "wasm64": "fd4f03b01c3e8ca89bb54997945ea197b96e7ddf87f1a8c92796466a6b20402c" + "wasm32": "2d820d399d95ba7f9f3c8935fba1d869142fbfc09667b8149dcbb33363aa4785", + "wasm64": "335451ce5e2093c95e758fb808e94150a2817af7c039ce2312659ff741810a0a" } }, "perl-vfs": { "manifestSha256": "1345cc102bc8c24a6bc276410c00b4fcc99ba5f6adc9b031f882aaa35d53c8bc", "cacheKeys": { - "wasm32": "c19bb5219bbca8610925ed7e021bfc15259e907d4bae5bdabe0728419cfaf010", - "wasm64": "03a69b841600f89a0ebfe3aa97c74f4649e765d25022a009cfa4f549af1b558e" + "wasm32": "41870476e5efbf57f60c4fdcc190e178360fa0610adf2bf6ecf06df24aa393f7", + "wasm64": "b307e174f1a075e9f880052c7126672b7c0f7096c74b8a53a399dda2006d70d4" } }, "php": { "manifestSha256": "fcd9d7915ed8935b05418e148edcb312adf0b1e4e627fe4aab75444b35b33bbc", "cacheKeys": { - "wasm32": "54c56075f8cdfca61bfbe51fded0cf9ce52ff6fa66f20e720d9d2d0c40b654dc", - "wasm64": "2be35fe201db3bd9cac87d16708bfef192fee38dc39f394e61c5b69f91f68d74" + "wasm32": "c5e70ff8e8ab519b31810e4dac16a7500033f549395152f7b00b239d6170acf6", + "wasm64": "5485a281b368a3c7dab477a3bcf14a41999b0df42ee16e2cb3d88746632e1c0d" } }, "posix-utils-lite": { "manifestSha256": "8fd7190b2848ef80143adc7b9268c79e13cd4db3430f7105faf49a4b4407a06f", "cacheKeys": { - "wasm32": "81206dd760c115c96d49ee53557f2e7b05f1fbf29502e201132aa8453469ed89", - "wasm64": "cbc5836815240d6d8fa0233f8dfd7b26314b29416759ada5579098c473447767" + "wasm32": "2308c7f1868ea3e052209430626f792e07a168286c45893724195d4709c60220", + "wasm64": "93ed9c8befec4c18904838a92cfca14c8eee7c403652a2379edaa453ceef6c62" } }, "python-vfs": { "manifestSha256": "d1aa99feae65f06c0ca8cf52c2176534b2723fd4ee6eba6e72c205245e1c9c26", "cacheKeys": { - "wasm32": "8990d72a3cf9965a726d7545d52f58efa2c40b64e747e5999b41b56d71f17c50", - "wasm64": "91243f662e0d8126df3df02da952733a076771b17f50accb09a2e9d2b1c556e6" + "wasm32": "492e7349b34a9b8d9ae873902fd6b4b750607e999d059fb011715dde52144c2c", + "wasm64": "571d1515e8def92afcbd6b937b08633b1945ad9ce9961d8af30c1c864641fcde" } }, "redis": { "manifestSha256": "151fb507de953ba2ba94b8f881bc7d66b4c741c1359a2f590cc07f9a4cd368a3", "cacheKeys": { - "wasm32": "9ff20fb4a59e14e06a729e8c36e4c8af15fc05d613d88ee3d6cd42d20b7ff1c1", - "wasm64": "10127b5744704616c44c49d15a7d712674cf3032c4519d719fc0a559df1ea43e" + "wasm32": "da7392e43962a0288ff0a9958190215ee01476e1d235f4275b790507ce5e4715", + "wasm64": "2b89d7853c8cbb8041daa1f7766dd88ce8bd4f2f08d71a33344cd653f7a8480d" } }, "redis-vfs": { "manifestSha256": "234c45c40f94e2a89f98295313b82cb9fce15a412ac829d9e87394e0dc731813", "cacheKeys": { - "wasm32": "ff41226308205e331d1b0402f0700e72b91dd9a93677aebc649b937ee352cd37", - "wasm64": "494f7a76ba0b3bcda0a1d108a4f745a68b15e3e1f861101590b5c7e6f90a9697" + "wasm32": "480f32952a8f7be451762b6012f9312ca4b1dad0b04d93c387f52427d660daa5", + "wasm64": "1502729cd03b57cc1643f4901d0b9ab9e36910751ceb51ff8adceeef90ccd25b" } }, "rootfs": { "manifestSha256": "0420201025170f48c32949ac62707d4577cdc13a53ad1f01f59d318ec07c8d6e", "cacheKeys": { - "wasm32": "f37ef8a5f785aa3d9cd9f601b4e96ce916ad72ed701952dc2fdc671639d5d9d8", - "wasm64": "0aad55f0e4cfd37ce2dcd66a1f2ef8a96d7b24f754865af1ef97dbd18ba4cdb0" + "wasm32": "4feb895e5854b8d3f1e3fd9e29af4f6f001ffaf458d311df1a6451f259134368", + "wasm64": "6028bdf295de0e9c2c302a5a96df54dc3ea963f73f673fddfb6b9a0b4c219cea" } }, "ruby": { "manifestSha256": "6ea67a246c29cd927a19decbb36ae4c4d478ff6642d2c77e08a6b2cfeb8251d2", "cacheKeys": { - "wasm32": "b9554136ba3f6ec44897088250599687cfc0c7fc3af971b908f2668cd5cacd7a", - "wasm64": "75406b0ae56e0e61185e0e6b69d4f2366b6bf09ec536a3c25d9a16672f2e0f03" + "wasm32": "fd5e4efd09f5f8fbb9b7fe6671ef1c9253f3d78f3709783b5002a3a1983163ab", + "wasm64": "c0cb2beb1218c89bfdf674375e350d33efb7dc53636ea3a3c52e374b31eb55ca" } }, "sed": { "manifestSha256": "2a08e9c5dacc5facc8983c1ff35ff2a72db328405e9a1f464cc7ad155c4d08af", "cacheKeys": { - "wasm32": "6265486b1ef7c27412920d531290a1bd43e0e1f5c0459861c1908ef52da6adc7", - "wasm64": "1c4773040b9b81873325ce0789c39a3049de2ac94de8bd1863d1abb7b80b6ceb" + "wasm32": "7ba13641a1723f061e59a41a5db4e713fa7ebc37db885087217c7cb2e1661c19", + "wasm64": "3151db4c0b7ed9433fd0835bacf7e63512d6ae09a39cdf149b407b9615c1dd82" } }, "shell": { "manifestSha256": "107adeee07fd0a10d9f2bb40a0cd84a6144bff91e7505b5ae9afeabaa000beb9", "cacheKeys": { - "wasm32": "0d2a2d62b6fb8ecf5c0fb47316d69ce241203fccf55ee5e084c451a3d8635644", - "wasm64": "53ae75bc22cebd217c2d65d26180e14c2910b4f082b1694b5c00fcfe8d587451" + "wasm32": "5be1bf60e0954662e5973417c42baa3ba028c4260f41e8e690a927be981fa0f1", + "wasm64": "31ce326602fd0b40b4b745b7f557176402840ae0a1585f99f11606aa2d5ec944" } }, "spidermonkey": { @@ -452,85 +452,85 @@ "sqlite-cli": { "manifestSha256": "1cebb768f9473dc58fc6e72c9b24565760aedaec4cbdb29ad43b22a0a5fd7030", "cacheKeys": { - "wasm32": "9c78c0e368f0cdb390dc2c54c075c990b4c462d918f2c2432387cbfa78bbeeec", - "wasm64": "2d757bcb6b7b2a459f5ac9614b1e41d6e4dddc69719ca8261f088f593d7ed9ed" + "wasm32": "1302bc16ff5d928390e99d8eda54447da06d6d601d2e93d3d77aa7a877973835", + "wasm64": "e2caa70802da2cf6cc091e7cf3bbec04e1cdfaa97dd7e0dfc853d278b39660f3" } }, "tar": { "manifestSha256": "08fa090c122d3105c735d74560bdd7b8b083a6f9ca8bdec17de0b4993e1be7fd", "cacheKeys": { - "wasm32": "d842e794f28f0f7c3fed3be0b1ad1f26dbe563b13a8883bc77e2fc59f21b52b4", - "wasm64": "ae9255c98618602a0eef8c67c69ea47a7701412bf766658f420c4914df7bdbd3" + "wasm32": "7db0ac25cdc6534a25f0f563bd5431e5af1664284cf53b213e9868e52fa6d072", + "wasm64": "614058a11e3563ddefbc3853baf1dd46efe944fc38d299073eb78b3c5151c069" } }, "tcl": { "manifestSha256": "67253d47de7df9184e68ec3c49835455746941835a26db9e588aa8fbee7d4636", "cacheKeys": { - "wasm32": "edae667904ad078a3b3e53e0620d1cd0981c70924884e3948e65f7b99557c90e", - "wasm64": "bb80fdf497f261cbcbb432594b599fafa922f3f709bac263b9d21a38f2a0ab66" + "wasm32": "0256f1aedd61bdae5546348dd188638aa2192a8ba5cdd013584b1701f42e2133", + "wasm64": "e99d1a35e80148841c32edbcc34f0e485cc7d745a63a7ce9e318cf0cf5e6c892" } }, "texlive": { "manifestSha256": "028ada023f8a8f9966c8a28575242a339aaaf0e8870fe4a221986133b381c3c0", "cacheKeys": { - "wasm32": "5b650c94bb8c45ecec9376f87d9db1e0cbd3f26645cb736aed32882578d4baa3", - "wasm64": "873c228fff637627c1524c83f8f6994e4c01479ce1c343346027de0ed71389e1" + "wasm32": "40b7b146f334e95fbc826ab23999bfb3dc2032705667c9ffca2a53a48f4613d3", + "wasm64": "4f560a5bdede5c76835874c5ac82e9bf15bf69ba55e2a7619fb11581086b68f8" } }, "unzip": { "manifestSha256": "ef4e5e34258827ab3cbc1930da3fd9514f08f2d7d56c7c3e0d5c495a4d3a20e9", "cacheKeys": { - "wasm32": "f4ee34b5872894f3a45faf97c7a13d3b0c5d7b46344c89905667a26b0ea5fdf6", - "wasm64": "beccd8ef50cc48279b881fdd94c354390025c497f391b01267f3b031e8d5d95a" + "wasm32": "d058df9f05e8d6b84e4cab72005e03c5de8968f4d72f6ef3ceceb8815d03532d", + "wasm64": "a396cc7e9ba4a8f2205f8e46f6b9735f439abc872b70c2dfd47163189a601629" } }, "userspace": { "manifestSha256": "221176f2a096dee19bbefd89da5c7f50138ecd92d37ec9d7d6b35dbb25e86924", "cacheKeys": { - "wasm32": "6538ad13ac4b15f5ca4a02c3eee15274bd7ff6aa959be24290ea5daab4b72135", - "wasm64": "cb6856ff8fcea64797e68d1a0e313b66a7ff9ca65bc396dbc632409777bd27cc" + "wasm32": "3a42c75d397d02825da5b6ea7eeb510f9b776a359e0ca1283d4109a0c4dde404", + "wasm64": "0551507fd8bda0e30814b11a0b75022246a58966abdd358bb5ac3c2158738e52" } }, "vim": { "manifestSha256": "c211660ef41d01f95f3fbdf675b74891e897e3f304e04f1bf5586f326007382c", "cacheKeys": { - "wasm32": "39d28fe5dd326480d8b3e140317bcdb65dee6cbda536a6a45ed2fe0b623660e2", - "wasm64": "581485dd1b5181aaeef781f89e1f29d1954280c461fbef588a1663c13785ec43" + "wasm32": "e0a4f2dccc1020ab2c5fde09301251c7da82dc9cb3baa24729f490294cc8373a", + "wasm64": "331c796ec4e4ae5781ee395770b852c7a7da0d5ba4ab58e1a7afdcd329fc43c6" } }, "vim-browser-bundle": { "manifestSha256": "e31d84057db49c3534381604d0c8f3c7d765e33ede560a7ea7b01a5a8f1aa0d3", "cacheKeys": { - "wasm32": "f0f9e77e4b4a1003f387d7706cf16a29da27e537d1b354547d1d94721b4d53bb", - "wasm64": "bcb98d055890ef832e911e6a9a2705a94ac267505f581f2f724b630e6d2abadc" + "wasm32": "fcfbb7dc5ba2daeea207eac0b7e991938d8cf3d778e7328f03cf31eb381e976d", + "wasm64": "dbed5eacbc44782c763216feb896d056fc02fde50a66e585589008fb6de9ab61" } }, "wget": { "manifestSha256": "d3c7ba9bc1ae708b99850a6eb2cae521c85bf1010c90135a935f5dfae57a53a0", "cacheKeys": { - "wasm32": "222061a6c8511df115329138bac0641312d7089558fbf6a2da886a50aa6150b3", - "wasm64": "1910da379eebf52b7940211b2cecc12100e0e64034df34231a2c1623b8fa9b67" + "wasm32": "bdf72a4f1ea11c510f3d1644802df33786cd9c8ee158561ba72c8004436e3a56", + "wasm64": "ce87453bd578d4377ee8299a3707f948223412b7b9276d08fd065316a36e1b5c" } }, "wordpress": { "manifestSha256": "36465e0596a06e855a8524eecfd87da98643bc13b37ee12939f5aab44bf33418", "cacheKeys": { - "wasm32": "c5aafb1958f6a08d97a22964a9855ddcb43dc3e5054c53356c80835bacfb415a", - "wasm64": "6bef5e0b1637b4185c7231026db87a744b10f924e9a6ccf4aab8a6ac63589e76" + "wasm32": "3273c155264665eaa8369f4ae19d674317c66d5f04603006a1aad357c39c9e37", + "wasm64": "32b94e1a478fa81c644d64febc579f0aff1332150f65c03bb7ab84d355df6d8d" } }, "xz": { "manifestSha256": "8707d35b8647b1af8fa165dcb6ce7b2a6a007e19ab2daca2680c39395864a737", "cacheKeys": { - "wasm32": "71047565d59bb74ecd56e2abb2bf57e500b1167638030fa52e6503de796f7682", - "wasm64": "f8202a6c4d1793455ec28ed4e33796a21ec0d4200611fb694a53985057fe43ec" + "wasm32": "261ecf6d602b685b0977854cc97ac51f13a0bf27c28753fcf35a6384714d6c9c", + "wasm64": "ae3820dd71871c6bfc3f3e584b45146752edac0cc93005e58d7e59b8ec37b9c7" } }, "zip": { "manifestSha256": "9c9cfa8220aaeaea26736b55f7cd0a7517b8853c07f9f0d241ad67dd43592e36", "cacheKeys": { - "wasm32": "a73d281ebf1c5fead1576a20094dc528b4a48a02a6fc065c9168a368bebc3be3", - "wasm64": "b154bbbed9698922f37994ea59a3b174b539f6acc1f5b0fa65d3a51785ec1375" + "wasm32": "7adafa87971ab6e08449b0958b55364989c06297b9027c1acb06f72959d43cfd", + "wasm64": "7dc135d2ede863f05633fac8f056fb4eea57d071310744127bfb21710e002be9" } }, "zlib": { @@ -543,8 +543,8 @@ "zstd": { "manifestSha256": "89f266938c2c253700a838e6352c9de66c976c84883325aaa979f72b732357e4", "cacheKeys": { - "wasm32": "1e00e7d149e4cb81a0fcf78121d9b3b173de21d1773f33f1d8c3700c7ef5dba1", - "wasm64": "747fd024b93d6ad99e93cd92a7b9c9cf4cdf5f9d8a179100074bf4ab6c30e460" + "wasm32": "47bf7eb3741ccdb3d7e0e320c7440c2106aa997e476830cbc2d63532240c0a38", + "wasm64": "1941bcb45970fb0966394cb4bcddfa4b1dfce88b5ff4927f1580191c8f8e01eb" } } }, @@ -555,14 +555,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "830ea09a72abe74ae1e3e728d80740f330250e1be2c52a9f1e053f151d15dc3c" + "wasm32": "1d813d7f9db4979fc5c0eef6b37e213cb73a42395757cc92ef8ebeee2e62913d" }, "dependencyClosures": { "wasm32": [ { "packageName": "ncurses", "manifestSha256": "0a1c180eeade627e204aa47065dbfebfcdc9359d25e2db436932c7fa3a86fc1e", - "cacheKey": "e95f67690426e286783716ea6e0b39edf44bca23e8f7fd225567a45109c13400" + "cacheKey": "1d43a87fe208ed807f1e71640e4fcf7c4417699c318780e49a56c58d37e54ebb" } ] }, @@ -582,7 +582,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "87d9afde76b4f3d9ba5ca78fe38f55cdf86b312bdce8320a5c17f8b0dbbc1b38" + "wasm32": "2e7bb9ff2a2cd8dd3b28d46c7d733f873d764820ff023a736a41cc1e16745207" }, "dependencyClosures": { "wasm32": [] @@ -603,7 +603,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "09122fb04b306e4f803ca3a461e075091be99e9bf17835a19d7013a803322a2f" + "wasm32": "bd59c26ea552d64a009bebb2d0150b0e2003e227f0bdfe9f0a47a3ae97f9a336" }, "dependencyClosures": { "wasm32": [] @@ -624,7 +624,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "72765db88ce28cb67eb7b38a545b40e004215d7ccbb0d4eb02419c509b8d46cf" + "wasm32": "33ca2b6c08b61b6ba3b013b13271c98b9b63d75f0a5c47d6d265c27c10c50458" }, "dependencyClosures": { "wasm32": [] @@ -645,7 +645,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "314d476303cbf1886be4bf31ee1e21c9012bc2056222766f89ec33d9845f974e" + "wasm32": "3395181397427732850ece33d0fd3edf875903acb625e59a348577f2613d70f6" }, "dependencyClosures": { "wasm32": [ @@ -679,7 +679,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "63cca65e3571ba910489e52d3c0ec424570a8ebfa08b3b3e24c39747adcd51da" + "wasm32": "44aa318c3c067dc7183b20535ac525637df817431351c244f6bc37b029792a84" }, "dependencyClosures": { "wasm32": [ @@ -711,7 +711,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "0b2035e2d9b130f8edc85024720502b612e52db3c71c0c35ed55e26140515a93" + "wasm32": "2ec3ac4e8baa4bfb4ced00ff411feb0ae28e2acf3b5709ac7c042c5a57295aa6" }, "dependencyClosures": { "wasm32": [] @@ -732,7 +732,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "5b829e0fee62ddcc82e67563f559482c77be4ad2a794b297d614a9ac0bfb96a2" + "wasm32": "5fd58502ff1d28c11cc7941a2f21d5ab2d9f40bfbe113f766d274361ab5e0a88" }, "dependencyClosures": { "wasm32": [] @@ -774,7 +774,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "28167e431807693bad67358e88d98cfb0603fd2d6efd9808bb051544846c5364" + "wasm32": "43651e927c2672e0a4f749a5cce47db470dd19d72736ea4c6b6a6b3d55d9f7ab" }, "dependencyClosures": { "wasm32": [ @@ -815,7 +815,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "fb022926bb648059ad387c1919207500dbdfd359eed86fb38f671584f2ea97e5" + "wasm32": "ff0aaf3f73b5c6186a6b72544d245bf4b4021021266430803ce781c2d3128856" }, "dependencyClosures": { "wasm32": [] @@ -843,14 +843,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "fd19b68ecacbba3881e594e17360a68b1b39800ddb2710b8a9f6ad48b3480faf" + "wasm32": "af8826989737cc530b222a7049f2ec7dcb628adfc5b8bff543afe04b21f9bae7" }, "dependencyClosures": { "wasm32": [ { "packageName": "erlang", "manifestSha256": "475d6037e73a0f1e2f4381067194b2422751206979ea17209e10efa5a2a93bbb", - "cacheKey": "fb022926bb648059ad387c1919207500dbdfd359eed86fb38f671584f2ea97e5" + "cacheKey": "ff0aaf3f73b5c6186a6b72544d245bf4b4021021266430803ce781c2d3128856" } ] }, @@ -870,7 +870,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "ad115c1c6aba37e0b0246d728ec5343e931bc0b4b00c1fe8e24bf2e881399ce3" + "wasm32": "7ec10e23738b06c8fcd6b1953b252476522a01c5b9cea19c0b16bea88217a712" }, "dependencyClosures": { "wasm32": [] @@ -891,7 +891,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "41f1cbea97d2331099f6ec8f24ad10c15d54d536dca8c5d96354d053685333f1" + "wasm32": "4c75af58729ffafa2d5ff73b3522aacb97465e1a85e7163274ff0994d30017c3" }, "dependencyClosures": { "wasm32": [] @@ -919,7 +919,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "9cf821c87b60d798687583dbb8e325ba26d10764f2404b01d15bd296f5630332" + "wasm32": "e9894ce98cd2055fa27987b544eb72df96fc824ce7c9372d1db4294879985ac1" }, "dependencyClosures": { "wasm32": [] @@ -947,7 +947,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "f25ff8e149130b153b32c462696ed336e03a217e5bf9098aefb31f62cb96cdb8" + "wasm32": "85ef6c0fdac5fa9ac1ab10bb1a057038e6d08015e571b8b36eb2c89505e1917f" }, "dependencyClosures": { "wasm32": [] @@ -968,7 +968,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "956b61b3c170c0be48a6b482941cefd2bbe895cec099f5c748969c71b2e37ee8" + "wasm32": "1f940f813db52db5777a114926c9a143eda473ed9d232bcbd3cd196f3e3ff0b4" }, "dependencyClosures": { "wasm32": [] @@ -996,7 +996,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "2b95ab13353b8f112cd0eeb7ffcd01f745ec00814bf531f97bb4ec1f85d3a77f" + "wasm32": "ccfc4ba22252a0d57c55ed8ada09dff22883fb35a37a7ea1efa395bcb405a748" }, "dependencyClosures": { "wasm32": [] @@ -1017,7 +1017,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "c5fea74ec33f362e863e9d99f5d4719cc369ec754884d8183ded6ebce6c346ba" + "wasm32": "46f2e656c573c0311dd0e820c58632e59d29f80745bd6bd9a1f28cc082c470df" }, "dependencyClosures": { "wasm32": [] @@ -1066,7 +1066,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "5ec342f97928be06680f16f6bcb621b5265e3375a4d602f8a0e78b3adb1b79cc" + "wasm32": "c14c5b31bdea4e86f612a88b34102f831be3f09a189faebaf12b0777920ed647" }, "dependencyClosures": { "wasm32": [ @@ -1093,14 +1093,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "3b207f25a7c0c143b0e0a5aec0e828bdac1a4125d46bf1d019718db529337d7e" + "wasm32": "b7b5b9e0bb1298d84322990bc7122693b42f07200ba95d289e35f6cf25d35171" }, "dependencyClosures": { "wasm32": [ { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "28167e431807693bad67358e88d98cfb0603fd2d6efd9808bb051544846c5364" + "cacheKey": "43651e927c2672e0a4f749a5cce47db470dd19d72736ea4c6b6a6b3d55d9f7ab" }, { "packageName": "homebrew-bootstrap", @@ -1140,17 +1140,17 @@ { "packageName": "mariadb", "manifestSha256": "aeb221be233e4b57bccb4b3375020f94c3ec593dac2734631f00e3b1aec9f48c", - "cacheKey": "89cb71f508decd03f2b38c57e3f071c00a8d0b12d365f0f98cebabbda13cec79" + "cacheKey": "769572acd89a8fa7738058f6c2f48e5270868d1cc1422c4a0610b100cf851f77" }, { "packageName": "msmtpd", "manifestSha256": "09de04a422ddf631a29a259d816e081f8d317d1077808ede55d8c500baae748f", - "cacheKey": "94c0ffb6de71a0cbffd57cbe6979ad7438b16b0ca7b32b46cce86bf65fa1d5a6" + "cacheKey": "82b32c29b05f3854bebfb414875d603fe66829ab79b0ea02b4a1d00e09fe3a52" }, { "packageName": "nginx", "manifestSha256": "41b0aaf05fdd0d8da42642703f9e60fd7b9545d8029896918b87c771c359147b", - "cacheKey": "cc1c9c86ad57a4b5a5ed23636c6c50a354f85e294cfc6addf0c9457cb33defc1" + "cacheKey": "6a92877569b9bf8dabd5333d3f0d5e96388379e28ec00f4d53e0ee675cb58eae" }, { "packageName": "openssl", @@ -1165,12 +1165,12 @@ { "packageName": "php", "manifestSha256": "fcd9d7915ed8935b05418e148edcb312adf0b1e4e627fe4aab75444b35b33bbc", - "cacheKey": "54c56075f8cdfca61bfbe51fded0cf9ce52ff6fa66f20e720d9d2d0c40b654dc" + "cacheKey": "c5e70ff8e8ab519b31810e4dac16a7500033f549395152f7b00b239d6170acf6" }, { "packageName": "shell", "manifestSha256": "107adeee07fd0a10d9f2bb40a0cd84a6144bff91e7505b5ae9afeabaa000beb9", - "cacheKey": "0d2a2d62b6fb8ecf5c0fb47316d69ce241203fccf55ee5e084c451a3d8635644" + "cacheKey": "5be1bf60e0954662e5973417c42baa3ba028c4260f41e8e690a927be981fa0f1" }, { "packageName": "sqlite", @@ -1200,7 +1200,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "69fd5aea1025f2974ce24964d3f93ba08df5f5082c48fd5e2f5c101df852cc07" + "wasm32": "9a509fe274611ce618bb65afde230b2e526adfed9f908dd286d63749bf924547" }, "dependencyClosures": { "wasm32": [] @@ -1221,7 +1221,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "0cbea55dc47e3ce5196d9b07cfa6a22a21fc80b87ba971fcac3f235bcfdfd6d5" + "wasm32": "3660780917e27528d76a83fb1ae76f3a547fe348cd1ba979bc9f015a03c1213b" }, "dependencyClosures": { "wasm32": [] @@ -1242,7 +1242,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "746a3cb4942a7375f68e1b8cfdc24e71dae6b1393da3b8bef29386af1af10302" + "wasm32": "a7063dcf8ce5bfe288b1520beac1eaf1d19f736fbdd683f344d83d74dcbab727" }, "dependencyClosures": { "wasm32": [] @@ -1263,7 +1263,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "8bbaab344db8e332b3763ce4700af83beb4ca8f6aa6670cce47f5ee9bd420899" + "wasm32": "49a0f581bbc70db8db660c144bbaf5683ea30f36f305c434c5e4d772c334723e" }, "dependencyClosures": { "wasm32": [] @@ -1285,8 +1285,8 @@ "wasm64" ], "cacheKeys": { - "wasm32": "89cb71f508decd03f2b38c57e3f071c00a8d0b12d365f0f98cebabbda13cec79", - "wasm64": "f6668fed09920d9e3526da63be09a9fc9650880341e11e5d87d93c32e9cef494" + "wasm32": "769572acd89a8fa7738058f6c2f48e5270868d1cc1422c4a0610b100cf851f77", + "wasm64": "815139d5343320f5a8ca5aeaff9421abb2ff6e568b13a90c2e5cc341e2ff5aa5" }, "dependencyClosures": { "wasm32": [ @@ -1337,24 +1337,24 @@ "wasm32" ], "cacheKeys": { - "wasm32": "6793c1e49769dedf80c692595db76abc462b9e6acfbdc2c37d425616a6dbdfe3" + "wasm32": "b70268f40e5f5e45c040132373266ddee6ad38fab897a60afcc47ea12ad0cf2a" }, "dependencyClosures": { "wasm32": [ { "packageName": "coreutils", "manifestSha256": "b8baabc9af9283434e0d80f7fdc0aaef242ffa31092ab0fc39a0603a12b3cca7", - "cacheKey": "72765db88ce28cb67eb7b38a545b40e004215d7ccbb0d4eb02419c509b8d46cf" + "cacheKey": "33ca2b6c08b61b6ba3b013b13271c98b9b63d75f0a5c47d6d265c27c10c50458" }, { "packageName": "dash", "manifestSha256": "10ce1bb611fefd78a6c9ec9038f6113e29a7062b1a73e08eccf9b33004cea9a8", - "cacheKey": "0b2035e2d9b130f8edc85024720502b612e52db3c71c0c35ed55e26140515a93" + "cacheKey": "2ec3ac4e8baa4bfb4ced00ff411feb0ae28e2acf3b5709ac7c042c5a57295aa6" }, { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "28167e431807693bad67358e88d98cfb0603fd2d6efd9808bb051544846c5364" + "cacheKey": "43651e927c2672e0a4f749a5cce47db470dd19d72736ea4c6b6a6b3d55d9f7ab" }, { "packageName": "libcxx", @@ -1364,7 +1364,7 @@ { "packageName": "mariadb", "manifestSha256": "aeb221be233e4b57bccb4b3375020f94c3ec593dac2734631f00e3b1aec9f48c", - "cacheKey": "89cb71f508decd03f2b38c57e3f071c00a8d0b12d365f0f98cebabbda13cec79" + "cacheKey": "769572acd89a8fa7738058f6c2f48e5270868d1cc1422c4a0610b100cf851f77" }, { "packageName": "pcre2-source", @@ -1390,25 +1390,25 @@ "wasm64" ], "cacheKeys": { - "wasm32": "8b7eb8e415bb55058e4d1c2ab8f027aaab704238a94f51e08e753ec359e7e7d7", - "wasm64": "c71c7f72d38ec16f4342158464a0a76556a78e96746e4c633c6b3365ed125b60" + "wasm32": "314fcb39160d1792c099471f02523816ec0f539f2085d425a04b4862b4515d43", + "wasm64": "30f2b70fb885278907dfc2a47d287d5ebe4ef61663f46c9ae6546401bedebe03" }, "dependencyClosures": { "wasm32": [ { "packageName": "coreutils", "manifestSha256": "b8baabc9af9283434e0d80f7fdc0aaef242ffa31092ab0fc39a0603a12b3cca7", - "cacheKey": "72765db88ce28cb67eb7b38a545b40e004215d7ccbb0d4eb02419c509b8d46cf" + "cacheKey": "33ca2b6c08b61b6ba3b013b13271c98b9b63d75f0a5c47d6d265c27c10c50458" }, { "packageName": "dash", "manifestSha256": "10ce1bb611fefd78a6c9ec9038f6113e29a7062b1a73e08eccf9b33004cea9a8", - "cacheKey": "0b2035e2d9b130f8edc85024720502b612e52db3c71c0c35ed55e26140515a93" + "cacheKey": "2ec3ac4e8baa4bfb4ced00ff411feb0ae28e2acf3b5709ac7c042c5a57295aa6" }, { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "28167e431807693bad67358e88d98cfb0603fd2d6efd9808bb051544846c5364" + "cacheKey": "43651e927c2672e0a4f749a5cce47db470dd19d72736ea4c6b6a6b3d55d9f7ab" }, { "packageName": "libcxx", @@ -1418,7 +1418,7 @@ { "packageName": "mariadb", "manifestSha256": "aeb221be233e4b57bccb4b3375020f94c3ec593dac2734631f00e3b1aec9f48c", - "cacheKey": "89cb71f508decd03f2b38c57e3f071c00a8d0b12d365f0f98cebabbda13cec79" + "cacheKey": "769572acd89a8fa7738058f6c2f48e5270868d1cc1422c4a0610b100cf851f77" }, { "packageName": "pcre2-source", @@ -1430,17 +1430,17 @@ { "packageName": "coreutils", "manifestSha256": "b8baabc9af9283434e0d80f7fdc0aaef242ffa31092ab0fc39a0603a12b3cca7", - "cacheKey": "837bdc6360d3481994ccb24c2fb12fd509ce70f445582a1ca32b0b77101dd3ad" + "cacheKey": "67ffde57ec940c7107d901231ed2d1c886957edc34de1c60c08094d1260ae797" }, { "packageName": "dash", "manifestSha256": "10ce1bb611fefd78a6c9ec9038f6113e29a7062b1a73e08eccf9b33004cea9a8", - "cacheKey": "981f288954ad9124917838d3deaf8514853a168f0f7c88c373b98aeafbebaeb0" + "cacheKey": "55133070386497aa605c431cf06777d717e4c4ececf2983cc507b858c4b529cd" }, { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "ce140644a3a60989c426590afbc252ed9b2042e7d810183b207dedb8c9f25738" + "cacheKey": "bbcd2d163560d0271c38581dae6f292cc4a119fc8f86af85552310157bef511a" }, { "packageName": "libcxx", @@ -1450,7 +1450,7 @@ { "packageName": "mariadb", "manifestSha256": "aeb221be233e4b57bccb4b3375020f94c3ec593dac2734631f00e3b1aec9f48c", - "cacheKey": "f6668fed09920d9e3526da63be09a9fc9650880341e11e5d87d93c32e9cef494" + "cacheKey": "815139d5343320f5a8ca5aeaff9421abb2ff6e568b13a90c2e5cc341e2ff5aa5" }, { "packageName": "pcre2-source", @@ -1475,7 +1475,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "8a5f567e2f8c33df0d1759f3e7cbeb0e6105044fd4b156855a9ebf79cde6bced" + "wasm32": "e61f328544530bf45ff0a339e0ed552c7c235bab8fa104e79c297ff9cea0d96e" }, "dependencyClosures": { "wasm32": [] @@ -1496,7 +1496,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "94c0ffb6de71a0cbffd57cbe6979ad7438b16b0ca7b32b46cce86bf65fa1d5a6" + "wasm32": "82b32c29b05f3854bebfb414875d603fe66829ab79b0ea02b4a1d00e09fe3a52" }, "dependencyClosures": { "wasm32": [] @@ -1517,7 +1517,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "48c6e796b2e94c9a4b9e56890b1a04e7df245263464df19e6c19d4676167e7fd" + "wasm32": "ee0b8eacc8659f5d690f5b3ee419584ae24403fe6b8e955e01e5192e6a73b635" }, "dependencyClosures": { "wasm32": [] @@ -1538,7 +1538,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "e95f67690426e286783716ea6e0b39edf44bca23e8f7fd225567a45109c13400" + "wasm32": "1d43a87fe208ed807f1e71640e4fcf7c4417699c318780e49a56c58d37e54ebb" }, "dependencyClosures": { "wasm32": [] @@ -1622,7 +1622,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "3d57ef272670ff5652a26adb0dbafc55ba34bdd0726b75e64bb8b3cafb65555b" + "wasm32": "e1f8dbb4e078921b75b109603c6b67a8035b3301659af937960841e563db2151" }, "dependencyClosures": { "wasm32": [] @@ -1643,14 +1643,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "35bd109acb9812c033d6d96996f5d00020554b1cee059811eec89b7aba5c1455" + "wasm32": "0acaa903f47da0a7730a75b9296217ce4723c5d4a53bff772ae18a1f0acc190f" }, "dependencyClosures": { "wasm32": [ { "packageName": "ncurses", "manifestSha256": "0a1c180eeade627e204aa47065dbfebfcdc9359d25e2db436932c7fa3a86fc1e", - "cacheKey": "e95f67690426e286783716ea6e0b39edf44bca23e8f7fd225567a45109c13400" + "cacheKey": "1d43a87fe208ed807f1e71640e4fcf7c4417699c318780e49a56c58d37e54ebb" } ] }, @@ -1670,19 +1670,19 @@ "wasm32" ], "cacheKeys": { - "wasm32": "8185e934440c3ff8fafe718d15028ab5dbd5c345a2f5517a2bc1dade96ca2154" + "wasm32": "a02b8fc8563203eab00795dfd2474d85769857ece24d9ce0e818c7bd8c56bca5" }, "dependencyClosures": { "wasm32": [ { "packageName": "ncurses", "manifestSha256": "0a1c180eeade627e204aa47065dbfebfcdc9359d25e2db436932c7fa3a86fc1e", - "cacheKey": "e95f67690426e286783716ea6e0b39edf44bca23e8f7fd225567a45109c13400" + "cacheKey": "1d43a87fe208ed807f1e71640e4fcf7c4417699c318780e49a56c58d37e54ebb" }, { "packageName": "nethack", "manifestSha256": "1a2f12ec2770bd8d40006d6c878a3493b97c71c2bb63ad08c7f6ad99bfd53504", - "cacheKey": "35bd109acb9812c033d6d96996f5d00020554b1cee059811eec89b7aba5c1455" + "cacheKey": "0acaa903f47da0a7730a75b9296217ce4723c5d4a53bff772ae18a1f0acc190f" } ] }, @@ -1702,7 +1702,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "cc1c9c86ad57a4b5a5ed23636c6c50a354f85e294cfc6addf0c9457cb33defc1" + "wasm32": "6a92877569b9bf8dabd5333d3f0d5e96388379e28ec00f4d53e0ee675cb58eae" }, "dependencyClosures": { "wasm32": [] @@ -1723,14 +1723,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "d9f7b81e4a1898ae67934b59515b74d11d2748143eae1cbc0ea9a14a666d0578" + "wasm32": "772ed9fc96057a9c9d80840c4983aed19e0557a8e40003ba4fe96e5a93808e2e" }, "dependencyClosures": { "wasm32": [ { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "28167e431807693bad67358e88d98cfb0603fd2d6efd9808bb051544846c5364" + "cacheKey": "43651e927c2672e0a4f749a5cce47db470dd19d72736ea4c6b6a6b3d55d9f7ab" }, { "packageName": "homebrew-bootstrap", @@ -1745,7 +1745,7 @@ { "packageName": "kernel", "manifestSha256": "db1d66db8575562ac7b3e720dbaa06d7dd5eef577d80220ea4167dc2d69b863b", - "cacheKey": "d0e03c4b0ab4858f28cb0d74e4a38b6d0af49781a6c53e12d14baade637a7ec9" + "cacheKey": "f864c4947733ce7a9c1bd6782038da97680db641c03b7696db690dac065e6436" }, { "packageName": "libcurl", @@ -1775,7 +1775,7 @@ { "packageName": "nginx", "manifestSha256": "41b0aaf05fdd0d8da42642703f9e60fd7b9545d8029896918b87c771c359147b", - "cacheKey": "cc1c9c86ad57a4b5a5ed23636c6c50a354f85e294cfc6addf0c9457cb33defc1" + "cacheKey": "6a92877569b9bf8dabd5333d3f0d5e96388379e28ec00f4d53e0ee675cb58eae" }, { "packageName": "openssl", @@ -1785,12 +1785,12 @@ { "packageName": "php", "manifestSha256": "fcd9d7915ed8935b05418e148edcb312adf0b1e4e627fe4aab75444b35b33bbc", - "cacheKey": "54c56075f8cdfca61bfbe51fded0cf9ce52ff6fa66f20e720d9d2d0c40b654dc" + "cacheKey": "c5e70ff8e8ab519b31810e4dac16a7500033f549395152f7b00b239d6170acf6" }, { "packageName": "shell", "manifestSha256": "107adeee07fd0a10d9f2bb40a0cd84a6144bff91e7505b5ae9afeabaa000beb9", - "cacheKey": "0d2a2d62b6fb8ecf5c0fb47316d69ce241203fccf55ee5e084c451a3d8635644" + "cacheKey": "5be1bf60e0954662e5973417c42baa3ba028c4260f41e8e690a927be981fa0f1" }, { "packageName": "sqlite", @@ -1820,14 +1820,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "9964113139d9f9d3ea6812ac15d23b2ecfae0bf253d948b27c6f02d2aeb6ac1a" + "wasm32": "40b4988e6e280e717ba3feb8353f76bbc6992598ce2e8cbf0bf53f14b9cdb4cf" }, "dependencyClosures": { "wasm32": [ { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "28167e431807693bad67358e88d98cfb0603fd2d6efd9808bb051544846c5364" + "cacheKey": "43651e927c2672e0a4f749a5cce47db470dd19d72736ea4c6b6a6b3d55d9f7ab" }, { "packageName": "homebrew-bootstrap", @@ -1842,12 +1842,12 @@ { "packageName": "nginx", "manifestSha256": "41b0aaf05fdd0d8da42642703f9e60fd7b9545d8029896918b87c771c359147b", - "cacheKey": "cc1c9c86ad57a4b5a5ed23636c6c50a354f85e294cfc6addf0c9457cb33defc1" + "cacheKey": "6a92877569b9bf8dabd5333d3f0d5e96388379e28ec00f4d53e0ee675cb58eae" }, { "packageName": "shell", "manifestSha256": "107adeee07fd0a10d9f2bb40a0cd84a6144bff91e7505b5ae9afeabaa000beb9", - "cacheKey": "0d2a2d62b6fb8ecf5c0fb47316d69ce241203fccf55ee5e084c451a3d8635644" + "cacheKey": "5be1bf60e0954662e5973417c42baa3ba028c4260f41e8e690a927be981fa0f1" } ] }, @@ -1909,7 +1909,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "4b6288bba28fa58807c061bb810bd296c0989d2ccafa865652efc9d2474da356" + "wasm32": "e7b886d3361bfa9a2628c648633cce9960faab811cfcc1829e85ba2a61897adc" }, "dependencyClosures": { "wasm32": [ @@ -1936,7 +1936,7 @@ { "packageName": "shell", "manifestSha256": "107adeee07fd0a10d9f2bb40a0cd84a6144bff91e7505b5ae9afeabaa000beb9", - "cacheKey": "0d2a2d62b6fb8ecf5c0fb47316d69ce241203fccf55ee5e084c451a3d8635644" + "cacheKey": "5be1bf60e0954662e5973417c42baa3ba028c4260f41e8e690a927be981fa0f1" }, { "packageName": "spidermonkey", @@ -1966,7 +1966,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "6aff0ce4fbc5a621ea551d1451edb0a372cf53444ea4cdb2267b15b457ebc97d" + "wasm32": "2d820d399d95ba7f9f3c8935fba1d869142fbfc09667b8149dcbb33363aa4785" }, "dependencyClosures": { "wasm32": [] @@ -1987,14 +1987,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "c19bb5219bbca8610925ed7e021bfc15259e907d4bae5bdabe0728419cfaf010" + "wasm32": "41870476e5efbf57f60c4fdcc190e178360fa0610adf2bf6ecf06df24aa393f7" }, "dependencyClosures": { "wasm32": [ { "packageName": "perl", "manifestSha256": "6cdc4dbc54d0e4008cff41f82ec8918c0e44b4aef23903539c1bc0f538fe3ad2", - "cacheKey": "6aff0ce4fbc5a621ea551d1451edb0a372cf53444ea4cdb2267b15b457ebc97d" + "cacheKey": "2d820d399d95ba7f9f3c8935fba1d869142fbfc09667b8149dcbb33363aa4785" } ] }, @@ -2014,7 +2014,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "54c56075f8cdfca61bfbe51fded0cf9ce52ff6fa66f20e720d9d2d0c40b654dc" + "wasm32": "c5e70ff8e8ab519b31810e4dac16a7500033f549395152f7b00b239d6170acf6" }, "dependencyClosures": { "wasm32": [ @@ -2137,7 +2137,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "81206dd760c115c96d49ee53557f2e7b05f1fbf29502e201132aa8453469ed89" + "wasm32": "2308c7f1868ea3e052209430626f792e07a168286c45893724195d4709c60220" }, "dependencyClosures": { "wasm32": [] @@ -2410,14 +2410,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "8990d72a3cf9965a726d7545d52f58efa2c40b64e747e5999b41b56d71f17c50" + "wasm32": "492e7349b34a9b8d9ae873902fd6b4b750607e999d059fb011715dde52144c2c" }, "dependencyClosures": { "wasm32": [ { "packageName": "cpython", "manifestSha256": "7dd4f446697a73941ec940c2ccba4d53be73fe6947f1e701031a4d0aa964c4ff", - "cacheKey": "314d476303cbf1886be4bf31ee1e21c9012bc2056222766f89ec33d9845f974e" + "cacheKey": "3395181397427732850ece33d0fd3edf875903acb625e59a348577f2613d70f6" }, { "packageName": "zlib", @@ -2442,7 +2442,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "9ff20fb4a59e14e06a729e8c36e4c8af15fc05d613d88ee3d6cd42d20b7ff1c1" + "wasm32": "da7392e43962a0288ff0a9958190215ee01476e1d235f4275b790507ce5e4715" }, "dependencyClosures": { "wasm32": [] @@ -2470,14 +2470,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "ff41226308205e331d1b0402f0700e72b91dd9a93677aebc649b937ee352cd37" + "wasm32": "480f32952a8f7be451762b6012f9312ca4b1dad0b04d93c387f52427d660daa5" }, "dependencyClosures": { "wasm32": [ { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "28167e431807693bad67358e88d98cfb0603fd2d6efd9808bb051544846c5364" + "cacheKey": "43651e927c2672e0a4f749a5cce47db470dd19d72736ea4c6b6a6b3d55d9f7ab" }, { "packageName": "libcxx", @@ -2487,7 +2487,7 @@ { "packageName": "redis", "manifestSha256": "151fb507de953ba2ba94b8f881bc7d66b4c741c1359a2f590cc07f9a4cd368a3", - "cacheKey": "9ff20fb4a59e14e06a729e8c36e4c8af15fc05d613d88ee3d6cd42d20b7ff1c1" + "cacheKey": "da7392e43962a0288ff0a9958190215ee01476e1d235f4275b790507ce5e4715" } ] }, @@ -2507,79 +2507,79 @@ "wasm32" ], "cacheKeys": { - "wasm32": "f37ef8a5f785aa3d9cd9f601b4e96ce916ad72ed701952dc2fdc671639d5d9d8" + "wasm32": "4feb895e5854b8d3f1e3fd9e29af4f6f001ffaf458d311df1a6451f259134368" }, "dependencyClosures": { "wasm32": [ { "packageName": "bash", "manifestSha256": "6478060f28d430d18a6ebe7c603392d35a41d9bcf6bc74322351d1450b9c5335", - "cacheKey": "830ea09a72abe74ae1e3e728d80740f330250e1be2c52a9f1e053f151d15dc3c" + "cacheKey": "1d813d7f9db4979fc5c0eef6b37e213cb73a42395757cc92ef8ebeee2e62913d" }, { "packageName": "bc", "manifestSha256": "a65661463bb7047b91ff00153bd99fd962c96e571934ab4e923f5215fb6ecfbd", - "cacheKey": "87d9afde76b4f3d9ba5ca78fe38f55cdf86b312bdce8320a5c17f8b0dbbc1b38" + "cacheKey": "2e7bb9ff2a2cd8dd3b28d46c7d733f873d764820ff023a736a41cc1e16745207" }, { "packageName": "coreutils", "manifestSha256": "b8baabc9af9283434e0d80f7fdc0aaef242ffa31092ab0fc39a0603a12b3cca7", - "cacheKey": "72765db88ce28cb67eb7b38a545b40e004215d7ccbb0d4eb02419c509b8d46cf" + "cacheKey": "33ca2b6c08b61b6ba3b013b13271c98b9b63d75f0a5c47d6d265c27c10c50458" }, { "packageName": "dash", "manifestSha256": "10ce1bb611fefd78a6c9ec9038f6113e29a7062b1a73e08eccf9b33004cea9a8", - "cacheKey": "0b2035e2d9b130f8edc85024720502b612e52db3c71c0c35ed55e26140515a93" + "cacheKey": "2ec3ac4e8baa4bfb4ced00ff411feb0ae28e2acf3b5709ac7c042c5a57295aa6" }, { "packageName": "diffutils", "manifestSha256": "3a78f0a46bae43ce5ea235c6638c2cbd1d8559b0b62b1fb42443b35968c1aca3", - "cacheKey": "5b829e0fee62ddcc82e67563f559482c77be4ad2a794b297d614a9ac0bfb96a2" + "cacheKey": "5fd58502ff1d28c11cc7941a2f21d5ab2d9f40bfbe113f766d274361ab5e0a88" }, { "packageName": "file", "manifestSha256": "7874c1affcbbf2c8ab8c8d4beb087f275af57b5caae6a8947bf5a63a181552b2", - "cacheKey": "41f1cbea97d2331099f6ec8f24ad10c15d54d536dca8c5d96354d053685333f1" + "cacheKey": "4c75af58729ffafa2d5ff73b3522aacb97465e1a85e7163274ff0994d30017c3" }, { "packageName": "findutils", "manifestSha256": "cceedf52aea67fbb0da03cfce6f2e9b1a7b5561fa1656c2762b43c651a625d02", - "cacheKey": "9cf821c87b60d798687583dbb8e325ba26d10764f2404b01d15bd296f5630332" + "cacheKey": "e9894ce98cd2055fa27987b544eb72df96fc824ce7c9372d1db4294879985ac1" }, { "packageName": "gawk", "manifestSha256": "a2567b8b0778805e385e1d3a41ac8b5d74aaf9d293b222001b17d09b11e5a0ba", - "cacheKey": "f25ff8e149130b153b32c462696ed336e03a217e5bf9098aefb31f62cb96cdb8" + "cacheKey": "85ef6c0fdac5fa9ac1ab10bb1a057038e6d08015e571b8b36eb2c89505e1917f" }, { "packageName": "grep", "manifestSha256": "59270d3bfb33167b32d13246bf855b49308f8c9c0a66d9635c804f702421fbc6", - "cacheKey": "2b95ab13353b8f112cd0eeb7ffcd01f745ec00814bf531f97bb4ec1f85d3a77f" + "cacheKey": "ccfc4ba22252a0d57c55ed8ada09dff22883fb35a37a7ea1efa395bcb405a748" }, { "packageName": "m4", "manifestSha256": "2c6582d99d6eabfb9da52badf49b899e9fdcba76a728536b25bc3741f3331543", - "cacheKey": "746a3cb4942a7375f68e1b8cfdc24e71dae6b1393da3b8bef29386af1af10302" + "cacheKey": "a7063dcf8ce5bfe288b1520beac1eaf1d19f736fbdd683f344d83d74dcbab727" }, { "packageName": "make", "manifestSha256": "f878d2d730f36a4c6dfe1fff1b4ccc704757ea22d8c4c8ccb95b11cd641e5fed", - "cacheKey": "8bbaab344db8e332b3763ce4700af83beb4ca8f6aa6670cce47f5ee9bd420899" + "cacheKey": "49a0f581bbc70db8db660c144bbaf5683ea30f36f305c434c5e4d772c334723e" }, { "packageName": "ncurses", "manifestSha256": "0a1c180eeade627e204aa47065dbfebfcdc9359d25e2db436932c7fa3a86fc1e", - "cacheKey": "e95f67690426e286783716ea6e0b39edf44bca23e8f7fd225567a45109c13400" + "cacheKey": "1d43a87fe208ed807f1e71640e4fcf7c4417699c318780e49a56c58d37e54ebb" }, { "packageName": "posix-utils-lite", "manifestSha256": "8fd7190b2848ef80143adc7b9268c79e13cd4db3430f7105faf49a4b4407a06f", - "cacheKey": "81206dd760c115c96d49ee53557f2e7b05f1fbf29502e201132aa8453469ed89" + "cacheKey": "2308c7f1868ea3e052209430626f792e07a168286c45893724195d4709c60220" }, { "packageName": "sed", "manifestSha256": "2a08e9c5dacc5facc8983c1ff35ff2a72db328405e9a1f464cc7ad155c4d08af", - "cacheKey": "6265486b1ef7c27412920d531290a1bd43e0e1f5c0459861c1908ef52da6adc7" + "cacheKey": "7ba13641a1723f061e59a41a5db4e713fa7ebc37db885087217c7cb2e1661c19" } ] }, @@ -2599,7 +2599,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "b9554136ba3f6ec44897088250599687cfc0c7fc3af971b908f2668cd5cacd7a" + "wasm32": "fd5e4efd09f5f8fbb9b7fe6671ef1c9253f3d78f3709783b5002a3a1983163ab" }, "dependencyClosures": { "wasm32": [ @@ -2633,7 +2633,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "6265486b1ef7c27412920d531290a1bd43e0e1f5c0459861c1908ef52da6adc7" + "wasm32": "7ba13641a1723f061e59a41a5db4e713fa7ebc37db885087217c7cb2e1661c19" }, "dependencyClosures": { "wasm32": [] @@ -2654,7 +2654,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "0d2a2d62b6fb8ecf5c0fb47316d69ce241203fccf55ee5e084c451a3d8635644" + "wasm32": "5be1bf60e0954662e5973417c42baa3ba028c4260f41e8e690a927be981fa0f1" }, "dependencyClosures": { "wasm32": [ @@ -2760,7 +2760,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "9c78c0e368f0cdb390dc2c54c075c990b4c462d918f2c2432387cbfa78bbeeec" + "wasm32": "1302bc16ff5d928390e99d8eda54447da06d6d601d2e93d3d77aa7a877973835" }, "dependencyClosures": { "wasm32": [] @@ -2781,7 +2781,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "d842e794f28f0f7c3fed3be0b1ad1f26dbe563b13a8883bc77e2fc59f21b52b4" + "wasm32": "7db0ac25cdc6534a25f0f563bd5431e5af1664284cf53b213e9868e52fa6d072" }, "dependencyClosures": { "wasm32": [] @@ -2802,7 +2802,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "edae667904ad078a3b3e53e0620d1cd0981c70924884e3948e65f7b99557c90e" + "wasm32": "0256f1aedd61bdae5546348dd188638aa2192a8ba5cdd013584b1701f42e2133" }, "dependencyClosures": { "wasm32": [] @@ -2823,7 +2823,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "5b650c94bb8c45ecec9376f87d9db1e0cbd3f26645cb736aed32882578d4baa3" + "wasm32": "40b7b146f334e95fbc826ab23999bfb3dc2032705667c9ffca2a53a48f4613d3" }, "dependencyClosures": { "wasm32": [ @@ -2862,7 +2862,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "f4ee34b5872894f3a45faf97c7a13d3b0c5d7b46344c89905667a26b0ea5fdf6" + "wasm32": "d058df9f05e8d6b84e4cab72005e03c5de8968f4d72f6ef3ceceb8815d03532d" }, "dependencyClosures": { "wasm32": [] @@ -2883,7 +2883,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "39d28fe5dd326480d8b3e140317bcdb65dee6cbda536a6a45ed2fe0b623660e2" + "wasm32": "e0a4f2dccc1020ab2c5fde09301251c7da82dc9cb3baa24729f490294cc8373a" }, "dependencyClosures": { "wasm32": [] @@ -2904,14 +2904,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "f0f9e77e4b4a1003f387d7706cf16a29da27e537d1b354547d1d94721b4d53bb" + "wasm32": "fcfbb7dc5ba2daeea207eac0b7e991938d8cf3d778e7328f03cf31eb381e976d" }, "dependencyClosures": { "wasm32": [ { "packageName": "vim", "manifestSha256": "c211660ef41d01f95f3fbdf675b74891e897e3f304e04f1bf5586f326007382c", - "cacheKey": "39d28fe5dd326480d8b3e140317bcdb65dee6cbda536a6a45ed2fe0b623660e2" + "cacheKey": "e0a4f2dccc1020ab2c5fde09301251c7da82dc9cb3baa24729f490294cc8373a" } ] }, @@ -2931,7 +2931,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "222061a6c8511df115329138bac0641312d7089558fbf6a2da886a50aa6150b3" + "wasm32": "bdf72a4f1ea11c510f3d1644802df33786cd9c8ee158561ba72c8004436e3a56" }, "dependencyClosures": { "wasm32": [] @@ -2952,14 +2952,14 @@ "wasm32" ], "cacheKeys": { - "wasm32": "c5aafb1958f6a08d97a22964a9855ddcb43dc3e5054c53356c80835bacfb415a" + "wasm32": "3273c155264665eaa8369f4ae19d674317c66d5f04603006a1aad357c39c9e37" }, "dependencyClosures": { "wasm32": [ { "packageName": "dinit", "manifestSha256": "44e125e1337503e27f8cff531cdbd79c4f406fd2d282b916a7746cf3a14057bf", - "cacheKey": "28167e431807693bad67358e88d98cfb0603fd2d6efd9808bb051544846c5364" + "cacheKey": "43651e927c2672e0a4f749a5cce47db470dd19d72736ea4c6b6a6b3d55d9f7ab" }, { "packageName": "homebrew-bootstrap", @@ -2999,12 +2999,12 @@ { "packageName": "msmtpd", "manifestSha256": "09de04a422ddf631a29a259d816e081f8d317d1077808ede55d8c500baae748f", - "cacheKey": "94c0ffb6de71a0cbffd57cbe6979ad7438b16b0ca7b32b46cce86bf65fa1d5a6" + "cacheKey": "82b32c29b05f3854bebfb414875d603fe66829ab79b0ea02b4a1d00e09fe3a52" }, { "packageName": "nginx", "manifestSha256": "41b0aaf05fdd0d8da42642703f9e60fd7b9545d8029896918b87c771c359147b", - "cacheKey": "cc1c9c86ad57a4b5a5ed23636c6c50a354f85e294cfc6addf0c9457cb33defc1" + "cacheKey": "6a92877569b9bf8dabd5333d3f0d5e96388379e28ec00f4d53e0ee675cb58eae" }, { "packageName": "openssl", @@ -3014,12 +3014,12 @@ { "packageName": "php", "manifestSha256": "fcd9d7915ed8935b05418e148edcb312adf0b1e4e627fe4aab75444b35b33bbc", - "cacheKey": "54c56075f8cdfca61bfbe51fded0cf9ce52ff6fa66f20e720d9d2d0c40b654dc" + "cacheKey": "c5e70ff8e8ab519b31810e4dac16a7500033f549395152f7b00b239d6170acf6" }, { "packageName": "shell", "manifestSha256": "107adeee07fd0a10d9f2bb40a0cd84a6144bff91e7505b5ae9afeabaa000beb9", - "cacheKey": "0d2a2d62b6fb8ecf5c0fb47316d69ce241203fccf55ee5e084c451a3d8635644" + "cacheKey": "5be1bf60e0954662e5973417c42baa3ba028c4260f41e8e690a927be981fa0f1" }, { "packageName": "sqlite", @@ -3049,7 +3049,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "71047565d59bb74ecd56e2abb2bf57e500b1167638030fa52e6503de796f7682" + "wasm32": "261ecf6d602b685b0977854cc97ac51f13a0bf27c28753fcf35a6384714d6c9c" }, "dependencyClosures": { "wasm32": [] @@ -3070,7 +3070,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "a73d281ebf1c5fead1576a20094dc528b4a48a02a6fc065c9168a368bebc3be3" + "wasm32": "7adafa87971ab6e08449b0958b55364989c06297b9027c1acb06f72959d43cfd" }, "dependencyClosures": { "wasm32": [] @@ -3091,7 +3091,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "1e00e7d149e4cb81a0fcf78121d9b3b173de21d1773f33f1d8c3700c7ef5dba1" + "wasm32": "47bf7eb3741ccdb3d7e0e320c7440c2106aa997e476830cbc2d63532240c0a38" }, "dependencyClosures": { "wasm32": [] diff --git a/tests/scripts/package-publish-flow.sh b/tests/scripts/package-publish-flow.sh index 910c472488..beea40a3ad 100755 --- a/tests/scripts/package-publish-flow.sh +++ b/tests/scripts/package-publish-flow.sh @@ -17,6 +17,7 @@ bash "$REPO_ROOT/.github/scripts/test-fetch-canonical-index.sh" bash "$REPO_ROOT/.github/scripts/test-select-package-archive-source.sh" bash "$REPO_ROOT/.github/scripts/test-download-verified-release-asset.sh" bash "$REPO_ROOT/.github/scripts/test-validate-staging-release.sh" +bash "$REPO_ROOT/.github/scripts/test-derive-rootfs-staging-scope.sh" bash "$REPO_ROOT/.github/scripts/test-recover-canonical-indexes.sh" bash "$REPO_ROOT/.github/scripts/test-init-merge-candidate.sh" bash "$REPO_ROOT/.github/scripts/test-clone-rejected-merge-candidate.sh" diff --git a/tests/sortix/os-test-local/basic/spawn/posix_spawn_large_environment.c b/tests/sortix/os-test-local/basic/spawn/posix_spawn_large_environment.c new file mode 100644 index 0000000000..a814eb8cc5 --- /dev/null +++ b/tests/sortix/os-test-local/basic/spawn/posix_spawn_large_environment.c @@ -0,0 +1,114 @@ +/* Test posix_spawn with argv+environment larger than one 64 KiB channel. */ + +#include + +#include +#include +#include +#include +#include + +#define ENV_COUNT 96 +#define VALUE_BYTES 1024 + +static char value_byte(unsigned index) +{ + return (char) ('a' + index % 26); +} + +static void environment_name(char* buffer, size_t size, unsigned index) +{ + int length = snprintf(buffer, size, "SPAWN_LARGE_%u", index); + if ( length < 0 || (size_t) length >= size ) + errx(1, "environment name overflow"); +} + +static char* make_environment_entry(unsigned index) +{ + char name[32]; + environment_name(name, sizeof(name), index); + size_t name_length = strlen(name); + char* entry = malloc(name_length + 1 + VALUE_BYTES + 1); + if ( !entry ) + err(1, "malloc"); + memcpy(entry, name, name_length); + entry[name_length] = '='; + memset(entry + name_length + 1, value_byte(index), VALUE_BYTES); + entry[name_length + 1 + VALUE_BYTES] = '\0'; + return entry; +} + +static void verify_child_environment(void) +{ + for (unsigned index = 0; index < ENV_COUNT; index++) + { + char name[32]; + environment_name(name, sizeof(name), index); + const char* value = getenv(name); + if ( !value ) + errx(1, "%s was not inherited", name); + if ( strlen(value) != VALUE_BYTES ) + errx(1, "%s has length %zu, expected %u", + name, strlen(value), VALUE_BYTES); + for (size_t offset = 0; offset < VALUE_BYTES; offset++) + { + if ( value[offset] != value_byte(index) ) + errx(1, "%s differs at byte %zu", name, offset); + } + } +} + +int main(int argc, char* argv[]) +{ + if ( argc == 2 ) + { + if ( strcmp(argv[1], "child") != 0 ) + errx(1, "child invoked incorrectly"); + verify_child_environment(); + return 0; + } + + char** environment = calloc(ENV_COUNT + 1, sizeof(char*)); + if ( !environment ) + err(1, "calloc"); + size_t environment_bytes = 0; + for (unsigned index = 0; index < ENV_COUNT; index++) + { + environment[index] = make_environment_entry(index); + environment_bytes += strlen(environment[index]) + 1; + } + if ( environment_bytes <= 65536 ) + errx(1, "test environment is only %zu bytes", environment_bytes); + + char* child_argv[] = { argv[0], "child", NULL }; + for (unsigned attempt = 0; attempt < 2; attempt++) + { + pid_t child_pid = 0; + int error = posix_spawn( + &child_pid, + argv[0], + NULL, + NULL, + child_argv, + environment + ); + if ( error ) + errx(1, "posix_spawn attempt %u: %s", + attempt + 1, strerror(error)); + + int status = 0; + if ( waitpid(child_pid, &status, 0) < 0 ) + err(1, "waitpid attempt %u", attempt + 1); + if ( !WIFEXITED(status) ) + errx(1, "child attempt %u did not exit normally: %#x", + attempt + 1, status); + if ( WEXITSTATUS(status) != 0 ) + errx(1, "child attempt %u exited with status %d", + attempt + 1, WEXITSTATUS(status)); + } + + for (unsigned index = 0; index < ENV_COUNT; index++) + free(environment[index]); + free(environment); + return 0; +}