From 70b4802fa1ca78549e33a4c2c74dd11695fcca56 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Tue, 11 Aug 2026 05:18:25 +0000 Subject: [PATCH] feat(wasix): add PostgreSQL postmaster research runtime --- docs/internal/PG18_WASIX_POSTGRES.md | 23 +- docs/maintainers/repo-structure.md | 15 + .../generated/docs/extension-evidence.json | 2 +- src/extensions/moon.yml | 6 +- src/extensions/tools/check-extension-model.py | 17 +- .../wasix-postmaster/.gitattributes | 2 + .../liboliphaunt/wasix-postmaster/.gitignore | 16 + .../liboliphaunt/wasix-postmaster/README.md | 726 + .../bench/sql/checkpoint-database-state.sql | 47 + .../bench/sql/checkpoint-volume.sql | 15 + .../bench/sql/checkpoint-workload-setup.sql | 83 + .../wasix-postmaster/bench/sql/core-smoke.sql | 30 + .../bench/sql/perf-probes.sql | 23 + .../bench/sql/query-perf/bulk-insert.sql | 22 + .../bench/sql/query-perf/copy-out.sql | 23 + .../bench/sql/query-perf/index-build.sql | 24 + .../bench/sql/query-perf/indexed-insert.sql | 26 + .../sql/query-perf/indexed-point-loop.sql | 47 + .../query-perf/indexed-read-hot-measured.sql | 33 + .../sql/query-perf/indexed-read-hot-setup.sql | 22 + .../bench/sql/query-perf/indexed-read-hot.sql | 49 + .../bench/sql/query-perf/indexed-read.sql | 31 + .../bench/sql/query-perf/indexed-update.sql | 37 + .../bench/sql/query-perf/md5-scan.sql | 12 + .../query-perf/single-transaction-insert.sql | 24 + .../query-perf/transaction-update-batches.sql | 44 + .../sql/query-perf/unlogged-bulk-insert.sql | 22 + .../unlogged-constant-insert-nocount.sql | 20 + .../query-perf/unlogged-constant-insert.sql | 22 + .../bench/sql/query-perf/wal-insert-stats.sql | 50 + .../bin/annotate-wasmer-perfmap.sh | 118 + .../bin/apply-wasix-core-overlay.sh | 83 + .../bin/bench-instrumentation-policy.test.sh | 233 + .../bin/bench-memory-budget-options.test.sh | 107 + .../bin/bench-pinned-warm-matrix.sh | 151 + ...nch-shared-memory-provider-options.test.sh | 169 + .../bin/bench-wasix-concurrent-query-suite.sh | 8104 +++ .../bin/bench-wasix-core-profiles.sh | 468 + .../bin/bench-wasix-query-suite.sh | 833 + .../bin/build-native-oracle.sh | 103 + .../bin/build-sealed-headless-carrier.sh | 1508 + .../bin/build-sealed-headless-carrier.test.sh | 1078 + .../wasix-postmaster/bin/build-wasix-core.sh | 723 + .../bin/build-wasix-regress-support.sh | 199 + .../bin/build-wasmer-perf-stats.sh | 52 + .../bin/capture-linux-cgroup-v2.py | 309 + .../wasix-postmaster/bin/check-prior-art.sh | 826 + .../bin/classify-linux-file-residency.py | 971 + .../bin/classify-linux-file-residency.test.py | 242 + .../bin/cold-ownership.test.py | 741 + .../bin/compare-libpq-latency.py | 1051 + .../bin/compare-libpq-latency.test.py | 350 + .../bin/compare-postgres-settings.py | 109 + .../bin/compare-postgres-settings.test.py | 116 + ...se-wasix-embedded-performance-viability.py | 318 + ...six-embedded-performance-viability.test.py | 178 + .../bin/connected-client-gate.test.sh | 95 + .../bin/current-evidence-manifest.py | 589 + .../wasix-postmaster/bin/delta-pg-stat-io.py | 162 + .../bin/delta-pg-stat-io.test.py | 86 + .../bin/deploy-immutable-sealed-carrier.sh | 91 + .../bin/extract-checkpoint-summary.py | 52 + .../bin/extract-checkpoint-summary.test.py | 54 + .../bin/freeze-wasix-lifecycle-evidence.py | 259 + .../freeze-wasix-lifecycle-evidence.test.py | 127 + .../bin/freeze-wasix-lifecycle-policy.py | 267 + .../bin/freeze-wasix-lifecycle-policy.test.py | 443 + .../bin/lifecycle-memory-checkpoint.test.sh | 153 + .../bin/pin-runtime-artifacts.sh | 285 + .../bin/precompile-wasix-core.sh | 255 + .../wasix-postmaster/bin/prepare-baseline.sh | 125 + .../bin/probe-wasix-runtime.sh | 104 + .../bin/process-tree-collector.test.sh | 72 + .../bin/profile-native-query.sh | 356 + .../bin/profile-wasix-query.sh | 486 + .../bin/prove-linux-cold-residency.py | 1087 + .../bin/qualify-wasix-checkpoint-recycle.sh | 1643 + .../qualify-wasix-checkpoint-recycle.test.sh | 101 + .../bin/qualify-wasix-cold-ownership.sh | 350 + .../bin/qualify-wasix-cold-ownership.test.sh | 137 + .../bin/qualify-wasix-immediate-recovery.sh | 1386 + .../qualify-wasix-immediate-recovery.test.sh | 184 + .../bin/qualify-wasix-libpq-latency.sh | 796 + .../bin/qualify-wasix-libpq-latency.test.sh | 501 + .../bin/qualify-wasix-single-backend.sh | 2150 + .../bin/qualify-wasix-single-backend.test.sh | 1174 + .../bin/resource-evidence.test.sh | 194 + .../bin/resource-monitor-race-retry.test.sh | 413 + .../wasix-postmaster/bin/run-acceptance.sh | 47 + .../bin/run-frozen-measurement.py | 374 + .../bin/run-frozen-measurement.test.py | 96 + .../bin/run-native-regress-subset.sh | 121 + .../bin/run-wasix-regress-subset.sh | 167 + .../bin/seal-wasix-core-exports.sh | 707 + .../bin/seal-wasix-core-exports.test.sh | 141 + ...eal-wasix-core-exports.transaction.test.sh | 1041 + .../bin/seal-wasix-linear-memory.sh | 296 + .../bin/seal-wasix-linear-memory.test.sh | 184 + .../bin/smoke-native-oracle.sh | 51 + .../bin/smoke-wasix-concurrent-connections.sh | 536 + .../wasix-postmaster/bin/smoke-wasix-core.sh | 174 + .../bin/stress-wasix-backend-waves.sh | 179 + .../bin/stress-wasix-initdb.sh | 82 + .../bin/summarize-checkpoint-qualification.py | 185 + ...summarize-checkpoint-qualification.test.py | 71 + .../bin/summarize-libpq-latency.sh | 179 + .../bin/summarize-libpq-latency.test.sh | 364 + .../bin/summarize-linux-smaps.sh | 195 + .../bin/summarize-linux-smaps.test.sh | 132 + .../bin/summarize-wasix-cold-ownership.py | 252 + .../bin/summarize-wasix-perf-stats.sh | 124 + .../bin/symbolize-wasmer-sample.sh | 117 + .../validate-adaptive-file-cache-telemetry.py | 1586 + ...date-adaptive-file-cache-telemetry.test.py | 1160 + .../bin/validate-checkpoint-memory.py | 420 + .../bin/validate-checkpoint-memory.test.py | 129 + .../bin/validate-checkpoint-recycle.py | 579 + .../bin/validate-checkpoint-recycle.test.py | 117 + .../bin/validate-file-cache-telemetry.py | 314 + .../bin/validate-file-cache-telemetry.test.py | 158 + .../bin/validate-host-fd-churn.sh | 69 + .../bin/validate-host-fd-churn.test.sh | 37 + .../bin/validate-memory-evidence.sh | 518 + .../bin/validate-memory-evidence.test.sh | 255 + .../bin/validate-sealed-loader-audit.py | 756 + .../bin/validate-sealed-loader-audit.test.py | 633 + .../bin/validate-wal-recycle.py | 197 + .../bin/validate-wal-recycle.test.py | 77 + .../bin/validate-wasix-cold-ownership.py | 419 + ...validate-wasix-lifecycle-memory-plateau.py | 605 + ...ate-wasix-lifecycle-memory-plateau.test.py | 238 + .../bin/validate-wasix-lifecycle-plateau.py | 1607 + .../validate-wasix-lifecycle-plateau.test.py | 897 + .../bin/verify-immutable-sealed-carrier.sh | 68 + .../bin/verify-sealed-headless-carrier.sh | 16 + .../wasix-postmaster/bin/wasix-make.sh | 78 + .../wasix-postmaster/docs/architecture.md | 1005 + .../docs/attested-start-fastpath.md | 197 + .../docs/checkpoint-recycle-qualification.md | 158 + .../docs/cold-ownership-qualification.md | 105 + .../docs/concurrent-root-cause.md | 309 + .../docs/copied-fork-continuation.md | 131 + .../docs/embedded-memory-architecture.md | 600 + .../docs/experiment-checkpoint.md | 419 + .../docs/immutable-carrier-deployment.md | 243 + .../docs/libpq-latency-qualification.md | 148 + .../wasix-postmaster/docs/replay-status.md | 608 + .../wasix-postmaster/docs/rss-memory-model.md | 894 + .../docs/sealed-export-closure.md | 150 + .../docs/semantic-cache-offers.md | 209 + .../docs/semantic-wal-cache-offers.md | 199 + .../docs/shared-memory-backing-experiment.md | 200 + .../wasix-postmaster/lib/common.sh | 1507 + .../wasix-postmaster/lib/common.test.sh | 451 + .../lib/durable_publication.py | 414 + .../lib/durable_publication.test.py | 112 + .../lib/durable_publication_crash.test.py | 235 + .../lib/guest_build_provenance.py | 330 + .../lib/guest_build_provenance.test.py | 98 + .../wasix-postmaster/lib/host-fd-telemetry.sh | 124 + .../lib/host-fd-telemetry.test.sh | 78 + .../wasix-postmaster/lib/immutable-carrier.py | 1316 + .../lib/immutable-carrier.test.py | 351 + .../lib/linear_memory_transaction.py | 453 + .../lib/linear_memory_transaction.test.py | 107 + .../wasix-postmaster/lib/postgres-profiles.sh | 380 + .../lib/postgres-profiles.test.sh | 126 + .../lib/process-supervision.sh | 392 + .../lib/process-supervision.test.sh | 188 + .../lib/qualification-identities.sh | 369 + .../wasix-postmaster/lib/sealed-carrier.sh | 351 + .../lib/sealed_export_chain.py | 560 + .../lib/sealed_export_chain.test.py | 194 + .../wasix-postmaster/lib/server-lifecycle.sh | 78 + .../lib/server-lifecycle.test.sh | 50 + .../lib/shared_memory_provider.py | 1406 + .../lib/shared_memory_provider.test.py | 566 + .../wasix-postmaster/lib/signal-owned-pid.py | 124 + .../lib/signal-owned-pid.test.py | 259 + .../lib/verify-sealed-carrier.py | 1582 + .../lib/verify-sealed-carrier.test.py | 260 + .../liboliphaunt/wasix-postmaster/moon.yml | 258 + .../experiment-patch-disposition.toml | 64 + .../wasix-core/src/backend/port/sysv_shmem.c | 287 + .../src/backend/postmaster/fork_process.c | 92 + .../wasix-core/src/include/port/wasix-core.h | 41 + .../interfaces/libpq/wasix_encoding_shim.c | 48 + .../src/makefiles/Makefile.wasix-core | 9 + .../wasix-core/src/template/wasix-core | 11 + .../0001-wasix-use-posix-dsm-not-sysv.patch | 33 + ...003-wasix-libpq-static-encoding-shim.patch | 14 + ...asix-core-execbackend-initdb-runtime.patch | 1076 + ...ump-avoid-lto-executequery-collision.patch | 161 + .../0006-wasix-retry-proc-join-on-eintr.patch | 50 + ...wasix-semantic-relation-cache-offers.patch | 315 + ...six-semantic-relation-cache-offers.test.py | 143 + ...0008-wasix-packed-atomic-latch-state.patch | 322 + ...08-wasix-packed-atomic-latch-state.test.py | 283 + ...six-inactive-durable-wal-cache-offer.patch | 369 + ...x-inactive-durable-wal-cache-offer.test.py | 615 + .../wasix-postmaster/postgres/patches/series | 8 + .../postgres/product-patch-provenance.toml | 56 + .../probes/bootstrap/fork_shm_probe.c | 70 + .../probes/bootstrap/libc_fork_shm_probe.c | 66 + .../probes/bootstrap/mmap_probe.c | 74 + .../probes/bootstrap/spawn_probe.c | 136 + .../probes/bootstrap/uid_probe.c | 16 + .../probes/libpq_checkpoint_probe.c | 479 + .../probes/libpq_latency_probe.c | 316 + .../embedded-steady-v1.gucs | 12 + .../embedded-steady-v1.tsv | 2 + .../profiles/durability/safe-v1.gucs | 3 + ...dle-postmaster-stabilized-qualified-v1.tsv | 33 + ...tabilized-idle-postmaster-candidate-v1.tsv | 33 + .../embedded-c4-lower-pressure-v1.tsv | 2 + ...ded-performance-viability-candidate-v1.tsv | 2 + .../embedded-concurrent-v1.gucs | 6 + .../embedded-postmaster-v1.tsv | 2 + .../wasix-postmaster/runtime/README.md | 138 + .../bin/build-patched-wasix-libc-sysroot.sh | 487 + .../runtime/bin/build-runtime.sh | 1288 + .../runtime/bin/prepare-upstream-checkouts.sh | 257 + .../runtime/bin/record-code-grounding.sh | 184 + .../runtime/bin/run-blocker-probes.sh | 1175 + .../runtime/bin/run-exec-backend-probes.sh | 40 + .../runtime/bin/run-upstream-checks.sh | 108 + .../verify-postmaster-concurrency-contract.py | 738 + ...fy-postmaster-concurrency-contract.test.py | 219 + .../bin/verify-postmaster-wasm-import.py | 223 + .../bin/verify-postmaster-wasm-import.test.py | 216 + .../bin/verify-runtime-execution-ownership.py | 414 + ...verify-runtime-execution-ownership.test.py | 310 + .../bin/verify-runtime-state-ownership.py | 434 + .../verify-runtime-state-ownership.test.py | 271 + .../runtime/bin/verify-source-lock.py | 430 + .../runtime/bin/verify-source-lock.test.py | 299 + .../wasix-postmaster/runtime/capabilities.tsv | 45 + .../0001-postgres-wasix-blockers.patch | 2157 + .../wasmer/0001-postgres-wasix-blockers.patch | 60394 ++++++++++++++++ .../policies/sealed-main-dlsym-exports.v1.txt | 5 + .../sealed-main-runtime-exports.v1.txt | 19 + .../policies/sealed-side-modules.v1.tsv | 29 + .../runtime/probes/dir_readdir_unlink_probe.c | 289 + .../runtime/probes/directory_fsync_probe.c | 96 + .../runtime/probes/dynamic_dlopen_probe.c | 54 + .../probes/dynamic_fork_dlopen_probe.c | 173 + .../probes/dynamic_fork_indirect_probe.c | 115 + .../runtime/probes/dynamic_probe_side.c | 13 + .../runtime/probes/dynamic_vfork_exec_probe.c | 85 + .../probes/epoll_listen_accept_probe.c | 157 + .../epoll_listen_after_vfork_exec_probe.c | 212 + .../epoll_listen_external_after_pipe_probe.c | 135 + .../probes/epoll_listen_external_probe.c | 108 + .../probes/epoll_ofd_lifecycle_probe.c | 286 + .../probes/exec_shared_latch_sigurg_probe.c | 515 + .../probes/fork_private_isolation_probe.c | 40 + .../runtime/probes/futex_timeout_probe.c | 46 + .../runtime/probes/libc_eh_fork_probe.c | 47 + .../runtime/probes/libc_fork_shared_probe.c | 59 + .../runtime/probes/mmap_fixed_probe.c | 84 + .../runtime/probes/mmap_writeback_probe.c | 235 + .../probes/posix_spawn_blocking_wait_probe.c | 50 + .../runtime/probes/posix_spawn_pipe_probe.c | 100 + .../posix_spawn_sigchld_default_probe.c | 60 + .../probes/posix_spawn_sigchld_probe.c | 78 + .../probes/postgres_shmem_reattach_probe.c | 85 + .../probes/proc_fork_pic_minimal_probe.c | 34 + .../runtime/probes/proc_fork_pic_probe.c | 63 + .../runtime/probes/rlimit_stack_probe.c | 52 + .../probes/setitimer_epoll_one_shot_probe.c | 198 + .../runtime/probes/shared_futex_fork_probe.c | 111 + .../runtime/probes/socket_nonblock_probe.c | 190 + .../probes/spawn_shmem_reattach_probe.c | 170 + .../runtime/probes/sync_file_range_probe.c | 172 + .../probes/waitpid_wnohang_any_probe.c | 81 + .../runtime/probes/wasm_eh_sjlj_probe.c | 46 + .../wasix-postmaster/sources.lock.toml | 94 + .../testdata/fake-postmaster-compiler.py | 42 + .../testdata/fake-sealed-wasmer.py | 301 + .../testdata/fake-start-proof.py | 66 + .../testdata/make-sealed-export-fixture.py | 158 + .../tools/sealed-export-closure/Cargo.lock | 283 + .../tools/sealed-export-closure/Cargo.toml | 16 + .../tools/sealed-export-closure/src/main.rs | 1608 + .../wasix/.release-semantic-inputs.json | 8 +- .../assets/generated/asset-inputs.sha256 | 2 +- src/sources/moon.yml | 34 +- src/sources/third-party/shared/moon.yml | 2 + .../third-party/wasix-postmaster/README.md | 15 + .../third-party/wasix-postmaster/moon.yml | 37 + .../wasix-postmaster/wasix-libc.toml | 4 + .../wasix-postmaster/wasmer-napi.toml | 4 + .../wasix-postmaster/wasmer-test-files.toml | 4 + .../third-party/wasix-postmaster/wasmer.toml | 4 + .../webassembly-testsuite.toml | 4 + tools/graph/synthetic/affected.toml | 19 + tools/graph/synthetic/ci-affected.toml | 12 + tools/graph/synthetic/coverage.toml | 4 + tools/graph/synthetic/release.toml | 6 + .../assertions/assert-source-inputs.mjs | 61 +- tools/policy/fetch-sources.mjs | 66 +- tools/policy/source-fetch-core.mjs | 29 +- tools/policy/source-fetch-core.test.mjs | 112 + tools/policy/source-fetch-scopes.mjs | 112 + tools/policy/source-fetch-scopes.test.mjs | 121 + tools/xtask/src/asset_checks.rs | 5 + tools/xtask/src/asset_fingerprint.rs | 4 +- tools/xtask/src/main.rs | 4 +- tools/xtask/src/source_spine.rs | 20 +- 309 files changed, 152420 insertions(+), 81 deletions(-) create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/.gitattributes create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/.gitignore create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/README.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/checkpoint-database-state.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/checkpoint-volume.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/checkpoint-workload-setup.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/core-smoke.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/perf-probes.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/bulk-insert.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/copy-out.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/index-build.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-insert.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-point-loop.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read-hot-measured.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read-hot-setup.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read-hot.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-update.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/md5-scan.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/single-transaction-insert.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/transaction-update-batches.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/unlogged-bulk-insert.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/unlogged-constant-insert-nocount.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/unlogged-constant-insert.sql create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/wal-insert-stats.sql create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/annotate-wasmer-perfmap.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/apply-wasix-core-overlay.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-instrumentation-policy.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-memory-budget-options.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-pinned-warm-matrix.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-shared-memory-provider-options.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-wasix-concurrent-query-suite.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-wasix-core-profiles.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-wasix-query-suite.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/build-native-oracle.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/build-sealed-headless-carrier.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/build-sealed-headless-carrier.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/build-wasix-core.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/build-wasix-regress-support.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/build-wasmer-perf-stats.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/capture-linux-cgroup-v2.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/check-prior-art.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/classify-linux-file-residency.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/classify-linux-file-residency.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/cold-ownership.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-libpq-latency.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-libpq-latency.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-postgres-settings.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-postgres-settings.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/compose-wasix-embedded-performance-viability.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/compose-wasix-embedded-performance-viability.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/connected-client-gate.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/current-evidence-manifest.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/delta-pg-stat-io.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/delta-pg-stat-io.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/deploy-immutable-sealed-carrier.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/extract-checkpoint-summary.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/extract-checkpoint-summary.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-evidence.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-evidence.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-policy.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-policy.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/lifecycle-memory-checkpoint.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/pin-runtime-artifacts.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/precompile-wasix-core.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/prepare-baseline.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/probe-wasix-runtime.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/process-tree-collector.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/profile-native-query.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/profile-wasix-query.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/prove-linux-cold-residency.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-checkpoint-recycle.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-checkpoint-recycle.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-cold-ownership.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-cold-ownership.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-immediate-recovery.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-immediate-recovery.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-libpq-latency.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-libpq-latency.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-single-backend.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-single-backend.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/resource-evidence.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/resource-monitor-race-retry.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/run-acceptance.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/run-frozen-measurement.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/run-frozen-measurement.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/run-native-regress-subset.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/run-wasix-regress-subset.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-core-exports.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-core-exports.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-core-exports.transaction.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-linear-memory.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-linear-memory.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/smoke-native-oracle.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/smoke-wasix-concurrent-connections.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/smoke-wasix-core.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/stress-wasix-backend-waves.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/stress-wasix-initdb.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-checkpoint-qualification.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-checkpoint-qualification.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-libpq-latency.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-libpq-latency.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-linux-smaps.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-linux-smaps.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-wasix-cold-ownership.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-wasix-perf-stats.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/symbolize-wasmer-sample.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-adaptive-file-cache-telemetry.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-adaptive-file-cache-telemetry.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-memory.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-memory.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-recycle.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-recycle.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-file-cache-telemetry.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-file-cache-telemetry.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-host-fd-churn.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-host-fd-churn.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-memory-evidence.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-memory-evidence.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-sealed-loader-audit.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-sealed-loader-audit.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wal-recycle.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wal-recycle.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-cold-ownership.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-memory-plateau.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-memory-plateau.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-plateau.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-plateau.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/verify-immutable-sealed-carrier.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/verify-sealed-headless-carrier.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/bin/wasix-make.sh create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/architecture.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/attested-start-fastpath.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/checkpoint-recycle-qualification.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/cold-ownership-qualification.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/concurrent-root-cause.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/copied-fork-continuation.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/embedded-memory-architecture.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/experiment-checkpoint.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/immutable-carrier-deployment.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/libpq-latency-qualification.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/replay-status.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/rss-memory-model.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/sealed-export-closure.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/semantic-cache-offers.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/semantic-wal-cache-offers.md create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/docs/shared-memory-backing-experiment.md create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/common.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/common.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/durable_publication.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/durable_publication.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/durable_publication_crash.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/guest_build_provenance.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/guest_build_provenance.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/host-fd-telemetry.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/host-fd-telemetry.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/immutable-carrier.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/immutable-carrier.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/linear_memory_transaction.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/linear_memory_transaction.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/postgres-profiles.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/postgres-profiles.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/process-supervision.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/process-supervision.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/qualification-identities.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/sealed-carrier.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/sealed_export_chain.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/sealed_export_chain.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/server-lifecycle.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/server-lifecycle.test.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/shared_memory_provider.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/shared_memory_provider.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/signal-owned-pid.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/signal-owned-pid.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/verify-sealed-carrier.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/lib/verify-sealed-carrier.test.py create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/moon.yml create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/experiment-patch-disposition.toml create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/overlays/wasix-core/src/backend/port/sysv_shmem.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/overlays/wasix-core/src/backend/postmaster/fork_process.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/overlays/wasix-core/src/include/port/wasix-core.h create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/overlays/wasix-core/src/interfaces/libpq/wasix_encoding_shim.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/overlays/wasix-core/src/makefiles/Makefile.wasix-core create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/overlays/wasix-core/src/template/wasix-core create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/patches/0001-wasix-use-posix-dsm-not-sysv.patch create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/patches/0003-wasix-libpq-static-encoding-shim.patch create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/patches/0004-wasix-core-execbackend-initdb-runtime.patch create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/patches/0005-pg-dump-avoid-lto-executequery-collision.patch create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/patches/0006-wasix-retry-proc-join-on-eintr.patch create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/patches/0007-wasix-semantic-relation-cache-offers.patch create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/postgres/patches/0007-wasix-semantic-relation-cache-offers.test.py create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/patches/0008-wasix-packed-atomic-latch-state.patch create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/postgres/patches/0008-wasix-packed-atomic-latch-state.test.py create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/patches/0009-wasix-inactive-durable-wal-cache-offer.patch create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/postgres/patches/0009-wasix-inactive-durable-wal-cache-offer.test.py create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/patches/series create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/postgres/product-patch-provenance.toml create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/probes/bootstrap/fork_shm_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/probes/bootstrap/libc_fork_shm_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/probes/bootstrap/mmap_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/probes/bootstrap/spawn_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/probes/bootstrap/uid_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/probes/libpq_checkpoint_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/probes/libpq_latency_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/profiles/checkpoint-policies/embedded-steady-v1.gucs create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/profiles/checkpoint-policies/embedded-steady-v1.tsv create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/profiles/durability/safe-v1.gucs create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/profiles/lifecycle-baselines/pg18-idle-postmaster-stabilized-qualified-v1.tsv create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/profiles/lifecycle-baselines/relative-stabilized-idle-postmaster-candidate-v1.tsv create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/profiles/memory-budgets/embedded-c4-lower-pressure-v1.tsv create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/profiles/qualification/embedded-performance-viability-candidate-v1.tsv create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/profiles/runtime-footprints/embedded-concurrent-v1.gucs create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/profiles/runtime-task-budgets/embedded-postmaster-v1.tsv create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/README.md create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/build-patched-wasix-libc-sysroot.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/build-runtime.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/prepare-upstream-checkouts.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/record-code-grounding.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/run-blocker-probes.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/run-exec-backend-probes.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/run-upstream-checks.sh create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-postmaster-concurrency-contract.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-postmaster-concurrency-contract.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-postmaster-wasm-import.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-postmaster-wasm-import.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-runtime-execution-ownership.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-runtime-execution-ownership.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-runtime-state-ownership.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-runtime-state-ownership.test.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-source-lock.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-source-lock.test.py create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/capabilities.tsv create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/patches/wasix-libc/0001-postgres-wasix-blockers.patch create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/patches/wasmer/0001-postgres-wasix-blockers.patch create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/policies/sealed-main-dlsym-exports.v1.txt create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/policies/sealed-main-runtime-exports.v1.txt create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/policies/sealed-side-modules.v1.tsv create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/dir_readdir_unlink_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/directory_fsync_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/dynamic_dlopen_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/dynamic_fork_dlopen_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/dynamic_fork_indirect_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/dynamic_probe_side.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/dynamic_vfork_exec_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/epoll_listen_accept_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/epoll_listen_after_vfork_exec_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/epoll_listen_external_after_pipe_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/epoll_listen_external_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/epoll_ofd_lifecycle_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/exec_shared_latch_sigurg_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/fork_private_isolation_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/futex_timeout_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/libc_eh_fork_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/libc_fork_shared_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/mmap_fixed_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/mmap_writeback_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/posix_spawn_blocking_wait_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/posix_spawn_pipe_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/posix_spawn_sigchld_default_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/posix_spawn_sigchld_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/postgres_shmem_reattach_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/proc_fork_pic_minimal_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/proc_fork_pic_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/rlimit_stack_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/setitimer_epoll_one_shot_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/shared_futex_fork_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/socket_nonblock_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/spawn_shmem_reattach_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/sync_file_range_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/waitpid_wnohang_any_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/runtime/probes/wasm_eh_sjlj_probe.c create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/sources.lock.toml create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/testdata/fake-postmaster-compiler.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/testdata/fake-sealed-wasmer.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/testdata/fake-start-proof.py create mode 100755 src/runtimes/liboliphaunt/wasix-postmaster/testdata/make-sealed-export-fixture.py create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/tools/sealed-export-closure/Cargo.lock create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/tools/sealed-export-closure/Cargo.toml create mode 100644 src/runtimes/liboliphaunt/wasix-postmaster/tools/sealed-export-closure/src/main.rs create mode 100644 src/sources/third-party/wasix-postmaster/README.md create mode 100644 src/sources/third-party/wasix-postmaster/moon.yml create mode 100644 src/sources/third-party/wasix-postmaster/wasix-libc.toml create mode 100644 src/sources/third-party/wasix-postmaster/wasmer-napi.toml create mode 100644 src/sources/third-party/wasix-postmaster/wasmer-test-files.toml create mode 100644 src/sources/third-party/wasix-postmaster/wasmer.toml create mode 100644 src/sources/third-party/wasix-postmaster/webassembly-testsuite.toml create mode 100644 tools/policy/source-fetch-scopes.mjs create mode 100644 tools/policy/source-fetch-scopes.test.mjs diff --git a/docs/internal/PG18_WASIX_POSTGRES.md b/docs/internal/PG18_WASIX_POSTGRES.md index c2d01a1c..144015cc 100644 --- a/docs/internal/PG18_WASIX_POSTGRES.md +++ b/docs/internal/PG18_WASIX_POSTGRES.md @@ -32,6 +32,15 @@ experiment evidence points to persistent process/fork/shmem/socket/RSS costs before query execution, while the released product wins by keeping one backend, one host lifecycle, direct FE/BE pumping, prebuilt PGDATA, and AOT reuse. +This assessment records the release-lane decision; it is not the current +physical-memory conclusion for concurrent WASIX. The independent, non-release +[`wasix-postmaster` research lane](../../src/runtimes/liboliphaunt/wasix-postmaster/README.md) +now measures aliased process-tree RSS only as a diagnostic and uses Linux PSS +plus cgroup accounting for physical-memory claims. Its +[memory model](../../src/runtimes/liboliphaunt/wasix-postmaster/docs/rss-memory-model.md) +and fresh-backend architecture remain research inputs and do not change the +single-backend product or its release support boundary. + The practical direction is therefore: - Keep full concurrent PostgreSQL under WASIX as upstream/runtime research and @@ -56,12 +65,14 @@ The practical direction is therefore: so experiment patches cannot be copied into this runtime without a WASIX rationale. -The immediate conclusion is that a "proper" concurrent PostgreSQL under WASIX -is unlikely to match native PostgreSQL or released Oliphaunt-style WASM performance -soon. A fresh PG18 WASIX runtime can plausibly beat the released PG17.5 lane -because it keeps the low-overhead lifecycle while inheriting newer PostgreSQL, -newer WASIX/Wasmer fixes, tighter host ABI boundaries, and targeted hot-path -patches from the experiment. +For the replacement decision recorded here, the immediate conclusion is that a +"proper" concurrent PostgreSQL under WASIX was not ready to match native +PostgreSQL or released Oliphaunt-style WASM performance. A fresh PG18 WASIX +runtime can plausibly beat the released PG17.5 lane because it keeps the +low-overhead lifecycle while inheriting newer PostgreSQL, newer WASIX/Wasmer +fixes, tighter host ABI boundaries, and targeted hot-path patches from the +experiment. Ongoing concurrent-runtime viability work is evaluated separately +with its own current carrier and PSS/cgroup evidence. ## PG17.5 Release-Lane Implementation Comparison diff --git a/docs/maintainers/repo-structure.md b/docs/maintainers/repo-structure.md index 96e12760..94bfaf44 100644 --- a/docs/maintainers/repo-structure.md +++ b/docs/maintainers/repo-structure.md @@ -37,6 +37,11 @@ source lives under `src//`. `src/sdks/react-native/`, and `src/sdks/js/` own platform and runtime SDKs. - `src/bindings/wasix-rust/` owns the first-class WASM/WASIX product lane. +- `src/runtimes/liboliphaunt/wasix-postmaster/` owns the independent, + non-release research lane for a PostgreSQL postmaster with fresh WASIX + backends. It may reuse the canonical WASIX build container and toolchain + recipe, but it does not share runtime patches, generated carriers, release + metadata, or support claims with the released single-backend WASIX product. - `src/*/moon.yml` is the canonical product graph. `tools/policy/sdk-manifest.toml` is a small SDK parity ownership registry and must agree with Moon metadata. - Tooling lives under `tools/`. @@ -54,6 +59,10 @@ source lives under `src//`. toolchain pins, extension-owned source pins, and generated extension catalogs live under `src/postgres/versions/18`, `src/sources/third-party`, `src/sources/toolchains`, and `src/extensions`. +- Research-only Wasmer and wasix-libc pins for the postmaster lane live under + `src/sources/third-party/wasix-postmaster/`. The default `production-all` + source scope excludes that directory; only the explicit research scope and + the repository-wide `all` scope may acquire it. There should be no tracked product source under retired roots such as `crates/`, `sdks/`, root `liboliphaunt/`, or root product examples. @@ -98,6 +107,12 @@ synthetic root: release lane and comparison target. It should not expose native engine selection or link/load `liboliphaunt`; native Rust work belongs in `src/sdks/rust`. +- `src/runtimes/liboliphaunt/wasix-postmaster` is a peer research product, not + an implementation directory of `liboliphaunt-wasix`. Its Moon project has no + release product, its expensive build and qualification tasks are manual, and + all generated checkouts, builds, carriers, caches, and reports stay under + `target/oliphaunt-wasix-postmaster/`. A future promotion must add an explicit + release boundary rather than inheriting one from another WASIX product. - `src/runtimes/liboliphaunt/wasix/assets/build` is source-only: scripts, patches, Docker inputs, and shims. Generated WASIX build and work trees live under `target/oliphaunt-wasix/wasix-build`. diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index 80431e38..18df713f 100644 --- a/src/extensions/generated/docs/extension-evidence.json +++ b/src/extensions/generated/docs/extension-evidence.json @@ -1347,7 +1347,7 @@ "current-source-status": "requires-exact-candidate-ci", "kind": "exact-sha-ci" }, - "source-digest": "sha256:d30cde83f40642f68c2dd7f2b1fe1bf1a9ca05122a7a6232fca42d624a06fed2", + "source-digest": "sha256:bdc96d067907df05c7c87442f89f1ef5b4d6c932d04ecf733c84e958ac445ce9", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/moon.yml b/src/extensions/moon.yml index 08dc4d70..19476b24 100644 --- a/src/extensions/moon.yml +++ b/src/extensions/moon.yml @@ -159,7 +159,9 @@ tasks: - "/src/runtimes/liboliphaunt/licenses/postgresql-18.4-COPYRIGHT" - "/src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/resources/dev/oliphaunt/android/extension-legal-catalog.json" - "/src/shared/extension-runtime-contract/**/*" - - "/src/sources/third-party/shared/openssl.toml" + - "/src/sources/third-party/shared/**/*" + - "/src/sources/third-party/native/README.md" + - "/src/sources/third-party/wasix/README.md" - "/src/extensions/**/*" - "!/src/extensions/generated/docs" - "!/src/extensions/generated/docs/**" @@ -169,6 +171,8 @@ tasks: - "/tools/dev/capture-command-output.mjs" - "/tools/policy/assertions/assert-source-inputs.mjs" - "/tools/policy/fetch-sources.mjs" + - "/tools/policy/source-fetch-scopes.mjs" + - "/tools/policy/source-fetch-scopes.test.mjs" - "/tools/policy/source-fetch-core.mjs" - "/tools/policy/source-fetch-core.test.mjs" - "/tools/policy/source-archive.py" diff --git a/src/extensions/tools/check-extension-model.py b/src/extensions/tools/check-extension-model.py index 15acd1f4..631ed303 100755 --- a/src/extensions/tools/check-extension-model.py +++ b/src/extensions/tools/check-extension-model.py @@ -351,9 +351,10 @@ def source_pin_paths() -> list[Path]: fail(f"{rel(THIRD_PARTY_ROOT)} must exist") if not EXTERNAL_ROOT.is_dir(): fail(f"{rel(EXTERNAL_ROOT)} must exist") + shared_source_root = THIRD_PARTY_ROOT / "shared" paths = [ path - for path in THIRD_PARTY_ROOT.glob("**/*.toml") + for path in shared_source_root.glob("**/*.toml") if path.is_file() ] paths.extend( @@ -2556,6 +2557,20 @@ def run_xtask_check() -> None: def self_test() -> None: digest_inputs = set(source_digest_inputs()) + for path in (THIRD_PARTY_ROOT / "shared").glob("**/*.toml"): + if path.is_file() and rel(path) not in digest_inputs: + fail(f"self-test expected shared third-party source pin in extension digest inputs: {rel(path)}") + unrelated_third_party_inputs = sorted( + path + for path in digest_inputs + if path.startswith("src/sources/third-party/") + and not path.startswith("src/sources/third-party/shared/") + ) + if unrelated_third_party_inputs: + fail( + "self-test expected non-shared runtime source pins to stay outside extension digest inputs: " + + ", ".join(unrelated_third_party_inputs) + ) for path in [ "src/extensions/external/vector/.release-semantic-inputs.json", "src/extensions/external/vector/VERSION", diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/.gitattributes b/src/runtimes/liboliphaunt/wasix-postmaster/.gitattributes new file mode 100644 index 00000000..8ce897d8 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/.gitattributes @@ -0,0 +1,2 @@ +# Unified-diff artifacts preserve the source whitespace they replay. +postgres/patches/*.patch whitespace=-trailing-space,-space-before-tab,-blank-at-eof diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/.gitignore b/src/runtimes/liboliphaunt/wasix-postmaster/.gitignore new file mode 100644 index 00000000..b8aead57 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/.gitignore @@ -0,0 +1,16 @@ +/build/ +/builds/ +/install/ +/reports/ +/run/ +/sources/ +/tools/* +!/tools/sealed-export-closure/ +/tools/sealed-export-closure/target/ +/work/ +*.a +*.dylib +*.log +*.o +*.so +*.wasm diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/README.md b/src/runtimes/liboliphaunt/wasix-postmaster/README.md new file mode 100644 index 00000000..7994b1a5 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/README.md @@ -0,0 +1,726 @@ +# liboliphaunt WASIX Postmaster + +This project is the reproducible research lane for a real PostgreSQL 18 +postmaster on WASIX: one postmaster accepting concurrent connections and one +isolated WASIX process per PostgreSQL backend. It is independent of +`pg_durable` and independent of the existing single-user +`liboliphaunt-wasix` product. + +The source was recovered from the `f0rr0/wasix-pg18-experiment` checkpoint at +commit `aa4211f4485327cb7493863ea3380acf27c5f951`. The checkpoint demonstrated +the architecture, but it did not yet meet native-like throughput or +resident-memory goals and did not contain a defensible query-tail-latency +measurement. This directory is therefore a non-release project: +it deliberately has no `release.toml`, release-product tag, SDK carrier, or +platform-support claim. + +## Stabilization checkpoint (2026-08-09) + +The imported experiment now has a clean, pinned-source replay and a green +Linux x86_64 runtime plus `release-o3` guest/core proof chain. The candidate +runtime ABI is +`5bb33347acd61470f80c55fb7905f188375b26f99252bbad2eb81f96c0374d18`; +the Wasmer and product-executor receipt SHA-256 values are +`ee49ac11894ca5f747906898bf6a8880cc52c9a7c1a7b9c34ef97faae12c8f82` +and +`201290f0690e8ac5f59add80bb8c3857fb6397b8ec1d2d6bda70789a9715b54b`. +The Wasmer and wasix-libc patch SHA-256 values are +`5d6bc8c6f8cf250daedcefc686f0ba8b0a17662ee9fc3a8c917497c9c5e2963d` +and +`59a936d5f6398b5b60c3e6e8b6c220a02a22603daf2eb0920f227021d56ffe7d`. + +The guest proof distinguishes the 1,111 pre-export-DCE fences from the 995 +fences in the sealed module. The final module preserves the exact latch +operators, all 4,739 table entries, and the canonical `bin/postgres` proof +identity. Export closure reduced 23,187 exports to 383, local functions from +16,923 to 12,777, and local globals from 10,911 to 1,020. The final guest +receipt SHA-256 is +`7203426ba7285ff11af924191ca656a841cdc36becd9e566ef0266c9af19920e`; +the final concurrency and linear-memory receipt SHA-256 values are +`5d328952ebb63bf6a788bd89796a96c2128682f812eec99495b858a28952144e` +and +`4a218ea45b57ee9472c2645216e8ca6e9b6c00f93e154e7b39261f97027b9ab7`. + +No new carrier is admitted at this checkpoint. The proof closure contains 27 +runtime-loadable side modules, while the existing prototype carrier packages +only five guest modules. The next carrier must derive its complete lazy-AOT +inventory from `runtime/policies/sealed-side-modules.v1.tsv`; otherwise it must +be explicitly named and qualified as a narrower product. Packaging or smoking +the five-module carrier would overstate PostgreSQL semantics. + +The controlled shared-memory experiment also resolves an important +performance question. Switching the same 256/224/0 MiB run from the portable +regular-file provider to Linux tmpfs raised throughput from 57,176 to 116,686 +ops/s while fan-out PSS stayed effectively flat (99.731 versus 98.314 MiB) and +cgroup peak stayed near 224.68 MiB. The gain is writeback/reclaim behavior, not +elimination of a duplicated guest-memory copy. The main remaining high-water +RSS target is Wasmer's unbounded retired coroutine-stack pool, followed by +allocator arenas; the robust fix is bounded/decommitting retention with +wave-to-quiescence telemetry, not weakening the fresh-instance architecture or +blindly shrinking its 32 MiB stack reservation. + +## Architecture + +The default path compiles PostgreSQL with `EXEC_BACKEND`. PostgreSQL launches a +backend with `vfork()` followed by `execv()`; WASIX maps that handoff onto its +process-state and exec syscalls and starts a fresh Wasmer instance. The child +restores PostgreSQL's serialized `BackendParameters` and reattaches the +postmaster's file-backed shared-memory segment at the original guest address +with `MAP_SHARED | MAP_FIXED`. + +```text +liboliphaunt supervisor (one native host process per cluster) + pinned Wasmer libraries + one logical WASIX process group + PostgreSQL postmaster.wasm + TCP listen + epoll + file-backed main shared memory + | + +-- vfork + execv --> backend.wasm instance + | restore BackendParameters + | fixed-address shared-memory reattach + +-- vfork + execv --> backend.wasm instance +``` + +The compiler-bearing Wasmer CLI remains the diagnostic and AOT-production +path. The research lane also contains a compiler-free `wasmer-headless` +carrier prototype with a fixed five-module PostgreSQL closure. That prototype +is not a complete carrier for the currently proven 27-side-module guest +closure and is therefore not admitted by this checkpoint. Before execution, the +carrier verifier streams an exact `payload.files` inventory and rejects a +missing, unexpected, renamed, symlinked, special, changed-during-read, or +digest/size-mismatched entry. The strict loader then verifies the manifest, +runtime ABI, producer recipe, executor identity, raw modules, and AOT bytes +before deserializing the already-compiled modules. The carrier also +contains independently reproduced post-start linear-memory images for +`initdb` and `postgres`; the builder verifies and flattens their receipts into +the manifest, and the loader maps their identical initialized prefixes +privately after ordinary module start. Exact sealed guest paths then resolve +from an immutable in-memory module registry, so warm +`EXEC_BACKEND` launches do not reread, rehash, or compile PostgreSQL. This is +implemented research machinery, not a release or all-platform support claim. + +The production boundary described in `docs/architecture.md` is a minimal +liboliphaunt supervisor built on Wasmer libraries; it owns verified guest/AOT +loading, mappings and futex lifetime, logical processes, signals/timers, +resource policy, and telemetry. The generic CLI and its writable cache are not +a release carrier, and the local sealed manifest is not yet a cryptographically +signed release trust root. + +The imported copied-memory `proc_fork` implementation and its probes are kept +as prior art, but no project task executes its runtime tests or probes. Its +continuation capture is materially more complex, retained too much per-child +runtime state, and +previously trapped on full PostgreSQL while consuming roughly 160 MiB per +child. The promotion candidate is the exec-backend topology unless new data +disproves it. PostgreSQL's selected path is `vfork()` followed by a fresh +`exec`, so making copied-fork memory transfer image-aware would not reduce the +normal postmaster/backend path. That research is deferred rather than added to +the canonical runtime patch without an end-to-end beneficiary. + +`runtime/bin/run-upstream-checks.sh` is retained only for checkpoint +archaeology. Its unfiltered default includes copied-fork tests, so it is not a +current qualification command. + +See [docs/architecture.md](docs/architecture.md) for component boundaries and +promotion gates, [docs/replay-status.md](docs/replay-status.md) for the carrier +and evidence identity, +[docs/immutable-carrier-deployment.md](docs/immutable-carrier-deployment.md) +for the privileged Linux ext deployment, unprivileged direct-loader contract, +crash recovery, and current path-race trust boundary, +[docs/cold-ownership-qualification.md](docs/cold-ownership-qualification.md) +for the Linux page-cache/cgroup ownership contract, and +[docs/rss-memory-model.md](docs/rss-memory-model.md) for the paired PSS +baseline, proven root causes, embedded-carrier roadmap, rejected shortcuts, and +reproducible measurement protocol. The original checkpoint reports remain +under `docs/` and are explicitly historical evidence rather than current +support claims. + +## Previously retained Linux carrier evidence + +The last measured five-module research runtime had runtime ABI +`995f6a9bf69ce6ff154533369eb4f9d6c45d9dfca13fdc213e0f6be8ae405217`. +Its compiler-bearing Wasmer producer is +`b4f8f34a5fc8d2419e97359a7a58c524811d8651285383525103b12eaae1e1a4`, +its headless executor is +`b9fb2eac796ddccee98cfd1277dd8518269c47d79ecb0b1426e4cca86a611327`, +and its canonical v2 build receipt is +`163b6591fe929a28a9edb6addc55714213e33fdc581299633e7cfd2b2be729a1`. +The verified carrier is reproduced at +`target/oliphaunt-wasix-postmaster/carriers/wasix-postmaster-18.4-995f6a9bf69ce6ff-8e907e600fa9d7197c2ae98ddece5cb3093e4e7e3caf8f27f325a24955c120a7/`. +The final suffix is the full `payload.files` SHA-256. Default carrier names bind +both the runtime ABI and content identity, so safe-O2 and O3 profiles cannot +collide merely because they use the same executor ABI. The sealed manifest +SHA-256 is +`cea8c0933fa01f6646184c1f97c2156300e50bcf8a5d1d2e38fbb4ed2bb11fec`. + +On one Linux x86_64 host, the final carrier's +`final-995f-embedded-c4-lower-pressure-v1` run passed the predeclared +`embedded-c4-lower-pressure-v1` memory-performance budget. Readiness PSS was +66.862 MiB. The budgeted four-client peak was 132.708 MiB PSS, 77.473 MiB +anonymous PSS, and 1.164 MiB page tables, below the 160/96/2 MiB ceilings. +Whole-run cgroup charge peaked at 224.707 MiB under a 256 MiB hard limit and +224 MiB high threshold with swap disabled. The required fan-out phases +recorded 2,528 `memory.high` events and 0.003289 memory-PSI some/full stall +fractions, below the predeclared 4,096, 0.015, and 0.010 ceilings; there were no +`memory.max`, OOM, OOM-kill, or swap events. The measured bulk row-operation +rates were 352,846.832/359,874.044/42,854.082/140,449.438 ops/s for +read/mixed-write/update/insert. This is one passed memory-performance run on one +carrier and host. The profile remains `candidate-unqualified` pending the other +composition lanes; it is not release or cross-platform qualification. + +That retained run is not cold page-cache ownership evidence: its complete +carrier verification and initdb occurred outside the measured server scope and +could precharge/preheat regular-file pages. Its PSS and active-scope budget +result remain valid warm-state observations, but a cold embedded claim now +requires the independent zero-residency receipt described below. + +Earlier retained Linux carrier runs moved readiness PSS from 226.940 through +150.062, 129.929, and 113.901 to 79.101 MiB while preserving fresh backend +instances and exact shared memory. These are named successive historical runs, +not additive component accounting. The prior safe-O2 carrier completed all +four one-client workloads inside a 192 MiB hard cgroup with swap disabled and +indexed read inside 96 MiB. Its four-client 100k runs completed inside 192 MiB +as a reclaim-heavy survival tier and inside 256 MiB as a lower-pressure +characterization; their values remain optimization history, not the last +retained five-module carrier's budget evidence. + +The benchmark's workload duration is bulk batch wall time. Its derived rate is +logical row operations per second, not request/transaction throughput, backend +launch latency, or a per-query p95. The optimized O3/ThinLTO/Binaryen guest +reached 0.754/0.801/0.779/0.829 of native in a direct one-million-iteration +one-client characterization. A balanced one-block 100k diagnostic passed all +throughput gates at 0.746/0.788/0.833/0.874 for read/mixed-write/update/insert, +but failed the bulk-batch residual gates for mixed-write, update, and insert. +O3 remains unpromoted: a fresh +four-client, one-million-iteration mixed-write run did not finish in 300 +seconds while clients serialized on relation extension, and a separate +contaminated sequence produced PostgreSQL's `IO in wrong state: 0` AIO error. +The concurrent harness now has an opt-in native-libpq true-latency lane. It +retains raw `CLOCK_MONOTONIC` nanoseconds and per-row warmup/measure status for +both repeated `SELECT 1` calls on one persistent connection and complete +`PQconnectdb` -> `SELECT 1` -> `PQfinish` reconnect/backend-launch operations. +Only an exact all-success sample set produces nearest-rank p50/p95/p99 output; +timeouts, missing rows, failed queries, and an unclean enclosing lifecycle +leave percentile fields invalid. The implementation and a tiny native smoke +are exercised, but no WASIX latency result is claimed yet. A statistically +useful paired native/WASIX run remains required before promotion. + +Compact WASIX lifecycle snapshots are intentionally excluded from every timed +workload and true-libpq qualification run: enabling them inserts timer/select, +registry-snapshot, formatting, and file-sync work into the parked-wait path. +They are used only by `--wasix-lifecycle-plateau`, an untimed reconnect lane. +`--wasix-wait-dump-verbose` is likewise diagnostic-only in that lane: it records +per-state FD, socket, and epoll inventories for long reconnect proofs, while +every timed lane rejects it so the inventory walk cannot perturb latency. +That lane opens readiness and post-quiescence windows, atomically requests a +runtime-writer fence at each close, and waits on a separate atomically published +`wasix-runtime-fence-commit-v1` ACK. The ACK names the exact synced-log byte +offset; the visible `wasix-runtime-fence-v1` line is never itself treated as a +commit signal. Runtime records use one gapless sequence starting at one and +cover process/task ownership, process topology/thread/retirement state, +runtime-state observer ownership, private/shared futex waiters and wakers, +epoll state/subscriptions/queues/guards, shared-registry slots, mappings, and +guest FDs. The harness freezes exactly the committed prefix, re-verifies the +ACK after writing, records the ACK and frozen hashes plus offset, appends the +canonical closing marker, and makes the validator independently verify that +receipt. Stable-tail coverage must reach both fences; foreign observers/wait +kinds or contradictory aggregate state inside the terminal tail fail closed. + +The current idle PostgreSQL topology legitimately contains six registered +WASIX processes and five parent-child edges, with six corresponding runtime +states. It has eleven execution leases: six active task owners plus the five +suspended parent continuations retained by the postmaster's EXEC_BACKEND +vfork/exec chain. The exploratory lifecycle policy therefore does not impose +the obsolete absolute-one topology. It requires readiness and post-reconnect +occupancy tuples to remain exactly equal and independently checks the +relational six-process/five-edge invariants. Pending child +publication, quiescence wakers, retiring nodes, stale registry entries, and +contradictory ownership still fail closed. Inner resource occupancy remains an +`exploratory-unbounded` policy whose claim scope is +`relative-to-fresh-baseline`: equality proves no reconnect growth, but not that +the fresh baseline is legitimately small. The pre-run policy hash is bound to +PostgreSQL profile, Wasmer, guest-module, and sealed-carrier identities. A +separate qualifying run must promote exact inner bounds under a distinct +`*-qualified-vN` policy ID; relabeling the exploratory policy is rejected. +Absolute PSS/cgroup budgets remain the authority on embedded baseline size. + +The lifecycle lane now establishes its readiness baseline only after a +causal PostgreSQL maintenance barrier. PostgreSQL 18's background writer emits +its first periodic running-transactions WAL record after roughly 15 seconds; +that wakes the WAL writer, which lazily opens the current WAL segment and keeps +the VFD cached. The observed aggregate guest-FD transition from 70 to 71 was +therefore PID 6, the WAL writer, opening +`pg_wal/000000010000000000000001`; PID 7 already held `global/1262` before the +transition. Rather than sleeping for the timer or special-casing an FD path, +the harness invokes `pg_log_standby_snapshot()`, records its target LSN, and +polls fresh `pg_stat_io` snapshots until the `walwriter`/`wal`/`normal` write +and byte counters increase and `pg_current_wal_flush_lsn()` reaches that LSN, +with an unchanged `stats_reset`. Only then can the readiness fence define the +steady topology. The barrier is unavailable when `wal_level` is below +`replica`, which is a fail-closed/not-applicable condition rather than a reason +to accept the cold baseline. + +After a clean exploratory run passes, freeze its observed tuple into a new +exact policy; this is a policy-authoring step, not qualification of that run: + +```sh +project=src/runtimes/liboliphaunt/wasix-postmaster +report=target/oliphaunt-wasix-postmaster/reports/EXPLORATORY +"$project/bin/freeze-wasix-lifecycle-policy.py" \ + --exploratory-result "$report/wasix-runtime-plateau.tsv" \ + --log "$report/wasix/wasix-runtime-evidence.log" \ + --freeze-receipt "$report/wasix/wasix-runtime-evidence.freeze.tsv" \ + --baseline-policy "$report/wasix-lifecycle-baseline-policy.tsv" \ + --baseline-binding "$report/wasix-lifecycle-baseline-binding.tsv" \ + --output target/oliphaunt-wasix-postmaster/policies/pg18-idle-postmaster-stabilized-qualified-v1.tsv \ + --policy-id pg18-idle-postmaster-stabilized-qualified-v1 +``` + +Run an independent clean lifecycle campaign with that output passed through +`--lifecycle-baseline-policy`; only that later run may produce +`qualification-bounded` plateau evidence. Keep verbose inventories diagnostic +and out of all timed commands. + +The four performance lanes do not yet compose into an embedded-viability +claim. `compose-wasix-embedded-performance-viability.py` is intentionally a +negative gate: it validates the checked-in unresolved policy and emits an +immutable non-release failure receipt, with no component manifest. A positive +path remains source-disabled until every lane binds the same Linux host/boot +receipt and the memory lane predeclares its 100k-row/100k-iteration workload, +controlled checkpoint mode, full resource sampling, and 0.1-second interval. + +The declared embedded PostgreSQL profile (`io_method=sync`, +`shared_buffers=32MB`) now has a same-carrier, four-client budget pass at +256/224 MiB hard/high: 66.862 MiB readiness PSS and 97.635/132.120/117.302/ +132.708 MiB read/mixed/update/insert fan-out PSS. Earlier 192/176 and 160/144 +MiB runs are retained as pressure characterizations: the former completed with +4,650 high events, while the latter's 11,804 events and severe update slowdown +make 160 MiB survival-only. This profile reduces process and memory cost; it +does not yet qualify WAL durability, query/reconnect latency, or other hosts. + +Raw RSS is intentionally not a promotion gate. The carrier aliases the same +PostgreSQL shared-memory backing into multiple guest memories, so RSS counts +the same resident pages at every alias. Linux PSS plus cgroup current/peak, +events, swap, and mapping identity are the current physical-memory gates. Only +Linux x86_64 has passed them. The Unix private-image mapper exists but macOS is +unqualified; the Windows private-image mapping backend is unsupported. + +## Layout + +- `postgres/`: PostgreSQL port overlay and ordered PG18 patch series. +- `postgres/product-patch-provenance.toml`: native behavior, WASIX fact, + rejected alternatives, and the exact proof obligation for locally designed + product patches. +- `docs/semantic-cache-offers.md`: exact guest relation-I/O hint seam, numeric + ABI classes, no-op failure contract, and the still-unresolved libc/runtime + dependency. +- `docs/shared-memory-backing-experiment.md`: versioned benchmark providers + for the host directory behind guest `/dev/shm`, exact filesystem/object + evidence, safe cleanup, and the Linux tmpfs diagnostic boundary. +- `postgres/experiment-patch-disposition.toml`: explicit accounting for every + experiment PostgreSQL patch; correctness patches are carried, safer + current-main performance ports are deferred until measured, and + single-user/semantic-policy changes are rejected. +- `runtime/`: pinned Wasmer/wasix-libc patch exports, capability ledger, and + blocker probes. +- `profiles/`: versioned PostgreSQL runtime-footprint and durability contracts, + plus predeclared memory-budget candidates; + the harness records their digests, deterministic resolution, and observed + `pg_settings` values with every named-profile run. +- `probes/bootstrap/`: small initial process, mmap, and identity probes. +- `bench/sql/`: native-versus-WASIX query workloads. +- `bin/`: preparation, build, smoke, regression, benchmark, and profiling + entrypoints. +- `sources.lock.toml`: exact experiment provenance and upstream source pins. +- `target/oliphaunt-sources/checkouts/`: immutable exact-pin upstream source + checkouts shared with the repository source spine. +- `target/oliphaunt-wasix-postmaster/`: disposable patched source worktrees, + builds, installs, reports, runtime caches, and run directories (ignored by + the repository). + +## Build toolchain boundary + +This research product owns its PostgreSQL and runtime experiments, but it does +not own a second WASIX compiler container. Its canonical build-time dependency +is the container recipe and environment currently owned by +`liboliphaunt-wasix` under +`src/runtimes/liboliphaunt/wasix/assets/build/`; the neutral +`source-toolchains` project owns and validates the corresponding pinned +toolchain metadata. The Moon project therefore declares +`liboliphaunt-wasix` as a build-scope dependency, runs its check, and lists the +toolchain manifest, complete Docker recipe, and environment script as explicit +inputs to every task that compiles WASIX code. A recipe change cannot leave a +cached postmaster check or build apparently unaffected. + +This is build-time coupling only. The research carrier neither packages nor +loads `liboliphaunt-wasix`, and it never falls back to a stock downloaded +Wasmer. The supported repository build uses the canonical recipe root; a local +`WASIX_TOOLCHAIN_ROOT` override produces a distinct build-recipe identity and +is not evidence for the canonical carrier. Moving the recipe into a neutral +physical directory remains a reasonable coordinated cleanup, but its +repository-relative path participates in the runtime ABI receipt today. Such a +move must update all production, CI, policy, and xtask consumers, refresh the +WASIX semantic fingerprint, rebuild the carriers, and rerun qualification; it +must not be hidden inside this research import. + +## Exec-backend qualification replay + +From the repository root: + +```sh +OLIPHAUNT_FETCH_SOURCES=1 moon run source-inputs:source-fetch-wasix-postmaster-runtime +moon run liboliphaunt-wasix-postmaster:check +moon run liboliphaunt-wasix-postmaster:prepare-postgres +moon run liboliphaunt-wasix-postmaster:prepare-runtime +moon run liboliphaunt-wasix-postmaster:runtime-build +moon run liboliphaunt-wasix-postmaster:configure +moon run liboliphaunt-wasix-postmaster:postgres-build +moon run liboliphaunt-wasix-postmaster:blocker-probes +moon run liboliphaunt-wasix-postmaster:initdb-smoke +moon run liboliphaunt-wasix-postmaster:initdb-stress +moon run liboliphaunt-wasix-postmaster:smoke +moon run liboliphaunt-wasix-postmaster:regression +# Diagnostic/promotion gate for repeated fresh-postmaster liveness: +moon run liboliphaunt-wasix-postmaster:backend-wave-stress +``` + +The source-fetch and PostgreSQL preparation steps need network access. +Runtime build, configure, probes, and WASIX smoke need a reachable Docker +daemon. Runtime compilation is intentionally manual and expensive: +`runtime-build` builds the patched Wasmer and patched wasix-libc sysroot without +running the imported copied-fork tests. It emits a canonical +`oliphaunt.wasix-postmaster.wasmer-build.v2` receipt that binds the Wasmer and +gitlink pins, both tracked patches and prepared source states, `Cargo.lock`, +the exact libc manifests, host ABI, Rust/LLVM provenance, artifact/runtime ABI, +and separate compiler-bearing producer and compiler-free executor identities +and features. Supported entrypoints reject a missing, noncanonical, +host-mismatched, feature-mismatched, or binary-mismatched receipt and never fall +back to a downloaded or `PATH` Wasmer. + +The same build now emits an isolated +`oliphaunt-wasix-postmaster-executor` binary and +`postmaster-executor-build.receipt`. This product executor exposes only the +sealed run/version contract needed by the PostgreSQL carrier; it does not link +the general CLI, registry, package, WCGI, or compiler command graph. The +separate v3 receipt binds its exact parent Wasmer receipt, runtime ABI, +package/feature set, bounded embedded-postmaster runtime policy, CLI contract, +host toolchain, +and binary bytes. The full `wasmer-headless` binary remains available as an +explicit control, not an implicit fallback. + +`blocker-probes` names the exec-backend capability probes explicitly. It covers +fixed shared-memory remapping, spawn/exec lifecycle, cross-instance +memory-order/latch wake, wait and signal behavior, epoll/socket readiness, +dynamic loading, and exception recovery. It excludes the copied-memory fork, +fork/dlopen, native-continuation probes, and the known-broken one-shot timer +diagnostic. The final +`smoke` starts one postmaster and concurrent native `psql` clients, then +requires distinct backend PIDs and overlapping client lifetimes. Before it, +`initdb-stress` repeats fresh cluster creation 20 times and rejects lifecycle +warnings. After it, `regression` exercises process lifecycle, C extension +loading, and representative SQL/copy behavior through `pg_regress`. +`backend-wave-stress` is a longer manual liveness gate: it repeatedly drives +read, mixed-write, and disjoint indexed-update backend waves through one +postmaster per attempt and preserves the first timeout for diagnosis. A +persistent lock sampler reduced the historical stall to a lost latch wake +after PostgreSQL had already granted a relation-extension lock. Historical +safe-O2 evidence proved Wasmer's real sequentially consistent fence lowering +with a strict 512-round latch probe and 100/100 fresh-postmaster repetitions. +The current release-O3 proof records 1,111 pre-seal fences and 995 final +fences, including the exact latch-critical 2/1/1 counts, after export DCE. This +proves the packed SC latch-state operators survive final-module verification; +the focused and backend-wave gates still need to be repeated before O3 is +promotable. The backend-wave gate remains +outside `run-acceptance.sh`, and every additional host/compiler configuration +needs its own run while the independent timer/lifecycle blockers remain open. + +For a direct replay without Moon, first materialize the immutable pinned +sources under `target/oliphaunt-sources/checkouts/`, then run +`bin/run-acceptance.sh`. The product copies those sources before patching; +all disposable worktrees, build products, caches, run directories, and reports +remain below `target/oliphaunt-wasix-postmaster/`. + +For a native control measurement, run `bin/build-native-oracle.sh` followed by +`bin/smoke-native-oracle.sh`. For the concurrent acceptance path, run +`bin/smoke-wasix-concurrent-connections.sh` after the WASIX build; this is the +same exec-backend path owned by the `smoke` task above. + +Internal shell variables retain the historical `FRESH_*` prefix so the +checkpoint scripts remain auditable. Their default paths and externally visible +project identity are now stable. + +The receipt is build provenance, not the release carrier described in +`docs/architecture.md`: it does not sign or bind the PostgreSQL guest/install +tree, native Wasmer dependencies, or AOT cache. `bin/pin-runtime-artifacts.sh` +therefore creates only a repo-dependent experiment snapshot for benchmark +replay. It must not be published or resolved by an SDK as a supported product. + +## Sealed headless research carrier + +After `runtime-build`, `postgres-build`, and an explicit precompile, build the +local carrier atomically. The builder never compiles implicitly and rejects +host-native CPU artifacts; the current research policy is LLVM aggressive +optimization against a generic baseline CPU: + +```sh +project=src/runtimes/liboliphaunt/wasix-postmaster +WASIX_PRECOMPILE_SCOPE=runtime "$project/bin/precompile-wasix-core.sh" +"$project/bin/build-sealed-headless-carrier.sh" \ + --output target/oliphaunt-wasix-postmaster/carriers/headless-research +# Paired general-headless control: +"$project/bin/build-sealed-headless-carrier.sh" \ + --executor-role full-headless \ + --output target/oliphaunt-wasix-postmaster/carriers/full-headless-control +``` + +Without `--output`, the builder publishes under a name containing the runtime +ABI prefix and the full final `payload.files` SHA-256. The latter is computed +after staging and verification, so different PostgreSQL profiles cannot collide +under one ABI-only default directory. + +The default carrier contains only the selected product executor at +`bin/wasmer-headless` and copies its exact build receipt to the root as +`postmaster-executor.receipt`. The sidecar's exact presence is the role +discriminator. A `full-headless` control omits it and binds the executor +directly to `wasmer-build.receipt`; both roles retain the format-6 +manifest contract. The carrier also contains PostgreSQL `initdb` and `postgres`, +the packaged `libpq`, `dict_snowball`, and `plpgsql` side-module subset, the +PostgreSQL support tree, five AOT artifacts, the v2 build receipt, a strict +format-6 `oliphaunt.wasix-postmaster.sealed-aot.v5` manifest, the canonical +guest build receipt, two immutable +linear-memory images and their capture receipts, and a full payload file +inventory. The manifest carries a deny-unknown `file-cache-policy` object that +binds the sole approved adaptive policy, compiled config ID and config digest, +and its observe-only portable fallback. `initdb` remains observe-only. The +builder first emits an image-free format-4 capture manifest, +runs two independent isolated captures for each executable, and requires both +the image bytes and receipts to match. It then checks final headless +`initdb --version` and `postgres --version` loads through the image-bearing +manifest, runs the same exact-inventory verifier used at benchmark preflight, +and only then fsyncs and atomically publishes the directory. Verify a copied or +stored carrier independently with: + +```sh +src/runtimes/liboliphaunt/wasix-postmaster/bin/verify-sealed-headless-carrier.sh \ + target/oliphaunt-wasix-postmaster/carriers/headless-research +``` + +Materialize a deterministic identity for an exactly verified candidate with: + +```sh +src/runtimes/liboliphaunt/wasix-postmaster/bin/current-evidence-manifest.py write \ + --carrier target/oliphaunt-wasix-postmaster/carriers/headless-research \ + --output target/oliphaunt-wasix-postmaster/evidence/current-candidate.json +src/runtimes/liboliphaunt/wasix-postmaster/bin/current-evidence-manifest.py verify \ + --carrier target/oliphaunt-wasix-postmaster/carriers/headless-research \ + --output target/oliphaunt-wasix-postmaster/evidence/current-candidate.json +``` + +The generated manifest binds the source lock and actual patch bytes to the +receipt, sealed manifest, executor, and payload inventory. Its classification +is always `candidate-unqualified-non-release` and its claim scope is only exact +artifact identity; a separate behavioral evidence receipt must bind its digest +before any documentation can call the carrier qualified or current. + +The verifier treats `manifest.json`, `guest-build.receipt`, +`wasmer-build.receipt`, and `payload.files` as one canonical identity. The +guest receipt binds the explicit `release-o3` candidate or `safe-o2` control, +effective build flags, and a recomputed identity of the exact installed guest +closure. It rejects duplicate manifest or +receipt keys, noncanonical receipts, unsafe or unsorted inventory paths, +missing and unexpected files or directories, symlinks and special files, +empty unlisted directories, and files whose identity changes while their +bytes are hashed. + +Run the concurrent harness without a compiler or mutable Wasmer cache: + +```sh +project=src/runtimes/liboliphaunt/wasix-postmaster +"$project/bin/bench-wasix-concurrent-query-suite.sh" \ + --skip-build \ + --sealed-carrier target/oliphaunt-wasix-postmaster/carriers/headless-research \ + --target wasix \ + --connections 1 \ + --workloads 'indexed-read mixed-write indexed-update indexed-insert' \ + --runtime-footprint embedded-concurrent \ + --durability safe \ + --resource-detail full \ + --quiescence-seconds 5 \ + --memory-map-snapshots \ + --label sealed-headless-c1 +``` + +That invocation is a Linux smaps/PSS attribution diagnostic. Because it does +not configure a dedicated cgroup, cgroup limits, events, and swap state are not +part of its evidence and it cannot qualify a physical-memory budget. Use the +frozen cgroup command and predeclared budgets in +[the memory protocol](docs/rss-memory-model.md#reproducible-measurement-protocol) +for memory qualification. Run the balanced timed lane separately so sampling +overhead cannot enter the throughput or latency distribution: + +To prove that verifier and initdb activity did not preheat carrier/PGDATA +pages outside the measured server cgroup, run the independent cold campaign: + +```sh +"$project/bin/qualify-wasix-cold-ownership.sh" \ + --sealed-carrier target/oliphaunt-wasix-postmaster/carriers/headless-research \ + --blocks 10 \ + --runtime-footprint embedded-concurrent \ + --durability safe \ + --memory-max 256M \ + --memory-high 224M \ + --swap-max 0 \ + --resource-interval 0.05 \ + --label sealed-headless-cold-ownership +``` + +This targeted lane hashes, synchronizes, evicts, and proves zero residency for +every regular carrier and fresh-PGDATA page immediately before postmaster +launch. It records spawn-to-first-query latency and whole-scope memory, +dirty/writeback, pressure, and I/O evidence without global `drop_caches`. + +```sh +"$project/bin/qualify-wasix-single-backend.sh" \ + --sealed-carrier target/oliphaunt-wasix-postmaster/carriers/headless-research \ + --blocks 10 \ + --connections 1 \ + --iterations 100000 \ + --rows 100000 \ + --runtime-footprint embedded-concurrent \ + --durability safe \ + --cgroup-memory-max 256M \ + --cgroup-memory-high 224M \ + --cgroup-swap-max 0 \ + --label sealed-headless-c1-timed +``` + +The qualifier uses `--resource-detail off`, freezes and repeatedly verifies the +complete packaged carrier inventory, and fails any native/WASIX pair whose effective +PostgreSQL settings differ. The named embedded footprint is +`io_method=sync`, `shared_buffers=32MB`, `max_connections=8`, +`max_wal_senders=10`, `autovacuum_worker_slots=4`, and +`max_worker_processes=8`; the worker-capacity settings are bound to the sealed +96-task host budget. The `safe` durability profile retains `fsync=on`, +`full_page_writes=on`, and `synchronous_commit=on`. +Qualification rejects an explicit `--postgres-guc` that overlaps either named +profile, while non-overlapping diagnostic settings remain recorded. Full +resource results are cumulative suite evidence; they must not be mixed into +the timed percentile population. The three cgroup options are all-or-none. +The qualifier freezes them in its pre-run policy and machine result, removes +the matching ambient `WASIX_CGROUP_*` variables before every harness launch, +and passes the exact triple explicitly to the lower-level harness. The finite +scope is enforced during the timed run, but resource sampling remains off, so +this receipt does not replace the independent memory-evidence lane. + +When a constrained Linux throughput result is intended to demonstrate that +adaptive WAL cache relief actually participated, opt into the acting-evidence +policy on a WAL-producing lane: + +```sh +"$project/bin/qualify-wasix-single-backend.sh" \ + --sealed-carrier target/oliphaunt-wasix-postmaster/carriers/headless-research \ + --blocks 10 \ + --connections 4 \ + --iterations 100000 \ + --rows 100000 \ + --workload indexed-update \ + --runtime-footprint embedded-concurrent \ + --durability safe \ + --cgroup-memory-max 256M \ + --cgroup-memory-high 224M \ + --cgroup-swap-max 0 \ + --adaptive-cache-evidence-policy constrained-linux-wal-action-v1 \ + --label sealed-headless-c4-256m-adaptive +``` + +The default `portable-correctness-v1` continues to accept either exact active +telemetry or the sealed observe-only fallback, which is required for portable +correctness campaigns. The constrained policy is CLI-only and fail-closed: it +requires adaptive-active admission, class-6 offers, positive class-6 advice +calls and advised bytes, and zero sampler, clock, or advice errors in every +WASIX sample. Native samples remain portable controls. The selected policy, +validator digest, per-sample policy receipts, and validation receipts are +bound into the qualification policy and machine result. + +Qualify true persistent-query and reconnect/backend-lifecycle latency in its +own balanced fresh-server lane: + +```sh +"$project/bin/qualify-wasix-libpq-latency.sh" \ + --sealed-carrier target/oliphaunt-wasix-postmaster/carriers/headless-research \ + --blocks 10 \ + --warmup 100 \ + --samples 1000 \ + --runtime-footprint embedded-concurrent \ + --durability safe \ + --cgroup-memory-max 256M \ + --cgroup-memory-high 224M \ + --cgroup-swap-max 0 \ + --label sealed-headless-true-libpq +``` + +The runner alternates fresh-server ABBA/BAAB blocks, declares relative and +absolute p95/p99 gates before the first sample, and binds every raw stream to +the exact carrier, native oracle, probe, libpq, named profile, and qualification +plan. See [true-libpq latency qualification](docs/libpq-latency-qualification.md) +for the gate semantics, initial evidence, and fail-closed receipt model. + +Before treating a carrier as durable, exercise the host-to-guest shutdown and +WAL-recovery boundary independently of the throughput lane: + +```sh +"$project/bin/qualify-wasix-immediate-recovery.sh" \ + --sealed-carrier target/oliphaunt-wasix-postmaster/carriers/headless-research \ + --require-zero-write-aot \ + --immutable-carrier-receipt /var/lib/oliphaunt/wasix-postmaster-receipts/headless-research.json \ + --cgroup-memory-max 256M \ + --cgroup-memory-high 224M \ + --cgroup-swap-max 0 \ + --transactions 16 \ + --rows-per-transaction 128 \ + --label sealed-headless-immediate-recovery +``` + +This Linux qualifier fixes the same `embedded-concurrent` and `safe` profile +inputs, creates a controlled checkpoint, records multiple acknowledged and +flushed commits, and proves the checkpoint did not advance. Qualification mode +fails closed unless the carrier is bound to an external immutable-deployment +receipt, all four outer executor invocations activate directly from immutable +AOT/memory-image inodes with a validated zero-write audit, and each of the +three postmaster trees proves its exact finite `MemoryMax`, `MemoryHigh`, and +`MemorySwapMax` cgroup-v2 controls and its identity-checked scope drains after +shutdown. The initdb executor legitimately activates sealed postgres for its +bootstrap/single-user work and may activate exact-manifest dynamic modules. +Those remain fully validated loader records; the population gate classifies +the one initdb and three postmaster invocations separately instead of treating +bootstrap postgres as a fourth outer postmaster. Full cryptographic carrier +verification brackets the +campaign; receipt-bound inode and `+i` verification guards each intermediate +execution. It then sends SIGQUIT only to the birth-identity-checked Wasmer +leader. Success requires PostgreSQL's immediate-shutdown path—not host +termination or bounded escalation—to drain the process group, listener, and +`/dev/shm` carrier. The same sealed closure, PGDATA, and shared-object mount +must restart through WAL redo with an exact content checksum. A bridged SIGTERM +must subsequently perform smart shutdown, and a third open must be clean. + +Use `--mode diagnostic` only for compatibility investigation when immutable +deployment or cgroup delegation is unavailable. Diagnostic mode retains the +same recovery assertions but is explicitly non-promotional; zero-write loader +and cgroup controls may still be supplied and are then verified identically. + +That test covers a guest-handled immediate shutdown. It deliberately does not +claim recovery after host SIGKILL, power loss, or supervisor crash: those paths +still require a cluster lease and runtime-owned crash-safe named-shared-object +lifecycle so no external stale-file deletion is needed. + +Do not add `--skip-precompile` in sealed mode: there is no compiler or cache to +skip. The loader uses detached AOT deserialization, retaining linked executable +code and runtime-required metadata while dropping the serialized archive after +linking. On a fresh executable instance, ordinary module start still runs first +so passive data segments, LLVM TLS relocations, globals, and segment-drop state +retain their normal semantics. Before dynamic linking, the runtime compares the +captured prefix byte-for-byte and replaces only that identical range with an +immutable file-backed private mapping. Writes remain per-instance COW; a +receipt, layout, or byte mismatch fails before side-module relocations and +constructors. The memory and measurement model is documented in +[docs/rss-memory-model.md](docs/rss-memory-model.md). diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/checkpoint-database-state.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/checkpoint-database-state.sql new file mode 100644 index 00000000..d22b85af --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/checkpoint-database-state.sql @@ -0,0 +1,47 @@ +\set ON_ERROR_STOP on + +COPY ( + SELECT relation, + row_count, + numeric_sum, + hash_sum, + hash_xor + FROM ( + SELECT + 'accounts'::text AS relation, + count(*)::numeric AS row_count, + sum(balance)::numeric AS numeric_sum, + sum(hashtextextended( + client_id::text || ':' || slot::text || ':' || + balance::text || ':' || payload, 0 + )::numeric) AS hash_sum, + bit_xor(hashtextextended( + client_id::text || ':' || slot::text || ':' || + balance::text || ':' || payload, 0 + )) AS hash_xor + FROM oliphaunt_checkpoint_accounts + UNION ALL + SELECT + 'append', count(*)::numeric, sum(sequence)::numeric, + sum(hashtextextended( + client_id::text || ':' || sequence::text || ':' || + ordinal::text || ':' || payload, 0 + )::numeric), + bit_xor(hashtextextended( + client_id::text || ':' || sequence::text || ':' || + ordinal::text || ':' || payload, 0 + )) + FROM oliphaunt_checkpoint_append + UNION ALL + SELECT + 'volume', count(*)::numeric, sum(value)::numeric, + sum(hashtextextended( + id::text || ':' || value::text || ':' || payload, 0 + )::numeric), + bit_xor(hashtextextended( + id::text || ':' || value::text || ':' || payload, 0 + )) + FROM oliphaunt_checkpoint_volume + ) AS state + ORDER BY relation +) TO STDOUT WITH (FORMAT csv, HEADER true, DELIMITER E'\t'); diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/checkpoint-volume.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/checkpoint-volume.sql new file mode 100644 index 00000000..f99aabcb --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/checkpoint-volume.sql @@ -0,0 +1,15 @@ +\set ON_ERROR_STOP on + +BEGIN; + +UPDATE oliphaunt_checkpoint_volume + SET value = value + 1, + payload = md5(id::text || ':' || (value + 1)::text) || + repeat('w', 480); + +INSERT INTO oliphaunt_checkpoint_volume (id, value, payload) +SELECT id, 1, md5(id::text || ':1') || repeat('n', 480) +FROM generate_series(200001, 300000) AS ids(id); + +COMMIT; +CHECKPOINT; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/checkpoint-workload-setup.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/checkpoint-workload-setup.sql new file mode 100644 index 00000000..48f711ea --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/checkpoint-workload-setup.sql @@ -0,0 +1,83 @@ +\set ON_ERROR_STOP on + +BEGIN; + +CREATE TABLE oliphaunt_checkpoint_accounts ( + client_id integer NOT NULL, + slot integer NOT NULL, + balance bigint NOT NULL DEFAULT 0, + payload text NOT NULL, + PRIMARY KEY (client_id, slot) +) WITH (fillfactor = 80); + +INSERT INTO oliphaunt_checkpoint_accounts (client_id, slot, payload) +SELECT client_id, slot, + md5(client_id::text || ':' || slot::text) || repeat('a', 480) +FROM generate_series(1, :connections::integer) AS clients(client_id) +CROSS JOIN generate_series(0, 65535) AS slots(slot); + +CREATE TABLE oliphaunt_checkpoint_append ( + client_id integer NOT NULL, + sequence bigint NOT NULL, + ordinal integer NOT NULL, + payload text NOT NULL, + PRIMARY KEY (client_id, sequence, ordinal) +); + +CREATE TABLE oliphaunt_checkpoint_volume ( + id integer PRIMARY KEY, + value bigint NOT NULL, + payload text NOT NULL +) WITH (fillfactor = 80); + +INSERT INTO oliphaunt_checkpoint_volume (id, value, payload) +SELECT id, 0, md5(id::text) || repeat('v', 480) +FROM generate_series(1, 200000) AS ids(id); + +CREATE FUNCTION oliphaunt_checkpoint_transaction( + p_client_id integer, + p_sequence bigint +) RETURNS TABLE ( + update_count bigint, + insert_count bigint, + read_count bigint, + insert_lsn pg_lsn +) LANGUAGE sql VOLATILE AS $function$ + WITH updated AS ( + UPDATE oliphaunt_checkpoint_accounts AS accounts + SET balance = accounts.balance + 1, + payload = md5( + p_client_id::text || ':' || p_sequence::text || ':' || + accounts.slot::text + ) || repeat('u', 480) + FROM generate_series(1, 48) AS offsets(slot_offset) + WHERE accounts.client_id = p_client_id + AND accounts.slot = + mod(p_sequence * 48 + offsets.slot_offset, 65536)::integer + RETURNING 1 + ), appended AS ( + INSERT INTO oliphaunt_checkpoint_append ( + client_id, sequence, ordinal, payload + ) + SELECT p_client_id, p_sequence, ordinal, + md5( + p_client_id::text || ':' || p_sequence::text || ':' || + ordinal::text + ) || repeat('i', 480) + FROM generate_series(1, 16) AS ordinals(ordinal) + RETURNING 1 + ), observed AS ( + SELECT 1 + FROM oliphaunt_checkpoint_accounts + WHERE client_id = p_client_id + ORDER BY slot + LIMIT 8 + ) + SELECT (SELECT count(*) FROM updated), + (SELECT count(*) FROM appended), + (SELECT count(*) FROM observed), + pg_current_wal_insert_lsn() +$function$; + +COMMIT; +CHECKPOINT; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/core-smoke.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/core-smoke.sql new file mode 100644 index 00000000..ef4b8c63 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/core-smoke.sql @@ -0,0 +1,30 @@ +\set ON_ERROR_STOP on + +SELECT 1 AS one; + +DROP TABLE IF EXISTS fresh_wasix_smoke; +CREATE TABLE fresh_wasix_smoke ( + id integer PRIMARY KEY, + payload text NOT NULL +); + +INSERT INTO fresh_wasix_smoke VALUES + (1, 'alpha'), + (2, 'beta'), + (3, 'gamma'); + +SELECT count(*) AS inserted_rows FROM fresh_wasix_smoke; +UPDATE fresh_wasix_smoke SET payload = payload || '-updated' WHERE id = 2; +DELETE FROM fresh_wasix_smoke WHERE id = 3; + +BEGIN; +INSERT INTO fresh_wasix_smoke VALUES (4, 'rolled-back'); +ROLLBACK; + +SELECT count(*) AS durable_rows FROM fresh_wasix_smoke; + +PREPARE fresh_wasix_lookup(integer) AS + SELECT payload FROM fresh_wasix_smoke WHERE id = $1; +EXECUTE fresh_wasix_lookup(2); + +COPY fresh_wasix_smoke TO STDOUT WITH CSV; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/perf-probes.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/perf-probes.sql new file mode 100644 index 00000000..9398f651 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/perf-probes.sql @@ -0,0 +1,23 @@ +\set ON_ERROR_STOP on +\timing on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif + +DROP TABLE IF EXISTS fresh_wasix_perf; +CREATE TABLE fresh_wasix_perf ( + id integer PRIMARY KEY, + payload text NOT NULL +); + +INSERT INTO fresh_wasix_perf +SELECT i, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; + +SELECT count(*) FROM fresh_wasix_perf; +SELECT payload FROM fresh_wasix_perf WHERE id = (:perf_rows * 3 / 4); +CREATE INDEX fresh_wasix_perf_payload_idx ON fresh_wasix_perf (payload); +SELECT count(*) FROM fresh_wasix_perf WHERE payload >= 'a' AND payload < 'b'; + +COPY fresh_wasix_perf TO STDOUT WITH CSV; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/bulk-insert.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/bulk-insert.sql new file mode 100644 index 00000000..3b18f68f --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/bulk-insert.sql @@ -0,0 +1,22 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_bulk_insert; +CREATE TABLE fresh_wasix_bulk_insert ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); + +\timing on +INSERT INTO fresh_wasix_bulk_insert +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; +\timing off + +SELECT count(*) FROM fresh_wasix_bulk_insert; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/copy-out.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/copy-out.sql new file mode 100644 index 00000000..85e80d00 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/copy-out.sql @@ -0,0 +1,23 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_copy_out; +CREATE TABLE fresh_wasix_copy_out ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); +INSERT INTO fresh_wasix_copy_out +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; + +\o /dev/null +\timing on +COPY fresh_wasix_copy_out TO STDOUT WITH CSV; +\timing off +\o diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/index-build.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/index-build.sql new file mode 100644 index 00000000..46200332 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/index-build.sql @@ -0,0 +1,24 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_index_build; +CREATE TABLE fresh_wasix_index_build ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); +INSERT INTO fresh_wasix_index_build +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; + +\timing on +CREATE INDEX fresh_wasix_index_build_payload_idx + ON fresh_wasix_index_build (payload); +\timing off + +SELECT count(*) FROM fresh_wasix_index_build; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-insert.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-insert.sql new file mode 100644 index 00000000..49398c18 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-insert.sql @@ -0,0 +1,26 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_indexed_insert; +CREATE TABLE fresh_wasix_indexed_insert ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); +CREATE INDEX fresh_wasix_indexed_insert_bucket_idx + ON fresh_wasix_indexed_insert (bucket); +CREATE INDEX fresh_wasix_indexed_insert_payload_idx + ON fresh_wasix_indexed_insert (payload); + +\timing on +INSERT INTO fresh_wasix_indexed_insert +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; +\timing off + +SELECT count(*) FROM fresh_wasix_indexed_insert; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-point-loop.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-point-loop.sql new file mode 100644 index 00000000..b46211c8 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-point-loop.sql @@ -0,0 +1,47 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\if :{?transaction_rows} +\else +\set transaction_rows :perf_rows +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_indexed_point_loop; +CREATE TABLE fresh_wasix_indexed_point_loop ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); +INSERT INTO fresh_wasix_indexed_point_loop +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; +ANALYZE fresh_wasix_indexed_point_loop; + +CREATE OR REPLACE FUNCTION fresh_wasix_indexed_point_loop_run(loop_count integer, row_count integer) +RETURNS bigint +LANGUAGE plpgsql +AS $$ +DECLARE + i integer; + key integer; + total bigint := 0; + value text; +BEGIN + FOR i IN 1..loop_count LOOP + key := (((i::bigint * 7919) % row_count) + 1)::integer; + SELECT payload INTO value + FROM fresh_wasix_indexed_point_loop + WHERE id = key; + total := total + length(value); + END LOOP; + RETURN total; +END; +$$; + +\timing on +SELECT fresh_wasix_indexed_point_loop_run(:transaction_rows, :perf_rows); +\timing off diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read-hot-measured.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read-hot-measured.sql new file mode 100644 index 00000000..b2e5b435 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read-hot-measured.sql @@ -0,0 +1,33 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\if :{?transaction_rows} +\else +\set transaction_rows :perf_rows +\endif +\pset tuples_only on +\timing on + +SELECT sum(length(t.payload)) +FROM generate_series(1, :transaction_rows) AS g(i) +JOIN LATERAL ( + SELECT payload + FROM fresh_wasix_indexed_read_hot + WHERE id = ((g.i * 7919) % :perf_rows) + 1 +) AS t ON true; + +SELECT sum(t.id) +FROM generate_series(1, (:transaction_rows / 10)) AS g(i) +JOIN LATERAL ( + SELECT id + FROM fresh_wasix_indexed_read_hot + WHERE bucket = ((g.i * 37) % 1000) + LIMIT 1 +) AS t ON true; + +SELECT count(*) +FROM fresh_wasix_indexed_read_hot +WHERE payload >= 'a' AND payload < 'b'; +\timing off diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read-hot-setup.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read-hot-setup.sql new file mode 100644 index 00000000..8d18c37b --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read-hot-setup.sql @@ -0,0 +1,22 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_indexed_read_hot; +CREATE TABLE fresh_wasix_indexed_read_hot ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); +INSERT INTO fresh_wasix_indexed_read_hot +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; +CREATE INDEX fresh_wasix_indexed_read_hot_bucket_idx + ON fresh_wasix_indexed_read_hot (bucket); +CREATE INDEX fresh_wasix_indexed_read_hot_payload_idx + ON fresh_wasix_indexed_read_hot (payload); +ANALYZE fresh_wasix_indexed_read_hot; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read-hot.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read-hot.sql new file mode 100644 index 00000000..708a7676 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read-hot.sql @@ -0,0 +1,49 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\if :{?transaction_rows} +\else +\set transaction_rows :perf_rows +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_indexed_read_hot; +CREATE TABLE fresh_wasix_indexed_read_hot ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); +INSERT INTO fresh_wasix_indexed_read_hot +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; +CREATE INDEX fresh_wasix_indexed_read_hot_bucket_idx + ON fresh_wasix_indexed_read_hot (bucket); +CREATE INDEX fresh_wasix_indexed_read_hot_payload_idx + ON fresh_wasix_indexed_read_hot (payload); +ANALYZE fresh_wasix_indexed_read_hot; + +\timing on +SELECT sum(length(t.payload)) +FROM generate_series(1, :transaction_rows) AS g(i) +JOIN LATERAL ( + SELECT payload + FROM fresh_wasix_indexed_read_hot + WHERE id = ((g.i * 7919) % :perf_rows) + 1 +) AS t ON true; + +SELECT sum(t.id) +FROM generate_series(1, (:transaction_rows / 10)) AS g(i) +JOIN LATERAL ( + SELECT id + FROM fresh_wasix_indexed_read_hot + WHERE bucket = ((g.i * 37) % 1000) + LIMIT 1 +) AS t ON true; + +SELECT count(*) +FROM fresh_wasix_indexed_read_hot +WHERE payload >= 'a' AND payload < 'b'; +\timing off diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read.sql new file mode 100644 index 00000000..87626d63 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-read.sql @@ -0,0 +1,31 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_indexed_read; +CREATE TABLE fresh_wasix_indexed_read ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); +INSERT INTO fresh_wasix_indexed_read +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; +CREATE INDEX fresh_wasix_indexed_read_bucket_idx + ON fresh_wasix_indexed_read (bucket); +CREATE INDEX fresh_wasix_indexed_read_payload_idx + ON fresh_wasix_indexed_read (payload); +ANALYZE fresh_wasix_indexed_read; + +\timing on +SELECT count(*) FROM fresh_wasix_indexed_read +WHERE bucket BETWEEN 100 AND 199; +SELECT payload FROM fresh_wasix_indexed_read +WHERE id = (:perf_rows * 3 / 4); +SELECT count(*) FROM fresh_wasix_indexed_read +WHERE payload >= 'a' AND payload < 'b'; +\timing off diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-update.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-update.sql new file mode 100644 index 00000000..79108ab6 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/indexed-update.sql @@ -0,0 +1,37 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\if :{?update_rows} +\else +\set update_rows :perf_rows +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_indexed_update; +CREATE TABLE fresh_wasix_indexed_update ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); +INSERT INTO fresh_wasix_indexed_update +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; +CREATE INDEX fresh_wasix_indexed_update_bucket_idx + ON fresh_wasix_indexed_update (bucket); +CREATE INDEX fresh_wasix_indexed_update_payload_idx + ON fresh_wasix_indexed_update (payload); +ANALYZE fresh_wasix_indexed_update; + +\timing on +UPDATE fresh_wasix_indexed_update +SET + bucket = bucket + 1, + payload = md5((id + :perf_rows)::text) +WHERE id <= :update_rows; +\timing off + +SELECT count(*) FROM fresh_wasix_indexed_update +WHERE id <= :update_rows AND bucket > 0; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/md5-scan.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/md5-scan.sql new file mode 100644 index 00000000..467fb660 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/md5-scan.sql @@ -0,0 +1,12 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\pset tuples_only on +\timing off + +\timing on +SELECT count(md5(i::text)) +FROM generate_series(1, :perf_rows) AS i; +\timing off diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/single-transaction-insert.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/single-transaction-insert.sql new file mode 100644 index 00000000..04d000bf --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/single-transaction-insert.sql @@ -0,0 +1,24 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_tx_insert; +CREATE TABLE fresh_wasix_tx_insert ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); + +\timing on +BEGIN; +INSERT INTO fresh_wasix_tx_insert +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; +COMMIT; +\timing off + +SELECT count(*) FROM fresh_wasix_tx_insert; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/transaction-update-batches.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/transaction-update-batches.sql new file mode 100644 index 00000000..d99c23ec --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/transaction-update-batches.sql @@ -0,0 +1,44 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\if :{?transaction_rows} +\else +\set transaction_rows :perf_rows +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_tx_update; +CREATE TABLE fresh_wasix_tx_update ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); +INSERT INTO fresh_wasix_tx_update +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; +CREATE INDEX fresh_wasix_tx_update_bucket_idx + ON fresh_wasix_tx_update (bucket); +ANALYZE fresh_wasix_tx_update; + +\timing on +BEGIN; +UPDATE fresh_wasix_tx_update +SET bucket = bucket + 10 +WHERE id BETWEEN 1 AND (:transaction_rows / 4); +UPDATE fresh_wasix_tx_update +SET bucket = bucket + 10 +WHERE id BETWEEN (:transaction_rows / 4 + 1) AND (:transaction_rows / 2); +UPDATE fresh_wasix_tx_update +SET bucket = bucket + 10 +WHERE id BETWEEN (:transaction_rows / 2 + 1) AND (:transaction_rows * 3 / 4); +UPDATE fresh_wasix_tx_update +SET bucket = bucket + 10 +WHERE id BETWEEN (:transaction_rows * 3 / 4 + 1) AND :transaction_rows; +COMMIT; +\timing off + +SELECT count(*) FROM fresh_wasix_tx_update +WHERE id <= :transaction_rows AND bucket >= 10; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/unlogged-bulk-insert.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/unlogged-bulk-insert.sql new file mode 100644 index 00000000..5b8ba8b7 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/unlogged-bulk-insert.sql @@ -0,0 +1,22 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_unlogged_bulk_insert; +CREATE UNLOGGED TABLE fresh_wasix_unlogged_bulk_insert ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); + +\timing on +INSERT INTO fresh_wasix_unlogged_bulk_insert +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; +\timing off + +SELECT count(*) FROM fresh_wasix_unlogged_bulk_insert; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/unlogged-constant-insert-nocount.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/unlogged-constant-insert-nocount.sql new file mode 100644 index 00000000..0dd4ce33 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/unlogged-constant-insert-nocount.sql @@ -0,0 +1,20 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_unlogged_constant_insert_nocount; +CREATE UNLOGGED TABLE fresh_wasix_unlogged_constant_insert_nocount ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); + +\timing on +INSERT INTO fresh_wasix_unlogged_constant_insert_nocount +SELECT i, i % 1000, 'payload' +FROM generate_series(1, :perf_rows) AS i; +\timing off diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/unlogged-constant-insert.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/unlogged-constant-insert.sql new file mode 100644 index 00000000..248f2b52 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/unlogged-constant-insert.sql @@ -0,0 +1,22 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\pset tuples_only on +\timing off + +DROP TABLE IF EXISTS fresh_wasix_unlogged_constant_insert; +CREATE UNLOGGED TABLE fresh_wasix_unlogged_constant_insert ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); + +\timing on +INSERT INTO fresh_wasix_unlogged_constant_insert +SELECT i, i % 1000, 'payload' +FROM generate_series(1, :perf_rows) AS i; +\timing off + +SELECT count(*) FROM fresh_wasix_unlogged_constant_insert; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/wal-insert-stats.sql b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/wal-insert-stats.sql new file mode 100644 index 00000000..7046b201 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bench/sql/query-perf/wal-insert-stats.sql @@ -0,0 +1,50 @@ +\set ON_ERROR_STOP on +\if :{?perf_rows} +\else +\set perf_rows 100000 +\endif +\pset tuples_only on +\timing off + +SELECT pg_stat_reset_shared('wal'); +SELECT pg_stat_reset_shared('io'); + +DROP TABLE IF EXISTS fresh_wasix_wal_insert_stats; +CREATE TABLE fresh_wasix_wal_insert_stats ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); + +\timing on +BEGIN; +INSERT INTO fresh_wasix_wal_insert_stats +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :perf_rows) AS i; +COMMIT; +\timing off + +SELECT pg_stat_force_next_flush(); + +SELECT + wal_records, + wal_fpi, + wal_bytes, + wal_buffers_full, + stats_reset +FROM pg_stat_wal; + +SELECT + backend_type, + object, + context, + writes, + write_bytes, + write_time, + fsyncs, + fsync_time +FROM pg_stat_io +WHERE writes <> 0 + OR write_bytes <> 0 + OR fsyncs <> 0 +ORDER BY object, backend_type, context; diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/annotate-wasmer-perfmap.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/annotate-wasmer-perfmap.sh new file mode 100755 index 00000000..e69404f0 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/annotate-wasmer-perfmap.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [ "$#" -ne 3 ]; then + cat >&2 <) { + chomp $line; + if ($line =~ /^\s*(\d+):(.*)$/) { + my ($idx, $name) = ($1 + 0, $2); + $name =~ s/^\s+|\s+$//g; + next if $name eq ''; + $index_names{$idx} = $name; + $has_explicit_indexes = 1; + next; + } + if ($line =~ /^\s*(.*?)\s+:\s+[0-9a-fA-F]+\s*$/) { + my $name = $1; + $name =~ s/^\s+|\s+$//g; + next if $name eq ''; + push @names, $name; + } +} +close $nm_fh; +die "no function names parsed from $name_map\n" + unless @names || %index_names; + +open my $perf_fh, '<', $perf_map or die "open $perf_map: $!"; +my @lines = <$perf_fh>; +close $perf_fh; + +my %modules; +for my $line (@lines) { + next unless $line =~ /^\S+\s+\S+\s+(\S+?::function_(\d+))(?:\+\S+)?\s*$/; + my ($symbol, $idx) = ($1, $2 + 0); + my ($module) = $symbol =~ /^(.*?)::function_\d+$/; + my $m = ($modules{$module} ||= { count => 0, min => $idx, max => $idx }); + $m->{count}++; + $m->{min} = $idx if $idx < $m->{min}; + $m->{max} = $idx if $idx > $m->{max}; +} + +my $target_module; +if (!$has_explicit_indexes) { + for my $module (sort keys %modules) { + my $m = $modules{$module}; + my $span = $m->{max} - $m->{min} + 1; + if ($m->{count} == @names && $span == @names) { + $target_module = $module; + last; + } + } +} + +if (!defined $target_module) { + my @candidates = sort { + $modules{$b}->{count} <=> $modules{$a}->{count} + || $modules{$a}->{min} <=> $modules{$b}->{min} + } grep { + $has_explicit_indexes + || ($modules{$_}->{count} <= @names + && ($modules{$_}->{max} - $modules{$_}->{min} + 1) <= @names) + } keys %modules; + $target_module = $candidates[0] if @candidates; +} +die "could not match $name_map to any fallback module in $perf_map\n" + unless defined $target_module; + +my $base = $modules{$target_module}->{min}; +open my $out_fh, '>', $output or die "open $output: $!"; +my $rewritten = 0; +for my $line (@lines) { + if ($line =~ /^(\S+\s+\S+\s+)\Q$target_module\E::function_(\d+)(\s*)$/) { + my ($prefix, $idx, $suffix) = ($1, $2 + 0, $3); + my $name; + if ($has_explicit_indexes) { + $name = $index_names{$idx}; + } else { + my $local = $idx - $base; + $name = $names[$local] if $local >= 0 && $local < @names; + } + if (defined $name) { + $name =~ s/\s+/_/g; + print {$out_fh} $prefix, $target_module, "::", $name, $suffix, "\n"; + $rewritten++; + next; + } + } + print {$out_fh} $line; +} +close $out_fh; + +print STDERR "annotated module $target_module from function_$base with $rewritten names\n"; +PERL diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/apply-wasix-core-overlay.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/apply-wasix-core-overlay.sh new file mode 100755 index 00000000..176f28fc --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/apply-wasix-core-overlay.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +fresh_ensure_dirs +fresh_require_command git + +lock_dir="$FRESH_WORK_ROOT/.overlay.lock" +lock_waits=0 +until mkdir "$lock_dir" 2>/dev/null; do + lock_waits=$((lock_waits + 1)) + if [ "$lock_waits" -gt 300 ]; then + echo "timed out waiting for overlay lock: $lock_dir" >&2 + exit 2 + fi + sleep 0.2 +done +trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT + +"$FRESH_ROOT/bin/prepare-baseline.sh" >/dev/null + +baseline_head="$(git -C "$BASELINE_DIR" rev-parse HEAD)" +overlay_digest="$(fresh_overlay_digest)" +signature="$baseline_head:$overlay_digest" +signature_file="$WASIX_SRC_DIR/.fresh-wasix-core-signature" + +if [ -f "$signature_file" ] && [ "$(cat "$signature_file")" = "$signature" ]; then + printf 'WASIX core worktree already up to date at %s\n' "$WASIX_SRC_DIR" + exit 0 +fi + +if [ -e "$WASIX_SRC_DIR/.git" ] || [ -f "$WASIX_SRC_DIR/.git" ]; then + git -C "$BASELINE_DIR" worktree remove --force "$WASIX_SRC_DIR" >/dev/null 2>&1 || rm -rf "$WASIX_SRC_DIR" +else + rm -rf "$WASIX_SRC_DIR" +fi + +git -C "$BASELINE_DIR" worktree prune +git -C "$BASELINE_DIR" worktree add --detach "$WASIX_SRC_DIR" "$baseline_head" + +cp -R "$FRESH_ROOT/postgres/overlays/wasix-core/." "$WASIX_SRC_DIR/" + +patches_dir="$FRESH_ROOT/postgres/patches" +series_file="$patches_dir/series" +if [ ! -f "$series_file" ]; then + echo "missing PostgreSQL patch series: $series_file" >&2 + exit 2 +fi + +while IFS= read -r patch_name || [ -n "$patch_name" ]; do + case "$patch_name" in + ''|'#'*) continue ;; + /*|*../*) + echo "unsafe PostgreSQL patch entry: $patch_name" >&2 + exit 2 + ;; + esac + patch="$patches_dir/$patch_name" + if [ ! -f "$patch" ]; then + echo "missing PostgreSQL patch from series: $patch" >&2 + exit 2 + fi + git -C "$WASIX_SRC_DIR" apply --whitespace=nowarn "$patch" +done <"$series_file" + +printf '%s' "$signature" >"$signature_file" + +report="$REPORT_DIR/wasix-core-overlay.md" +fresh_write_report_header "$report" "WASIX Core Overlay" +{ + printf '## Result\n\n' + printf -- '- Worktree: `%s`\n' "$WASIX_SRC_DIR" + printf -- '- Baseline commit: `%s`\n' "$baseline_head" + printf -- '- Overlay digest: `%s`\n\n' "$overlay_digest" + printf '## Patch Discipline\n\n' + printf -- '- Production patch source is clean PostgreSQL `%s` plus this overlay.\n' "$POSTGRES_TAG" + printf -- '- The overlay adds a WASIX configure template/header, narrow process/shared-memory port files, and small patch files for DSM, dynamic loading, and static libpq encoding linkage.\n' + printf -- '- Single-user shims, loop rewrites, fake sockets, fake longjmp, fake poll, fake shared memory, disabled largefile, and disabled spinlocks are intentionally absent.\n' +} >>"$report" + +printf 'applied WASIX core overlay at %s\n' "$WASIX_SRC_DIR" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-instrumentation-policy.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-instrumentation-policy.test.sh new file mode 100755 index 00000000..139daa67 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-instrumentation-policy.test.sh @@ -0,0 +1,233 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +bench="$root/bin/bench-wasix-concurrent-query-suite.sh" +tmp="$(mktemp -d)" +trap 'rm -rf -- "$tmp"' EXIT + +expect_policy_failure() { + local label="$1" + shift + set +e + "$@" >"$tmp/$label.log" 2>&1 + local status=$? + set -e + if [ "$status" -ne 2 ]; then + printf '%s: expected policy exit 2, got %s\n' "$label" "$status" >&2 + sed -n '1,100p' "$tmp/$label.log" >&2 + exit 1 + fi +} + +expect_policy_failure timed-option "$bench" --wasix-wait-dump-interval-ms 100 +grep -Fq 'untimed lifecycle diagnostic only' "$tmp/timed-option.log" +expect_policy_failure timed-max-option "$bench" --wasix-wait-dump-max-per-wait 2 +expect_policy_failure timed-verbose-option "$bench" --wasix-wait-dump-verbose +expect_policy_failure ambient-runtime-interval \ + env WASIX_WAIT_DUMP_INTERVAL_MS=100 "$bench" +expect_policy_failure ambient-wrapper-interval \ + env WASIX_PERF_WAIT_DUMP_INTERVAL_MS=100 "$bench" +expect_policy_failure ambient-runtime-file \ + env WASIX_WAIT_DUMP_FILE="$tmp/foreign.log" "$bench" +expect_policy_failure zero-write-without-sealed \ + "$bench" --target wasix --require-zero-write-aot --resource-detail off +grep -Fq -- '--require-zero-write-aot requires --sealed-carrier' \ + "$tmp/zero-write-without-sealed.log" +expect_policy_failure lifecycle-owned-path \ + env WASIX_WAIT_DUMP_FENCE_REQUEST_FILE="$tmp/foreign.request" \ + "$bench" --wasix-lifecycle-plateau --resource-detail off +grep -Fq 'owns its wait-dump log, fence-request, and committed-ACK paths' \ + "$tmp/lifecycle-owned-path.log" +expect_policy_failure lifecycle-owned-ack-path \ + env WASIX_WAIT_DUMP_FENCE_ACK_FILE="$tmp/foreign.ack" \ + "$bench" --wasix-lifecycle-plateau --resource-detail off +expect_policy_failure unknown-adaptive-cache-evidence-policy \ + "$bench" --adaptive-cache-evidence-policy almost-constrained +grep -Fq -- '--adaptive-cache-evidence-policy requires portable-correctness-v1 or constrained-linux-wal-action-v1' \ + "$tmp/unknown-adaptive-cache-evidence-policy.log" +expect_policy_failure duplicate-adaptive-cache-evidence-policy \ + "$bench" --adaptive-cache-evidence-policy portable-correctness-v1 \ + --adaptive-cache-evidence-policy portable-correctness-v1 +expect_policy_failure unconstrained-adaptive-cache-evidence \ + "$bench" --adaptive-cache-evidence-policy constrained-linux-wal-action-v1 +grep -Fq 'requires explicit finite cgroup MemoryMax, MemoryHigh, and MemorySwapMax' \ + "$tmp/unconstrained-adaptive-cache-evidence.log" +expect_policy_failure invalid-explicit-guc-name \ + "$bench" --postgres-guc Work_mem=4MB +grep -Fq 'invalid explicit PostgreSQL setting name: Work_mem' \ + "$tmp/invalid-explicit-guc-name.log" +expect_policy_failure empty-explicit-guc-value \ + "$bench" --postgres-guc work_mem= +grep -Fq 'invalid empty/edge-whitespace explicit PostgreSQL value: work_mem' \ + "$tmp/empty-explicit-guc-value.log" +expect_policy_failure duplicate-explicit-guc \ + "$bench" --postgres-guc work_mem=4MB --postgres-guc work_mem=8MB +grep -Fq 'duplicate explicit PostgreSQL setting: work_mem' \ + "$tmp/duplicate-explicit-guc.log" + +sed -n '/^capture_checkpoint_settings()/,/^validate_controlled_checkpoint_settings()/p' \ + "$bench" | sed '$d' >"$tmp/capture-settings.sh" +# shellcheck source=/dev/null +source "$tmp/capture-settings.sh" +captured_settings_sql="" +fresh_run_process_group_timeout() { + local capture_next=0 arg + shift + [ "$1" = -- ] && shift + for arg in "$@"; do + if [ "$capture_next" -eq 1 ]; then + captured_settings_sql="$arg" + capture_next=0 + elif [ "$arg" = -c ]; then + capture_next=1 + fi + done +} +timeout_seconds=1 +NATIVE_INSTALL_DIR="$tmp/native" +explicit_postgres_guc_names=(work_mem shared_buffers) +capture_checkpoint_settings postgres://example "$tmp/settings.tsv" +grep -Fxq $'name\tsetting\tunit\tsource' "$tmp/settings.tsv" +case "$captured_settings_sql" in + *"'work_mem'"*) ;; + *) echo 'effective-settings query omitted explicit work_mem' >&2; exit 1 ;; +esac +[ "$(printf '%s' "$captured_settings_sql" | grep -o "'shared_buffers'" | wc -l)" -eq 1 ] || { + echo 'effective-settings query did not form a set union with baseline names' >&2 + exit 1 +} + +sed -n '/^configure_wasmer_env_command()/,/^prepare_wasix_runtime()/p' "$bench" | + sed '$d' >"$tmp/configure-env.sh" +wait_dump_environment_names=( + WASIX_PERF_WAIT_DUMP_INTERVAL_MS + WASIX_PERF_WAIT_DUMP_FILE + WASIX_PERF_WAIT_DUMP_MAX_PER_WAIT + WASIX_PERF_WAIT_DUMP_VERBOSE + WASIX_WAIT_DUMP_INTERVAL_MS + WASIX_WAIT_DUMP_FILE + WASIX_WAIT_DUMP_MAX_PER_WAIT + WASIX_WAIT_DUMP_VERBOSE + WASIX_WAIT_DUMP_FENCE_REQUEST_FILE + WASIX_WAIT_DUMP_FENCE_ACK_FILE +) +sealed_loader_environment_names=( + OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT + OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE +) +# shellcheck source=/dev/null +source "$tmp/configure-env.sh" +for name in "${wait_dump_environment_names[@]}"; do + export "$name=ambient-value" +done +for name in "${sealed_loader_environment_names[@]}"; do + export "$name=ambient-value" +done +configure_wasmer_env_command +"${wasmer_env_command[@]}" >"$tmp/sanitized.env" +if grep -Eq '^WASIX_(PERF_)?WAIT_DUMP_' "$tmp/sanitized.env"; then + echo 'Wasmer child environment retained wait-dump instrumentation variables' >&2 + exit 1 +fi +if grep -Eq '^OLIPHAUNT_WASIX_(REQUIRE_ZERO_WRITE_AOT|SEALED_LOADER_AUDIT_FILE)=' \ + "$tmp/sanitized.env"; then + echo 'Wasmer child environment retained sealed-loader policy variables' >&2 + exit 1 +fi + +nonce=0123456789abcdef0123456789abcdef +tab=$'\t' +sed -n '/^validate_walwriter_stabilization_state_file()/,/^write_lifecycle_fence_request()/p' "$bench" | + sed '$d' >"$tmp/walwriter-stabilization.sh" +# shellcheck source=/dev/null +source "$tmp/walwriter-stabilization.sh" +walwriter_state="$tmp/walwriter-state.tsv" +printf '10\t81920\t1786320000000000\t200\t0/1008000\tt\n' >"$walwriter_state" +validate_walwriter_stabilization_state_file "$walwriter_state" +printf '10\t81920\t1786320000000000\t200\t0/1008000\n' >"$walwriter_state" +if validate_walwriter_stabilization_state_file "$walwriter_state"; then + echo 'WAL-writer state accepted a missing flush-reached field' >&2 + exit 1 +fi +printf '10\t81920\t1786320000000000\t0\t0/1008000\tt\n' >"$walwriter_state" +if validate_walwriter_stabilization_state_file "$walwriter_state"; then + echo 'WAL-writer state accepted a zero wal_writer_delay' >&2 + exit 1 +fi +stabilization_log="$tmp/stabilization.log" +append_walwriter_stabilization_record "$stabilization_log" "$nonce" \ + 10 12 81920 147456 1786320000000000 1786320000000000 \ + 0/1000000 0/1008000 200 25 35 passed 42 +grep -Fxq "wasix-runtime-stabilization-v1${tab}nonce=$nonce${tab}method=pg_log_standby_snapshot${tab}before_writes=10${tab}after_writes=12${tab}before_write_bytes=81920${tab}after_write_bytes=147456${tab}before_stats_reset=1786320000000000${tab}after_stats_reset=1786320000000000${tab}target_lsn=0/1000000${tab}observed_flush_lsn=0/1008000${tab}wal_writer_delay_ms=200${tab}start_mono_ns=25${tab}end_mono_ns=35${tab}status=passed${tab}observer_pid=42" \ + "$stabilization_log" +grep -Fq "SELECT pg_log_standby_snapshot()" "$bench" +grep -Fq 'io.backend_type = '\''walwriter'\''' "$bench" +grep -Fq 'append_lifecycle_phase_marker "$wait_dump_log" "$nonce" cold-readiness' "$bench" +grep -Fq 'append_lifecycle_phase_marker "$wait_dump_log" "$nonce" maintenance-stabilization' "$bench" +grep -Fq 'checkpoint_policy=controlled' "$bench" +grep -Fq 'wasix-runtime-reconnect-churn-v1\tnonce=%s\trequested=%s\tcompleted=%s\tcommand_sha256=%s' "$bench" +grep -Fq 'run_lifecycle_reconnect_churn "$conn" "$reconnect_log" "$wait_dump_log"' "$bench" +grep -Fq 'cache_offer_postgres_adaptive_telemetry="${cache_offer_postgres_telemetry%.json}.adaptive.json"' \ + "$bench" +grep -Fq 'validate-adaptive-file-cache-telemetry.py' "$bench" +grep -Fq -- '--acceptance-policy "$adaptive_cache_evidence_policy"' "$bench" +grep -Fq 'adaptive-cache-evidence-policy.tsv' "$bench" +if grep -Fq 'OLIPHAUNT_WASIX_ADAPTIVE_CACHE_TELEMETRY_FILE' "$bench"; then + echo 'adaptive cache telemetry gained an environment activation surface' >&2 + exit 1 +fi +if grep -Eq 'WASIX_ADAPTIVE_CACHE_(EVIDENCE_)?POLICY' "$bench"; then + echo 'adaptive cache evidence acceptance gained an ambient environment surface' >&2 + exit 1 +fi + +sed -n '/^write_lifecycle_fence_request()/,/^wait_for_lifecycle_fence_ack()/p' "$bench" | + sed '$d' >"$tmp/fence-request.sh" +# shellcheck source=/dev/null +source "$tmp/fence-request.sh" +request="$tmp/fence.request" +write_lifecycle_fence_request "$request" "$nonce" 1 readiness 42 +[ "$(cat "$request")" = \ + "wasix-runtime-fence-request-v1${tab}nonce=$nonce${tab}request_seq=1${tab}phase=readiness${tab}observer_pid=42" ] || { + echo 'atomic lifecycle fence request did not use the exact schema' >&2 + exit 1 +} +write_lifecycle_fence_request "$request" "$nonce" 2 post-quiescence 42 +grep -Fxq "wasix-runtime-fence-request-v1${tab}nonce=$nonce${tab}request_seq=2${tab}phase=post-quiescence${tab}observer_pid=42" \ + "$request" +if find "$tmp" -maxdepth 1 -name 'fence.request.pending.*' -print -quit | grep -q .; then + echo 'atomic lifecycle fence request left a pending file' >&2 + exit 1 +fi + +sed -n '/^wait_for_lifecycle_fence_ack()/,/^run_lifecycle_reconnect_churn()/p' "$bench" | + sed '$d' >"$tmp/fence-ack.sh" +# shellcheck source=/dev/null +source "$tmp/fence-ack.sh" +ack="$tmp/fence.ack" +printf 'wasix-runtime-fence-commit-v1\tnonce=%s\tseq=11\tmono_ns=3200000000\tphase=post-quiescence\tobserver_pid=42\tobserver_tid=7\trequest_seq=2\tfence_end_offset=1234\n' \ + "$nonce" >"$ack" +timeout_seconds=1 +now_ms() { date +%s%3N; } +wait_for_lifecycle_fence_ack "$ack" "$nonce" 2 post-quiescence 42 "$$" +printf 'extra\n' >>"$ack" +timeout_seconds=0 +if wait_for_lifecycle_fence_ack "$ack" "$nonce" 2 post-quiescence 42 "$$" \ + >/dev/null 2>&1; then + echo 'multi-line committed ACK unexpectedly passed exact validation' >&2 + exit 1 +fi + +printf 'stale\n' >"$ack" +wait_for_lifecycle_fence_ack() { + [ ! -e "$1" ] && [ -s "$request" ] +} +request_lifecycle_fence "$request" "$ack" "$nonce" 2 post-quiescence 42 "$$" +[ ! -e "$ack" ] || { + echo 'request publication retained a stale committed ACK' >&2 + exit 1 +} + +printf 'passed: instrumentation isolation, exact adaptive evidence policy, maintenance stabilization, bound reconnect churn, and committed fence protocol\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-memory-budget-options.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-memory-budget-options.test.sh new file mode 100755 index 00000000..2e4f358e --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-memory-budget-options.test.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +bench="$root/bin/bench-wasix-concurrent-query-suite.sh" +tmp="$(mktemp -d)" +trap 'rm -rf -- "$tmp"' EXIT + +expect_usage_failure() { + local label="$1" + shift + set +e + "$bench" "$@" >"$tmp/$label.log" 2>&1 + local actual=$? + set -e + if [ "$actual" -ne 2 ]; then + printf '%s: expected exit 2, got %s\n' "$label" "$actual" >&2 + sed -n '1,80p' "$tmp/$label.log" >&2 + exit 1 + fi +} + +expect_usage_failure invalid-kib --max-peak-pss-kib 1.5 +expect_usage_failure disabled-sampler \ + --resource-detail off --max-peak-pss-kib 1 +expect_usage_failure missing-cgroup --max-psi-some-stall-fraction 0.1 +expect_usage_failure lifecycle-conflict \ + --wasix-lifecycle-plateau --max-peak-pss-kib 1 + +mkdir -p "$tmp/fake/bin" "$tmp/report" +printf '%s\n' '#!/usr/bin/env bash' \ + 'set -euo pipefail' \ + 'printf "%s\n" "$@" >"$CAPTURED_ARGS"' \ + 'output=""' \ + 'while [ "$#" -gt 0 ]; do' \ + ' if [ "$1" = --output ]; then shift; output="$1"; fi' \ + ' shift' \ + 'done' \ + 'printf "target\\tstatus\\tdetail\\tsamples\\nwasix\\tpassed\\tfixture\\t2\\n" >"$output"' \ + >"$tmp/fake/bin/validate-memory-evidence.sh" +chmod +x "$tmp/fake/bin/validate-memory-evidence.sh" + +sed -n '/^validate_target_memory_evidence()/,/^run_target()/p' "$bench" | + sed '$d' >"$tmp/validator-function.sh" +# shellcheck source=/dev/null +source "$tmp/validator-function.sh" + +FRESH_ROOT="$tmp/fake" +fresh_wasmer_bin_hash() { + sha256sum "$1" | awk '{ print $1 }' +} +memory_budget_identity="$(printf 'fixture memory budget\n' | sha256sum | awk '{ print $1 }')" +execution_identity_sha256="$(printf 'fixture execution identity\n' | sha256sum | awk '{ print $1 }')" +resource_detail=full +resource_sample_interval=0.1 +cgroup_memory_max=256M +cgroup_memory_high=224M +cgroup_swap_max=0 +max_peak_pss_kib=163840 +max_peak_pss_anon_kib=98304 +max_peak_page_table_kib=2048 +max_cgroup_high_events_delta=4096 +max_psi_some_stall_fraction=0.015 +max_psi_full_stall_fraction=0.010 +workloads=(indexed-read mixed-write indexed-update indexed-insert) +memory_evidence_tsv="$tmp/report/aggregate.tsv" +CAPTURED_ARGS="$tmp/forwarded.args" +export CAPTURED_ARGS +printf 'fixture\n' >"$tmp/resource-samples.tsv" + +validate_target_memory_evidence wasix "$tmp/report" "$tmp/resource-samples.tsv" + +assert_pair() { + local option="$1" + local expected="$2" + awk -v option="$option" -v expected="$expected" ' + previous == option && $0 == expected { found = 1 } + { previous = $0 } + END { exit(found ? 0 : 1) } + ' "$CAPTURED_ARGS" || { + printf 'missing forwarded pair: %s %s\n' "$option" "$expected" >&2 + exit 1 + } +} + +assert_pair --max-peak-pss-kib 163840 +assert_pair --max-peak-pss-anon-kib 98304 +assert_pair --max-peak-page-table-kib 2048 +assert_pair --max-cgroup-high-events-delta 4096 +assert_pair --max-psi-some-stall-fraction 0.015 +assert_pair --max-psi-full-stall-fraction 0.010 +awk -F '\t' '$1 == "wasix" && $2 == "passed" { found = 1 } END { exit(found ? 0 : 1) }' \ + "$memory_evidence_tsv" +awk -F '\t' -v budget="$memory_budget_identity" -v execution="$execution_identity_sha256" ' + NR == 1 { + exit !($1 == "schema_version" && $5 == "resource_samples_sha256" && + $6 == "memory_budget_sha256" && $7 == "execution_identity_sha256" && + $8 == "validator_sha256" && $9 == "memory_evidence_sha256") + } + NR == 2 { + exit !($1 == "oliphaunt.wasix-postmaster.memory-validation.v1" && + $2 == "wasix" && $3 == "passed" && $6 == budget && $7 == execution) + } +' "$tmp/report/memory-validation-receipt.tsv" + +printf 'passed: benchmark memory budget parsing and forwarding\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-pinned-warm-matrix.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-pinned-warm-matrix.sh new file mode 100755 index 00000000..c31213b4 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-pinned-warm-matrix.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +usage() { + cat <<'USAGE' +Usage: bench-pinned-warm-matrix.sh --pin NAME_OR_PATH [bench args...] + +Run the query performance matrix against a pinned runtime bundle. The wrapper +sources the pin's env.sh and forces --skip-build --skip-precompile so accepted +PostgreSQL and Wasmer compiled artifacts are not rebuilt after experiments. +USAGE +} + +pin="" +forwarded=() +profile_explicit=0 + +while [ "$#" -gt 0 ]; do + case "$1" in + --pin) + shift + if [ "$#" -eq 0 ]; then + echo "--pin requires a name or path" >&2 + exit 2 + fi + pin="$1" + ;; + --profile|--profiles) + opt="$1" + profile_explicit=1 + forwarded+=("$opt") + shift + if [ "$#" -eq 0 ]; then + echo "$opt requires a value" >&2 + exit 2 + fi + forwarded+=("$1") + ;; + -h|--help) + usage + exit 0 + ;; + *) + forwarded+=("$1") + ;; + esac + shift +done + +if [ -z "$pin" ]; then + echo "--pin is required" >&2 + usage >&2 + exit 2 +fi + +case "$pin" in + /*|.*/*|*/*) pin_root="$pin" ;; + *) pin_root="$FRESH_WORK_ROOT/tools/pinned-runtimes/$pin" ;; +esac +if [ ! -f "$pin_root/env.sh" ]; then + printf 'missing pinned runtime env: %s\n' "$pin_root/env.sh" >&2 + exit 2 +fi + +# shellcheck disable=SC1090 +source "$pin_root/env.sh" + +if [ "$profile_explicit" -ne 1 ]; then + forwarded=(--profile "${FRESH_PINNED_WASIX_CORE_PROFILE:-$WASIX_CORE_PROFILE}" "${forwarded[@]}") +fi + +snapshot_tree() { + local root="$1" + local out="$2" + if [ "${FRESH_PINNED_VERIFY_HASH:-0}" = "1" ]; then + ( + cd "$root" + find . -type f | LC_ALL=C sort | while IFS= read -r rel; do + hash="$(shasum -a 256 "$rel" | awk '{print $1}')" + printf '%s\t%s\n' "$hash" "${rel#./}" + done + find . -type l | LC_ALL=C sort | while IFS= read -r rel; do + target="$(readlink "$rel")" + printf 'SYMLINK\t%s\t%s\n' "${rel#./}" "$target" + done + ) >"$out" + else + ( + cd "$root" + perl -MFile::Find -e ' + use strict; + use warnings; + my @rows; + find({ + no_chdir => 1, + wanted => sub { + my $path = $File::Find::name; + return if $path eq "."; + (my $rel = $path) =~ s{^\./}{}; + if (-l $path) { + my $target = readlink($path); + push @rows, join("\t", "L", $rel, $target); + return; + } + return unless -f $path; + my @s = stat($path); + push @rows, join("\t", "F", $rel, $s[7], $s[9]); + }, + }, "."); + print "$_\n" for sort @rows; + ' + ) >"$out" + fi +} + +verify_unchanged="${FRESH_PINNED_VERIFY_UNCHANGED:-1}" +tmp_dir="" +if [ "$verify_unchanged" != "0" ]; then + tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/fresh-wasix-pin.XXXXXX")" + trap 'rm -rf "$tmp_dir"' EXIT + snapshot_tree "$FRESH_PINNED_WASIX_INSTALL_DIR" "$tmp_dir/install.before" + snapshot_tree "$FRESH_PINNED_WASMER_CACHE_DIR" "$tmp_dir/cache.before" +fi + +set +e +"$FRESH_ROOT/bin/bench-wasix-query-suite.sh" \ + --skip-build \ + --skip-precompile \ + "${forwarded[@]}" +bench_status=$? +set -e + +if [ "$verify_unchanged" != "0" ]; then + snapshot_tree "$FRESH_PINNED_WASIX_INSTALL_DIR" "$tmp_dir/install.after" + snapshot_tree "$FRESH_PINNED_WASMER_CACHE_DIR" "$tmp_dir/cache.after" + if ! diff -u "$tmp_dir/install.before" "$tmp_dir/install.after" >"$tmp_dir/install.diff"; then + echo "pinned WASIX install changed during benchmark: $FRESH_PINNED_WASIX_INSTALL_DIR" >&2 + sed -n '1,80p' "$tmp_dir/install.diff" >&2 + exit 1 + fi + if ! diff -u "$tmp_dir/cache.before" "$tmp_dir/cache.after" >"$tmp_dir/cache.diff"; then + echo "pinned Wasmer cache changed during benchmark: $FRESH_PINNED_WASMER_CACHE_DIR" >&2 + sed -n '1,80p' "$tmp_dir/cache.diff" >&2 + exit 1 + fi +fi + +exit "$bench_status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-shared-memory-provider-options.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-shared-memory-provider-options.test.sh new file mode 100755 index 00000000..f4a8a9d2 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-shared-memory-provider-options.test.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# The lifecycle registry is extracted and sourced below, so ShellCheck cannot +# statically see either the FRESH_ROOT consumer or the sourced array assignment. + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +bench="$root/bin/bench-wasix-concurrent-query-suite.sh" +helper="$root/lib/shared_memory_provider.py" +tmp="$(mktemp -d "${TMPDIR:-/tmp}/oliphaunt-bench-shm-provider.XXXXXX")" +trap 'rm -rf -- "$tmp"' EXIT + +expect_usage_failure() { + local label="$1" + shift + set +e + "$bench" "$@" >"$tmp/$label.log" 2>&1 + local status=$? + set -e + if [ "$status" -ne 2 ]; then + printf '%s: expected usage exit 2, got %s\n' "$label" "$status" >&2 + sed -n '1,100p' "$tmp/$label.log" >&2 + exit 1 + fi +} + +help_output="$("$bench" --help)" +grep -Fq -- '--shared-memory-provider ID' <<<"$help_output" +grep -Fq 'portable-file-v1.' <<<"$help_output" +expect_usage_failure missing-value --shared-memory-provider +expect_usage_failure unknown-provider --shared-memory-provider almost-tmpfs +grep -Fq 'requires portable-file-v1 or linux-tmpfs-v1' \ + "$tmp/unknown-provider.log" +expect_usage_failure duplicate-provider \ + --shared-memory-provider portable-file-v1 \ + --shared-memory-provider portable-file-v1 +expect_usage_failure native-only-provider \ + --target native --shared-memory-provider portable-file-v1 +grep -Fq 'requires the wasix target' "$tmp/native-only-provider.log" + +grep -Fxq 'shared_memory_provider=portable-file-v1' "$bench" +if grep -Eq 'WASIX_SHARED_MEMORY_PROVIDER|OLIPHAUNT_WASIX_SHARED_MEMORY_PROVIDER' \ + "$bench"; then + echo 'shared-memory provider gained an ambient activation surface' >&2 + exit 1 +fi +grep -Fq 'capture-objects' "$bench" +grep -Fq 'assert-empty' "$bench" +grep -Fq 'register_external_shared_memory_provider' "$bench" +grep -Fq 'release_external_shared_memory_providers exit-drain' "$bench" + +if [ "$(uname -s)" = Linux ] && [ -d /dev/shm ] && [ -w /dev/shm ] && + python3 - "$helper" <<'PY' +import importlib.util +from pathlib import Path +import sys + +spec = importlib.util.spec_from_file_location("provider", sys.argv[1]) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +raise SystemExit(0 if module._filesystem_evidence(Path("/dev/shm"))["filesystem_type"] == "tmpfs" else 1) +PY +then + mkdir -p "$tmp/report" + evidence="$tmp/report/provider.json" + prepare_identity="$( + python3 "$helper" prepare --provider linux-tmpfs-v1 \ + --evidence "$evidence" --measurement-id bench-exit-test --target wasix \ + --output-format path-sha256-tsv + )" + IFS=$'\t' read -r provider_root evidence_sha256 prepare_extra \ + <<<"$prepare_identity" + [ -n "$provider_root" ] + [ -z "$prepare_extra" ] + [[ "$evidence_sha256" =~ ^[0-9a-f]{64}$ ]] + [ "$evidence_sha256" = "$(sha256sum "$evidence" | awk '{ print $1 }')" ] + cleanup_evidence="$tmp/report/cleanup.json" + exit_objects="$tmp/report/exit-objects.json" + exit_release="$tmp/report/exit-release.json" + + sed -n '/^active_shared_memory_records=()/,/^register_background_pid()/p' \ + "$bench" | sed '$d' >"$tmp/provider-functions.sh" + # shellcheck source=/dev/null + source "$tmp/provider-functions.sh" + # shellcheck disable=SC2034 + FRESH_ROOT="$root" + if register_external_shared_memory_provider one two three four five six \ + 2>"$tmp/external-arity.log"; then + echo 'external provider registry accepted the wrong field count' >&2 + exit 1 + fi + if register_pending_external_shared_memory_provider one two three four \ + 2>"$tmp/pending-arity.log"; then + echo 'pending provider registry accepted the wrong field count' >&2 + exit 1 + fi + grep -Fq 'requires seven fields' "$tmp/external-arity.log" + grep -Fq 'requires five fields' "$tmp/pending-arity.log" + register_external_shared_memory_provider linux-tmpfs-v1 "$provider_root" \ + "$evidence" "$evidence_sha256" "$cleanup_evidence" "$exit_objects" \ + "$exit_release" + + : >"$provider_root/postgresql-wasix-00000001-00000002" + if release_external_shared_memory_providers bench-early-return \ + >"$tmp/nonempty.log" 2>&1; then + echo 'benchmark cleanup removed a nonempty provider root' >&2 + exit 1 + fi + [ -d "$provider_root" ] + [ -s "$exit_objects" ] + rm -f -- "$provider_root/postgresql-wasix-00000001-00000002" + release_external_shared_memory_providers bench-early-return + [ ! -e "$provider_root" ] + [ -s "$cleanup_evidence" ] + # shellcheck disable=SC2154 + [ "${#active_shared_memory_records[@]}" -eq 0 ] + + pending_evidence="$tmp/report/pending-provider.json" + pending_identity="$( + python3 "$helper" prepare --provider linux-tmpfs-v1 \ + --evidence "$pending_evidence" --measurement-id bench-pending-test \ + --target wasix --output-format path-sha256-tsv + )" + IFS=$'\t' read -r pending_root pending_sha256 pending_extra \ + <<<"$pending_identity" + [ -n "$pending_root" ] + [ -z "$pending_extra" ] + [[ "$pending_sha256" =~ ^[0-9a-f]{64}$ ]] + register_pending_external_shared_memory_provider linux-tmpfs-v1 \ + "$pending_evidence" "$tmp/report/pending-cleanup.json" \ + "$tmp/report/pending-exit-objects.json" \ + "$tmp/report/pending-exit-release.json" + release_external_shared_memory_providers bench-pending-adoption + [ ! -e "$pending_root" ] + # shellcheck disable=SC2154 + [ "${#pending_shared_memory_records[@]}" -eq 0 ] + + missing_evidence="$tmp/report/missing-provider.json" + missing_identity="$( + python3 "$helper" prepare --provider linux-tmpfs-v1 \ + --evidence "$missing_evidence" --measurement-id bench-missing-test \ + --target wasix --output-format path-sha256-tsv + )" + IFS=$'\t' read -r missing_root missing_sha256 missing_extra \ + <<<"$missing_identity" + [ -n "$missing_root" ] + [ -z "$missing_extra" ] + python3 "$helper" cleanup --provider linux-tmpfs-v1 \ + --root "$missing_root" --evidence "$missing_evidence" \ + --evidence-sha256 "$missing_sha256" \ + --cleanup-evidence "$tmp/report/missing-prior-cleanup.json" \ + --reason test-fixture-removal + register_pending_external_shared_memory_provider linux-tmpfs-v1 \ + "$missing_evidence" "$tmp/report/missing-cleanup.json" \ + "$tmp/report/missing-exit-objects.json" \ + "$tmp/report/missing-exit-release.json" + if recover_pending_external_shared_memory_providers \ + >"$tmp/missing-root.log" 2>&1; then + echo 'pending provider recovery accepted a vanished evidenced root' >&2 + exit 1 + fi + grep -Fq 'root disappeared before adoption' "$tmp/missing-root.log" + # shellcheck disable=SC2154 + [ "${#pending_shared_memory_records[@]}" -eq 1 ] + unregister_pending_external_shared_memory_provider "$missing_evidence" +fi + +printf 'passed: explicit shared-memory provider parsing, evidence, and drained exact cleanup\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-wasix-concurrent-query-suite.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-wasix-concurrent-query-suite.sh new file mode 100755 index 00000000..05ba5eb6 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-wasix-concurrent-query-suite.sh @@ -0,0 +1,8104 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" +source "$FRESH_ROOT/lib/process-supervision.sh" +source "$FRESH_ROOT/lib/server-lifecycle.sh" +source "$FRESH_ROOT/lib/sealed-carrier.sh" +source "$FRESH_ROOT/lib/host-fd-telemetry.sh" +source "$FRESH_ROOT/lib/postgres-profiles.sh" +source "$FRESH_ROOT/lib/qualification-identities.sh" + +usage() { + cat <<'USAGE' +Usage: bench-wasix-concurrent-query-suite.sh [options] + +Runs native PostgreSQL and WASIX PostgreSQL under the same concurrent client +fanout, then records bulk-batch wall throughput, summed psql-timed statement +duration, verification counts, and runtime epoll interruption counts. Every +client connects before the timed start gate and remains connected through an +untimed drain gate, so the fanout resource phase has a stable backend +population. These batch fields exclude connection/backend launch and teardown; +they are not per-query latency. + +The optional native-libpq latency lane records raw CLOCK_MONOTONIC durations +for persistent SELECT 1 calls and, separately, complete +PQconnectdb -> SELECT 1 -> PQfinish reconnect/backend-launch operations. + +Options: + --connections N Concurrent psql clients. Default: 4. + --iterations N Operations per client. Default: 1000. + --rows N Seed rows for read/update workloads. Default: 100000. + --workload NAME Workload to run. May repeat. + Names: indexed-read, mixed-write, indexed-update, + indexed-insert. Aliases: read, mwrite, iupdate, + indexed. + --workloads LIST Space-separated workload names. + --target NAME Target to run. May repeat. Names: native, wasix. + --skip-native Only run WASIX. + --skip-wasix Only run native. + --skip-build Require existing installs. + --skip-precompile Reuse current Wasmer cache. + --sealed-carrier DIR Run WASIX from a self-contained, compiler-free sealed + carrier. The carrier must contain bin/wasmer-headless, + bin/{initdb,postgres}, lib/, share/postgresql/, aot/, + manifest.json, and wasmer-build.receipt. This mode does + not use a Wasmer filesystem cache or precompilation. + --require-zero-write-aot + Require direct activation from immutable AOT and + executable memory-image inodes. Reflink and streamed + compatibility modes are rejected. The loader emits a + machine audit receipt whose write and sync counters are + validated after shutdown. Requires sealed mode, Linux, + an unprivileged caller, and no CAP_LINUX_IMMUTABLE. + --immutable-carrier-receipt FILE + External receipt created by + deploy-immutable-sealed-carrier.sh. Required with + --require-zero-write-aot and rejected otherwise. The + read-only verifier revalidates the exact full carrier + closure and receipt before any measured process starts. + --immutable-carrier-verification-scope MODE + Immutable verification scope: `full` performs the + campaign-boundary cryptographic pass in this process; + `campaign-fast` performs only receipt-bound inode/+i + checks because an enclosing qualifier owns the full + start/end passes. Default: full. + --timeout SECONDS Absolute initdb/readiness/control/setup/client/verify + timeout. Process cleanup has a separate bounded grace. + Default: 120. + --resource-interval S Process resource sample interval in seconds. Default: 0.5. + --resource-detail MODE + Resource sampler detail: `full` records Linux PSS and + mapping accounting, `light` records only ps process + totals, and `off` disables the background sampler. + Use full for memory attribution and off for + throughput/latency qualification. Latency samples are + rejected unless this is off. Default: full. + --shared-memory-provider ID + Host backing mounted at guest /dev/shm for the WASIX + target. `portable-file-v1` preserves the run-tree file + directory. Explicit `linux-tmpfs-v1` creates a private, + evidence-bound directory on Linux /dev/shm. Default: + portable-file-v1. + --cgroup-memory-max SIZE + Launch only the measured server tree in a transient + systemd user scope with MemoryMax=SIZE. + --cgroup-memory-high SIZE + Set MemoryHigh=SIZE on that server scope. + --cgroup-swap-max SIZE + Set MemorySwapMax=SIZE on that server scope. Defaults + to 0 whenever a cgroup memory control is requested. + --adaptive-cache-evidence-policy POLICY + Adaptive file-cache acceptance policy. The default + `portable-correctness-v1` accepts exact active or + observe-only fallback evidence. Opt-in + `constrained-linux-wal-action-v1` requires a sealed + Linux run with explicit finite MemoryMax/MemoryHigh, + adaptive-active admission, class-6 offers and action, + and zero telemetry/advice errors. + --cold-ownership Run only the Linux sealed-carrier cold-start lane. After + initdb, content-hash and individually evict every + regular carrier/PGDATA page, prove zero residency with + mincore, then immediately launch the postmaster in the + fresh cgroup. Requires full sampling and explicit + MemoryMax/MemoryHigh/MemorySwapMax limits. + --cold-ownership-workloads + Keep explicitly selected WASIX workloads after the cold + first-query capture, proving page-residency/charge and + fanout/retention overlap in one cgroup. Storage-I/O + first-touch is claimed only when io.stat is available. + Requires + --cold-ownership and an explicit --workload(s). + --max-peak-pss-kib N + Fail full-detail evidence if any required fan-out sample + exceeds this aggregate process-tree PSS ceiling. + --max-peak-pss-anon-kib N + Fail if aggregate anonymous PSS exceeds this KiB ceiling. + --max-peak-page-table-kib N + Fail if aggregate page-table memory exceeds this KiB ceiling. + --max-cgroup-high-events-delta N + Fail if summed memory.events high deltas across required + fan-out phases exceed this count. + --max-psi-some-stall-fraction F + Fail if summed cgroup PSI some-stall time divided by + elapsed fan-out time exceeds F in [0,1]. + --max-psi-full-stall-fraction F + Fail if the corresponding cgroup PSI full-stall fraction + exceeds F in [0,1]. Any memory budget requires full + resource detail; cgroup budgets require a dedicated scope. + --quiescence-seconds S + Leave the server idle after readiness and each workload, + retaining resource samples and before/after memory maps. + Default: 0 (disabled). + --checkpoint-policy POLICY + PostgreSQL checkpoint policy. `default` leaves server + defaults intact. `controlled` preserves fsync/WAL + durability, checkpoints synchronously after setup, and + fails if a checkpoint runs during fanout. The lifecycle + lane always owns the controlled policy so timed + maintenance cannot masquerade as reconnect retention. + Default: default. + --runtime-footprint ID + Named PostgreSQL runtime-footprint profile. Supported: + embedded-concurrent. + --durability ID Named PostgreSQL durability profile. Supported: safe. + --libpq-latency-samples N + Enable the true-latency lane with N measured samples + per mode and target. Default: 0 (disabled). + --libpq-latency-warmup N + Warmup samples per latency mode. Warmups are retained + raw but excluded from percentiles. Default: 20. + --libpq-latency-only Run only the two latency modes; do not create or run + bulk workloads. Requires --libpq-latency-samples. + The measured server inherits hard RLIMIT_NOFILE unchanged + and is forced to soft RLIMIT_NOFILE=1024. Linux evidence + also gates quiescent host-FD growth after reconnects. + --pg-wait-sample-interval S + Sample pg_stat_activity wait events during each fanout + through one persistent diagnostic connection. + Default: 0 (disabled). + --wasix-perf-stats Enable WASIX perf-stats counters for the WASIX target. + Requires a Wasmer build with wasmer-wasix/perf-stats. + --wasix-wait-dump-interval-ms MS + Dump compact runtime lifecycle state while waits remain + parked. This perturbs the wait hot path and is valid only + with --wasix-lifecycle-plateau, never a timed workload or + latency lane. Requires a wait-dump capable Wasmer build + but does not require perf counters. Default: 0. + --wasix-wait-dump-max-per-wait N + Maximum wait-registry snapshots per individual parked + wait before logging one suppression marker. Use 0 for + unlimited. Default: 8. + --wasix-wait-dump-verbose + Include per-state futex, shared-mapping, guest-FD, socket, + and epoll inventories in the untimed lifecycle log. + Rejected by every timed workload and latency lane. + --wasix-lifecycle-plateau + Run only the untimed WASIX reconnect lifecycle lane. + It gates exact readiness/post-quiescence plateaus for + process, task, shared-futex registry, mapping, and guest + FD occupancy. Incompatible with latency/workload lanes. + --wasix-lifecycle-reconnects N + Sequential reconnects in the lifecycle lane. Default: 64. + --wasix-lifecycle-window-seconds S + Readiness and post-churn sampling window. Must provide + at least three samples spanning one second. Default: 5. + --wasix-lifecycle-memory-checkpoint-every N + On Linux, capture one host-Wasmer smaps checkpoint after + every N reconnect clients have been reaped and the + explicit settling window has elapsed. Baseline and final + checkpoints are additionally runtime-fenced. Default: 0 + (disabled). + --wasix-lifecycle-memory-quiescence-seconds S + Settling window before each intermediate memory + checkpoint. No smaps reads occur while a reconnect client + is running. Default: 2. + --max-lifecycle-pss-growth-kib N + Maximum full-run, terminal, and late-tail PSS growth in + the checkpoint lane. + --max-lifecycle-pss-anon-growth-kib N + Corresponding Pss_Anon growth ceiling. + --max-lifecycle-heap-growth-kib N + Corresponding [heap] mapping PSS growth ceiling. All + three ceilings are required when checkpoints are enabled. + --max-lifecycle-late-pss-slope-kib-per-1000 N + Maximum late-tail Theil-Sen PSS slope in KiB per 1000 + reconnects. + --max-lifecycle-late-pss-anon-slope-kib-per-1000 N + Corresponding Pss_Anon late-tail slope ceiling. + --max-lifecycle-late-heap-slope-kib-per-1000 N + Corresponding [heap] PSS late-tail slope ceiling. All + three slope ceilings are also required. + --lifecycle-baseline-policy FILE + Checked-in policy below profiles/lifecycle-baselines/. + The default is exploratory/unbounded. A qualifying run + must select a separately frozen qualification-bounded + policy produced from an earlier exploratory run. + --memory-map-snapshots + Capture vmmap/pmap snapshots at readiness and fanout + boundaries. Expensive and off by default. + --sample-seconds S Run sample(1) against the WASIX server during fanout. + Default: 0 (disabled). + --sample-delay S Delay after fanout starts before sample(1). Default: 0.2. + --start-port PORT First PostgreSQL port. Default: PGPORT or 55620. + --label NAME Report/run label. Default: timestamped. + --replace-existing Explicitly replace an existing run/report label. + Without this flag, label collisions fail closed. + --discard-pgdata After a fully successful target run and clean shutdown, + delete only that target's generated PGDATA and provider + backing. External linux-tmpfs-v1 backing is released + after owned processes drain; identity drift or surviving + objects fail closed and retain it for diagnosis. + Failed PGDATA and portable backing are retained; an + empty external tmpfs root is still safely released. + --postgres-guc GUC Extra postmaster -c name=value setting. May repeat. + --wasmer-arg ARG Extra wasmer run argument. May repeat. + -h, --help Show this help. +USAGE +} + +connections="${WASIX_CONCURRENT_CONNECTIONS:-4}" +iterations="${WASIX_CONCURRENT_ITERATIONS:-1000}" +row_count="${WASIX_CONCURRENT_ROWS:-100000}" +timeout_seconds="${WASIX_CONCURRENT_TIMEOUT:-120}" +process_term_grace_ms="${WASIX_PROCESS_TERM_GRACE_MS:-1000}" +process_kill_grace_ms="${WASIX_PROCESS_KILL_GRACE_MS:-3000}" +resource_sample_interval="${WASIX_RESOURCE_SAMPLE_INTERVAL:-0.5}" +resource_detail="${WASIX_RESOURCE_DETAIL:-full}" +resource_detail_explicit=0 +shared_memory_provider=portable-file-v1 +shared_memory_provider_explicit=0 +cgroup_memory_max="${WASIX_CGROUP_MEMORY_MAX:-}" +cgroup_memory_high="${WASIX_CGROUP_MEMORY_HIGH:-}" +cgroup_swap_max="${WASIX_CGROUP_SWAP_MAX:-}" +adaptive_cache_evidence_policy=portable-correctness-v1 +adaptive_cache_evidence_policy_explicit=0 +cold_ownership="${WASIX_COLD_OWNERSHIP:-0}" +cold_ownership_workloads=0 +max_peak_pss_kib="${WASIX_MAX_PEAK_PSS_KIB:-}" +max_peak_pss_anon_kib="${WASIX_MAX_PEAK_PSS_ANON_KIB:-}" +max_peak_page_table_kib="${WASIX_MAX_PEAK_PAGE_TABLE_KIB:-}" +max_cgroup_high_events_delta="${WASIX_MAX_CGROUP_HIGH_EVENTS_DELTA:-}" +max_psi_some_stall_fraction="${WASIX_MAX_PSI_SOME_STALL_FRACTION:-}" +max_psi_full_stall_fraction="${WASIX_MAX_PSI_FULL_STALL_FRACTION:-}" +quiescence_seconds="${WASIX_QUIESCENCE_SECONDS:-0}" +checkpoint_policy="${WASIX_CHECKPOINT_POLICY:-default}" +runtime_footprint="${WASIX_RUNTIME_FOOTPRINT:-}" +durability_profile="${WASIX_DURABILITY_PROFILE:-}" +libpq_latency_samples="${WASIX_LIBPQ_LATENCY_SAMPLES:-0}" +libpq_latency_warmup="${WASIX_LIBPQ_LATENCY_WARMUP:-20}" +libpq_latency_soft_nofile=1024 +libpq_latency_host_fd_allowance="${WASIX_LIBPQ_LATENCY_HOST_FD_ALLOWANCE:-4}" +libpq_latency_only=0 +pg_wait_sample_interval="${WASIX_PG_WAIT_SAMPLE_INTERVAL:-0}" +start_port="${PGPORT:-55620}" +run_label="${WASIX_CONCURRENT_LABEL:-$(date -u +%Y%m%dT%H%M%SZ)}" +replace_existing=0 +discard_pgdata=0 +skip_build=0 +skip_precompile="${WASIX_SKIP_PRECOMPILE:-0}" +skip_precompile_explicit=0 +sealed_carrier="" +sealed_carrier_explicit=0 +require_frozen_measurement_tools="${FRESH_REQUIRE_FROZEN_MEASUREMENT_TOOLS:-0}" +require_zero_write_aot=0 +require_zero_write_aot_explicit=0 +immutable_carrier_receipt="" +immutable_carrier_receipt_explicit=0 +immutable_carrier_verification_scope=full +wasix_perf_stats="${WASIX_PERF_STATS:-0}" +wasix_wait_dump_interval_ms="${WASIX_PERF_WAIT_DUMP_INTERVAL_MS:-0}" +wasix_wait_dump_max_per_wait="${WASIX_WAIT_DUMP_MAX_PER_WAIT:-8}" +wasix_wait_dump_verbose="${WASIX_WAIT_DUMP_VERBOSE:-}" +wasix_lifecycle_plateau="${WASIX_LIFECYCLE_PLATEAU:-0}" +wasix_lifecycle_reconnects="${WASIX_LIFECYCLE_RECONNECTS:-64}" +wasix_lifecycle_window_seconds="${WASIX_LIFECYCLE_WINDOW_SECONDS:-5}" +wasix_lifecycle_memory_checkpoint_every="${WASIX_LIFECYCLE_MEMORY_CHECKPOINT_EVERY:-0}" +wasix_lifecycle_memory_quiescence_seconds="${WASIX_LIFECYCLE_MEMORY_QUIESCENCE_SECONDS:-2}" +max_lifecycle_pss_growth_kib="${WASIX_MAX_LIFECYCLE_PSS_GROWTH_KIB:-}" +max_lifecycle_pss_anon_growth_kib="${WASIX_MAX_LIFECYCLE_PSS_ANON_GROWTH_KIB:-}" +max_lifecycle_heap_growth_kib="${WASIX_MAX_LIFECYCLE_HEAP_GROWTH_KIB:-}" +max_late_lifecycle_pss_slope_kib_per_1000="${WASIX_MAX_LATE_LIFECYCLE_PSS_SLOPE_KIB_PER_1000:-}" +max_late_lifecycle_pss_anon_slope_kib_per_1000="${WASIX_MAX_LATE_LIFECYCLE_PSS_ANON_SLOPE_KIB_PER_1000:-}" +max_late_lifecycle_heap_slope_kib_per_1000="${WASIX_MAX_LATE_LIFECYCLE_HEAP_SLOPE_KIB_PER_1000:-}" +lifecycle_baseline_policy_source="$FRESH_ROOT/profiles/lifecycle-baselines/relative-stabilized-idle-postmaster-candidate-v1.tsv" +lifecycle_baseline_policy_explicit=0 +memory_map_snapshots="${WASIX_MEMORY_MAP_SNAPSHOTS:-0}" +sample_seconds="${WASIX_CONCURRENT_SAMPLE_SECONDS:-0}" +sample_delay="${WASIX_CONCURRENT_SAMPLE_DELAY:-0.2}" +targets=() +workloads=() +workloads_explicit=0 +postgres_gucs=() +wasmer_extra_args=() +wasix_wait_dump_cli_config=0 +wait_dump_environment_names=( + WASIX_PERF_WAIT_DUMP_INTERVAL_MS + WASIX_PERF_WAIT_DUMP_FILE + WASIX_PERF_WAIT_DUMP_MAX_PER_WAIT + WASIX_PERF_WAIT_DUMP_VERBOSE + WASIX_WAIT_DUMP_INTERVAL_MS + WASIX_WAIT_DUMP_FILE + WASIX_WAIT_DUMP_MAX_PER_WAIT + WASIX_WAIT_DUMP_VERBOSE + WASIX_WAIT_DUMP_FENCE_REQUEST_FILE + WASIX_WAIT_DUMP_FENCE_ACK_FILE +) +sealed_loader_environment_names=( + OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT + OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE + OLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE +) +ambient_wait_dump_environment=() +for wait_dump_name in "${wait_dump_environment_names[@]}"; do + if [[ -v $wait_dump_name ]]; then + ambient_wait_dump_environment+=("$wait_dump_name") + fi +done + +default_workloads=( + indexed-read + mixed-write + indexed-update + indexed-insert +) + +while [ "$#" -gt 0 ]; do + case "$1" in + --connections) + shift + [ "$#" -gt 0 ] || { echo "--connections requires a value" >&2; exit 2; } + connections="$1" + ;; + --iterations) + shift + [ "$#" -gt 0 ] || { echo "--iterations requires a value" >&2; exit 2; } + iterations="$1" + ;; + --rows) + shift + [ "$#" -gt 0 ] || { echo "--rows requires a value" >&2; exit 2; } + row_count="$1" + ;; + --workload) + shift + [ "$#" -gt 0 ] || { echo "--workload requires a value" >&2; exit 2; } + workloads+=("$1") + workloads_explicit=1 + ;; + --workloads) + shift + [ "$#" -gt 0 ] || { echo "--workloads requires a value" >&2; exit 2; } + for workload in $1; do + workloads+=("$workload") + done + workloads_explicit=1 + ;; + --target) + shift + [ "$#" -gt 0 ] || { echo "--target requires native or wasix" >&2; exit 2; } + targets+=("$1") + ;; + --skip-native) + targets=(wasix) + ;; + --skip-wasix) + targets=(native) + ;; + --skip-build) + skip_build=1 + ;; + --skip-precompile) + skip_precompile=1 + skip_precompile_explicit=1 + ;; + --sealed-carrier) + shift + [ "$#" -gt 0 ] || { echo "--sealed-carrier requires a directory" >&2; exit 2; } + [ "$sealed_carrier_explicit" -eq 0 ] || { echo "--sealed-carrier may only be specified once" >&2; exit 2; } + sealed_carrier="$1" + sealed_carrier_explicit=1 + ;; + --require-zero-write-aot) + [ "$require_zero_write_aot_explicit" -eq 0 ] || { + echo '--require-zero-write-aot may only be specified once' >&2 + exit 2 + } + require_zero_write_aot=1 + require_zero_write_aot_explicit=1 + ;; + --immutable-carrier-receipt) + shift + [ "$#" -gt 0 ] || { echo '--immutable-carrier-receipt requires a file' >&2; exit 2; } + [ "$immutable_carrier_receipt_explicit" -eq 0 ] || { + echo '--immutable-carrier-receipt may only be specified once' >&2 + exit 2 + } + immutable_carrier_receipt="$1" + immutable_carrier_receipt_explicit=1 + ;; + --immutable-carrier-verification-scope) + shift + [ "$#" -gt 0 ] || { + echo '--immutable-carrier-verification-scope requires full or campaign-fast' >&2 + exit 2 + } + immutable_carrier_verification_scope="$1" + ;; + --timeout) + shift + [ "$#" -gt 0 ] || { echo "--timeout requires a value" >&2; exit 2; } + timeout_seconds="$1" + ;; + --resource-interval) + shift + [ "$#" -gt 0 ] || { echo "--resource-interval requires a value" >&2; exit 2; } + resource_sample_interval="$1" + ;; + --resource-detail) + shift + [ "$#" -gt 0 ] || { echo "--resource-detail requires full, light, or off" >&2; exit 2; } + resource_detail="$1" + resource_detail_explicit=1 + ;; + --shared-memory-provider) + shift + [ "$#" -gt 0 ] || { echo "--shared-memory-provider requires an ID" >&2; exit 2; } + [ "$shared_memory_provider_explicit" -eq 0 ] || { + echo "--shared-memory-provider may only be specified once" >&2 + exit 2 + } + shared_memory_provider="$1" + shared_memory_provider_explicit=1 + ;; + --cgroup-memory-max) + shift + [ "$#" -gt 0 ] || { echo "--cgroup-memory-max requires a size" >&2; exit 2; } + cgroup_memory_max="$1" + ;; + --cgroup-memory-high) + shift + [ "$#" -gt 0 ] || { echo "--cgroup-memory-high requires a size" >&2; exit 2; } + cgroup_memory_high="$1" + ;; + --cgroup-swap-max) + shift + [ "$#" -gt 0 ] || { echo "--cgroup-swap-max requires a size" >&2; exit 2; } + cgroup_swap_max="$1" + ;; + --adaptive-cache-evidence-policy) + shift + [ "$#" -gt 0 ] || { + echo "--adaptive-cache-evidence-policy requires a policy ID" >&2 + exit 2 + } + [ "$adaptive_cache_evidence_policy_explicit" -eq 0 ] || { + echo "--adaptive-cache-evidence-policy may only be specified once" >&2 + exit 2 + } + adaptive_cache_evidence_policy="$1" + adaptive_cache_evidence_policy_explicit=1 + ;; + --cold-ownership) + cold_ownership=1 + ;; + --cold-ownership-workloads) + cold_ownership_workloads=1 + ;; + --max-peak-pss-kib) + shift + [ "$#" -gt 0 ] || { echo "--max-peak-pss-kib requires a value" >&2; exit 2; } + max_peak_pss_kib="$1" + ;; + --max-peak-pss-anon-kib) + shift + [ "$#" -gt 0 ] || { echo "--max-peak-pss-anon-kib requires a value" >&2; exit 2; } + max_peak_pss_anon_kib="$1" + ;; + --max-peak-page-table-kib) + shift + [ "$#" -gt 0 ] || { echo "--max-peak-page-table-kib requires a value" >&2; exit 2; } + max_peak_page_table_kib="$1" + ;; + --max-cgroup-high-events-delta) + shift + [ "$#" -gt 0 ] || { echo "--max-cgroup-high-events-delta requires a value" >&2; exit 2; } + max_cgroup_high_events_delta="$1" + ;; + --max-psi-some-stall-fraction) + shift + [ "$#" -gt 0 ] || { echo "--max-psi-some-stall-fraction requires a value" >&2; exit 2; } + max_psi_some_stall_fraction="$1" + ;; + --max-psi-full-stall-fraction) + shift + [ "$#" -gt 0 ] || { echo "--max-psi-full-stall-fraction requires a value" >&2; exit 2; } + max_psi_full_stall_fraction="$1" + ;; + --quiescence-seconds) + shift + [ "$#" -gt 0 ] || { echo "--quiescence-seconds requires a value" >&2; exit 2; } + quiescence_seconds="$1" + ;; + --checkpoint-policy) + shift + [ "$#" -gt 0 ] || { echo "--checkpoint-policy requires default or controlled" >&2; exit 2; } + checkpoint_policy="$1" + ;; + --runtime-footprint) + shift + [ "$#" -gt 0 ] || { echo "--runtime-footprint requires an ID" >&2; exit 2; } + runtime_footprint="$1" + ;; + --durability) + shift + [ "$#" -gt 0 ] || { echo "--durability requires an ID" >&2; exit 2; } + durability_profile="$1" + ;; + --libpq-latency-samples) + shift + [ "$#" -gt 0 ] || { echo "--libpq-latency-samples requires a value" >&2; exit 2; } + libpq_latency_samples="$1" + ;; + --libpq-latency-warmup) + shift + [ "$#" -gt 0 ] || { echo "--libpq-latency-warmup requires a value" >&2; exit 2; } + libpq_latency_warmup="$1" + ;; + --libpq-latency-only) + libpq_latency_only=1 + ;; + --pg-wait-sample-interval) + shift + [ "$#" -gt 0 ] || { echo "--pg-wait-sample-interval requires a value" >&2; exit 2; } + pg_wait_sample_interval="$1" + ;; + --wasix-perf-stats) + wasix_perf_stats=1 + ;; + --wasix-wait-dump-interval-ms) + shift + [ "$#" -gt 0 ] || { echo "--wasix-wait-dump-interval-ms requires a value" >&2; exit 2; } + wasix_wait_dump_interval_ms="$1" + wasix_wait_dump_cli_config=1 + ;; + --wasix-wait-dump-max-per-wait) + shift + [ "$#" -gt 0 ] || { echo "--wasix-wait-dump-max-per-wait requires a value" >&2; exit 2; } + wasix_wait_dump_max_per_wait="$1" + wasix_wait_dump_cli_config=1 + ;; + --wasix-wait-dump-verbose) + wasix_wait_dump_verbose=1 + wasix_wait_dump_cli_config=1 + ;; + --wasix-lifecycle-plateau) + wasix_lifecycle_plateau=1 + ;; + --wasix-lifecycle-reconnects) + shift + [ "$#" -gt 0 ] || { echo "--wasix-lifecycle-reconnects requires a value" >&2; exit 2; } + wasix_lifecycle_reconnects="$1" + ;; + --wasix-lifecycle-window-seconds) + shift + [ "$#" -gt 0 ] || { echo "--wasix-lifecycle-window-seconds requires a value" >&2; exit 2; } + wasix_lifecycle_window_seconds="$1" + ;; + --wasix-lifecycle-memory-checkpoint-every) + shift + [ "$#" -gt 0 ] || { echo "--wasix-lifecycle-memory-checkpoint-every requires a value" >&2; exit 2; } + wasix_lifecycle_memory_checkpoint_every="$1" + ;; + --wasix-lifecycle-memory-quiescence-seconds) + shift + [ "$#" -gt 0 ] || { echo "--wasix-lifecycle-memory-quiescence-seconds requires a value" >&2; exit 2; } + wasix_lifecycle_memory_quiescence_seconds="$1" + ;; + --max-lifecycle-pss-growth-kib) + shift + [ "$#" -gt 0 ] || { echo "--max-lifecycle-pss-growth-kib requires a value" >&2; exit 2; } + max_lifecycle_pss_growth_kib="$1" + ;; + --max-lifecycle-pss-anon-growth-kib) + shift + [ "$#" -gt 0 ] || { echo "--max-lifecycle-pss-anon-growth-kib requires a value" >&2; exit 2; } + max_lifecycle_pss_anon_growth_kib="$1" + ;; + --max-lifecycle-heap-growth-kib) + shift + [ "$#" -gt 0 ] || { echo "--max-lifecycle-heap-growth-kib requires a value" >&2; exit 2; } + max_lifecycle_heap_growth_kib="$1" + ;; + --max-lifecycle-late-pss-slope-kib-per-1000) + shift + [ "$#" -gt 0 ] || { echo "--max-lifecycle-late-pss-slope-kib-per-1000 requires a value" >&2; exit 2; } + max_late_lifecycle_pss_slope_kib_per_1000="$1" + ;; + --max-lifecycle-late-pss-anon-slope-kib-per-1000) + shift + [ "$#" -gt 0 ] || { echo "--max-lifecycle-late-pss-anon-slope-kib-per-1000 requires a value" >&2; exit 2; } + max_late_lifecycle_pss_anon_slope_kib_per_1000="$1" + ;; + --max-lifecycle-late-heap-slope-kib-per-1000) + shift + [ "$#" -gt 0 ] || { echo "--max-lifecycle-late-heap-slope-kib-per-1000 requires a value" >&2; exit 2; } + max_late_lifecycle_heap_slope_kib_per_1000="$1" + ;; + --lifecycle-baseline-policy) + shift + [ "$#" -gt 0 ] || { echo "--lifecycle-baseline-policy requires a file" >&2; exit 2; } + [ "$lifecycle_baseline_policy_explicit" -eq 0 ] || { + echo "--lifecycle-baseline-policy may only be specified once" >&2 + exit 2 + } + lifecycle_baseline_policy_source="$1" + lifecycle_baseline_policy_explicit=1 + ;; + --memory-map-snapshots|--vmmap-snapshots) + memory_map_snapshots=1 + ;; + --sample-seconds) + shift + [ "$#" -gt 0 ] || { echo "--sample-seconds requires a value" >&2; exit 2; } + sample_seconds="$1" + ;; + --sample-delay) + shift + [ "$#" -gt 0 ] || { echo "--sample-delay requires a value" >&2; exit 2; } + sample_delay="$1" + ;; + --start-port) + shift + [ "$#" -gt 0 ] || { echo "--start-port requires a value" >&2; exit 2; } + start_port="$1" + ;; + --label) + shift + [ "$#" -gt 0 ] || { echo "--label requires a value" >&2; exit 2; } + run_label="$1" + ;; + --replace-existing) + replace_existing=1 + ;; + --discard-pgdata) + discard_pgdata=1 + ;; + --postgres-guc) + shift + [ "$#" -gt 0 ] || { echo "--postgres-guc requires name=value" >&2; exit 2; } + postgres_gucs+=("$1") + ;; + --wasmer-arg) + shift + [ "$#" -gt 0 ] || { echo "--wasmer-arg requires a value" >&2; exit 2; } + wasmer_extra_args+=("$1") + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +is_positive_integer() { + [[ "$1" =~ ^[1-9][0-9]*$ ]] +} + +is_nonnegative_integer() { + [[ "$1" =~ ^[0-9]+$ ]] +} + +is_nonnegative_number() { + [[ "$1" =~ ^([0-9]+([.][0-9]*)?|[.][0-9]+)$ ]] +} + +is_positive_number() { + is_nonnegative_number "$1" && awk -v value="$1" 'BEGIN { exit !((value + 0) > 0) }' +} + +validate_unsigned_budget() { + local option="$1" + local value="$2" + [ -z "$value" ] && return 0 + if ! is_nonnegative_integer "$value" || + ! awk -v value="$value" 'BEGIN { exit !(value <= 9007199254740991) }' + then + printf '%s requires a nonnegative integer in the exact supported range\n' \ + "$option" >&2 + exit 2 + fi +} + +validate_fraction_budget() { + local option="$1" + local value="$2" + [ -z "$value" ] && return 0 + if ! is_nonnegative_number "$value" || + ! awk -v value="$value" 'BEGIN { exit !(value >= 0 && value <= 1) }' + then + printf '%s requires a decimal fraction between 0 and 1 inclusive\n' \ + "$option" >&2 + exit 2 + fi +} + +case "$require_frozen_measurement_tools" in + 0|1) ;; + *) + echo 'FRESH_REQUIRE_FROZEN_MEASUREMENT_TOOLS must be 0 or 1' >&2 + exit 2 + ;; +esac + +is_positive_integer "$connections" || { echo "--connections requires a positive integer" >&2; exit 2; } +is_positive_integer "$iterations" || { echo "--iterations requires a positive integer" >&2; exit 2; } +is_positive_integer "$row_count" || { echo "--rows requires a positive integer" >&2; exit 2; } +is_positive_integer "$timeout_seconds" || { echo "--timeout requires a positive integer" >&2; exit 2; } +case "$process_term_grace_ms:$process_kill_grace_ms" in + *[!0-9:]*|:*) + echo "WASIX_PROCESS_TERM_GRACE_MS and WASIX_PROCESS_KILL_GRACE_MS require nonnegative integer milliseconds" >&2 + exit 2 + ;; +esac +export WASIX_PROCESS_TERM_GRACE_MS="$process_term_grace_ms" +export WASIX_PROCESS_KILL_GRACE_MS="$process_kill_grace_ms" +is_positive_number "$resource_sample_interval" || { echo "--resource-interval requires a positive number" >&2; exit 2; } +is_nonnegative_number "$quiescence_seconds" || { echo "--quiescence-seconds requires a non-negative number" >&2; exit 2; } +case "$resource_detail" in + full|light|off) ;; + *) echo "--resource-detail requires full, light, or off" >&2; exit 2 ;; +esac +case "$shared_memory_provider" in + portable-file-v1|linux-tmpfs-v1) ;; + *) + echo "--shared-memory-provider requires portable-file-v1 or linux-tmpfs-v1" >&2 + exit 2 + ;; +esac +case "$adaptive_cache_evidence_policy" in + portable-correctness-v1|constrained-linux-wal-action-v1) ;; + *) + echo "--adaptive-cache-evidence-policy requires portable-correctness-v1 or constrained-linux-wal-action-v1" >&2 + exit 2 + ;; +esac +validate_cgroup_size() { + [[ "$1" =~ ^[0-9]+([KMGTPE]([i]?B)?)?$ ]] +} +cgroup_size_to_bytes() { + python3 - "$1" <<'PY' +import re +import sys + +match = re.fullmatch(r"([0-9]+)([KMGTPE])?(?:i?B)?", sys.argv[1]) +if match is None: + raise SystemExit(2) +value = int(match.group(1)) +suffix = match.group(2) +if suffix is not None: + value *= 1024 ** ("KMGTPE".index(suffix) + 1) +if value > 2**63 - 1: + raise SystemExit(2) +print(value) +PY +} +for cgroup_size in "$cgroup_memory_max" "$cgroup_memory_high" "$cgroup_swap_max"; do + if [ -n "$cgroup_size" ] && ! validate_cgroup_size "$cgroup_size"; then + printf 'invalid cgroup size: %s\n' "$cgroup_size" >&2 + exit 2 + fi +done +validate_unsigned_budget --max-peak-pss-kib "$max_peak_pss_kib" +validate_unsigned_budget --max-peak-pss-anon-kib "$max_peak_pss_anon_kib" +validate_unsigned_budget --max-peak-page-table-kib "$max_peak_page_table_kib" +validate_unsigned_budget --max-cgroup-high-events-delta \ + "$max_cgroup_high_events_delta" +validate_fraction_budget --max-psi-some-stall-fraction \ + "$max_psi_some_stall_fraction" +validate_fraction_budget --max-psi-full-stall-fraction \ + "$max_psi_full_stall_fraction" +memory_budget_requested=0 +if [ -n "$max_peak_pss_kib$max_peak_pss_anon_kib$max_peak_page_table_kib$max_cgroup_high_events_delta$max_psi_some_stall_fraction$max_psi_full_stall_fraction" ]; then + memory_budget_requested=1 +fi +cgroup_budget_requested=0 +if [ -n "$max_cgroup_high_events_delta$max_psi_some_stall_fraction$max_psi_full_stall_fraction" ]; then + cgroup_budget_requested=1 +fi +if [ "$memory_budget_requested" -eq 1 ] && [ "$resource_detail" != full ]; then + echo "memory performance budgets require --resource-detail full" >&2 + exit 2 +fi +if [ "$cgroup_budget_requested" -eq 1 ] && + [ -z "$cgroup_memory_max$cgroup_memory_high$cgroup_swap_max" ]; then + echo "cgroup event and PSI budgets require a dedicated cgroup memory scope" >&2 + exit 2 +fi +if [ -n "$cgroup_memory_max$cgroup_memory_high$cgroup_swap_max" ]; then + [ "$(uname -s)" = "Linux" ] || { + echo "cgroup memory controls require Linux" >&2 + exit 2 + } + command -v systemd-run >/dev/null 2>&1 || { + echo "cgroup memory controls require systemd-run" >&2 + exit 127 + } + if [ -z "$cgroup_swap_max" ]; then + cgroup_swap_max=0 + fi +fi +cgroup_memory_max_bytes=none +cgroup_memory_high_bytes=none +cgroup_swap_max_bytes=none +if [ -n "$cgroup_memory_max" ]; then + cgroup_memory_max_bytes="$(cgroup_size_to_bytes "$cgroup_memory_max")" || { + echo "could not canonicalize cgroup MemoryMax" >&2 + exit 2 + } +fi +if [ -n "$cgroup_memory_high" ]; then + cgroup_memory_high_bytes="$(cgroup_size_to_bytes "$cgroup_memory_high")" || { + echo "could not canonicalize cgroup MemoryHigh" >&2 + exit 2 + } +fi +if [ -n "$cgroup_swap_max" ]; then + cgroup_swap_max_bytes="$(cgroup_size_to_bytes "$cgroup_swap_max")" || { + echo "could not canonicalize cgroup MemorySwapMax" >&2 + exit 2 + } +fi +if [ "$adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ]; then + [ "$(uname -s)" = Linux ] || { + echo "constrained-linux-wal-action-v1 requires Linux" >&2 + exit 2 + } + [ -n "$cgroup_memory_max" ] && [ -n "$cgroup_memory_high" ] && + [ -n "$cgroup_swap_max" ] || { + echo "constrained-linux-wal-action-v1 requires explicit finite cgroup MemoryMax, MemoryHigh, and MemorySwapMax" >&2 + exit 2 + } + if [[ "$cgroup_memory_max" =~ ^0+([KMGTPE]([i]?B)?)?$ ]] || + [[ "$cgroup_memory_high" =~ ^0+([KMGTPE]([i]?B)?)?$ ]]; then + echo "constrained-linux-wal-action-v1 requires positive finite MemoryMax and MemoryHigh" >&2 + exit 2 + fi +fi +case "$checkpoint_policy" in + default|controlled) ;; + *) echo "--checkpoint-policy requires default or controlled" >&2; exit 2 ;; +esac +[[ "$libpq_latency_samples" =~ ^(0|[1-9][0-9]*)$ ]] || { + echo "--libpq-latency-samples requires a nonnegative integer" >&2 + exit 2 +} +[[ "$libpq_latency_warmup" =~ ^(0|[1-9][0-9]*)$ ]] || { + echo "--libpq-latency-warmup requires a nonnegative integer" >&2 + exit 2 +} +if ! awk -v samples="$libpq_latency_samples" -v warmup="$libpq_latency_warmup" \ + 'BEGIN { exit !((samples + 0) <= 10000000 && (warmup + 0) <= 10000000) }' +then + echo "libpq latency sample and warmup counts may not exceed 10000000" >&2 + exit 2 +fi +if [ "$libpq_latency_only" -eq 1 ]; then + [ "$libpq_latency_samples" -gt 0 ] || { + echo "--libpq-latency-only requires --libpq-latency-samples greater than zero" >&2 + exit 2 + } + [ "$workloads_explicit" -eq 0 ] || { + echo "--libpq-latency-only cannot be combined with --workload or --workloads" >&2 + exit 2 + } +fi +if [ "$libpq_latency_samples" -gt 0 ] && [ "$resource_detail" != "off" ]; then + echo "true libpq latency qualification requires --resource-detail off; run FD/PSS sampling as a separate diagnostic" >&2 + exit 2 +fi +is_nonnegative_integer "$libpq_latency_host_fd_allowance" || { + echo "WASIX_LIBPQ_LATENCY_HOST_FD_ALLOWANCE requires a nonnegative integer" >&2 + exit 2 +} +is_nonnegative_number "$pg_wait_sample_interval" || { echo "--pg-wait-sample-interval requires a non-negative number" >&2; exit 2; } +is_nonnegative_number "$sample_seconds" || { echo "--sample-seconds requires a non-negative number" >&2; exit 2; } +is_nonnegative_number "$sample_delay" || { echo "--sample-delay requires a non-negative number" >&2; exit 2; } +is_nonnegative_integer "$wasix_wait_dump_interval_ms" || { echo "--wasix-wait-dump-interval-ms requires a non-negative integer" >&2; exit 2; } +is_nonnegative_integer "$wasix_wait_dump_max_per_wait" || { echo "--wasix-wait-dump-max-per-wait requires a non-negative integer" >&2; exit 2; } +is_positive_integer "$wasix_lifecycle_reconnects" || { echo "--wasix-lifecycle-reconnects requires a positive integer" >&2; exit 2; } +is_positive_integer "$wasix_lifecycle_window_seconds" || { echo "--wasix-lifecycle-window-seconds requires a positive integer" >&2; exit 2; } +is_nonnegative_integer "$wasix_lifecycle_memory_checkpoint_every" || { + echo "--wasix-lifecycle-memory-checkpoint-every requires a non-negative integer" >&2 + exit 2 +} +is_positive_number "$wasix_lifecycle_memory_quiescence_seconds" || { + echo "--wasix-lifecycle-memory-quiescence-seconds requires a positive number" >&2 + exit 2 +} +validate_unsigned_budget --max-lifecycle-pss-growth-kib \ + "$max_lifecycle_pss_growth_kib" +validate_unsigned_budget --max-lifecycle-pss-anon-growth-kib \ + "$max_lifecycle_pss_anon_growth_kib" +validate_unsigned_budget --max-lifecycle-heap-growth-kib \ + "$max_lifecycle_heap_growth_kib" +for lifecycle_slope_budget in \ + "$max_late_lifecycle_pss_slope_kib_per_1000" \ + "$max_late_lifecycle_pss_anon_slope_kib_per_1000" \ + "$max_late_lifecycle_heap_slope_kib_per_1000"; do + [ -z "$lifecycle_slope_budget" ] || \ + is_nonnegative_number "$lifecycle_slope_budget" || { + echo 'lifecycle late-tail slope ceilings require nonnegative numbers' >&2 + exit 2 + } +done +if ! is_positive_integer "$start_port" || [ "$start_port" -gt 65535 ]; then + echo "--start-port requires a port number from 1 through 65535" >&2 + exit 2 +fi +case "$run_label" in ""|*[!A-Za-z0-9._-]*) echo "--label may only contain letters, numbers, '.', '_', and '-'" >&2; exit 2 ;; esac +case "$(printf '%s' "$wasix_perf_stats" | tr '[:upper:]' '[:lower:]')" in + 1|true|yes|on) wasix_perf_stats=1 ;; + 0|false|no|off|"") wasix_perf_stats=0 ;; + *) echo "WASIX_PERF_STATS must be 0/1, true/false, yes/no, or on/off" >&2; exit 2 ;; +esac +case "$(printf '%s' "$wasix_wait_dump_verbose" | tr '[:upper:]' '[:lower:]')" in + 1|true|yes|on) wasix_wait_dump_verbose=1 ;; + 0|false|no|off) wasix_wait_dump_verbose=0 ;; + "") wasix_wait_dump_verbose="$wasix_perf_stats" ;; + *) echo "WASIX_WAIT_DUMP_VERBOSE must be 0/1, true/false, yes/no, or on/off" >&2; exit 2 ;; +esac +case "$(printf '%s' "$wasix_lifecycle_plateau" | tr '[:upper:]' '[:lower:]')" in + 1|true|yes|on) wasix_lifecycle_plateau=1 ;; + 0|false|no|off|"") wasix_lifecycle_plateau=0 ;; + *) echo "WASIX_LIFECYCLE_PLATEAU must be 0/1, true/false, yes/no, or on/off" >&2; exit 2 ;; +esac +lifecycle_memory_budget_count=0 +for lifecycle_memory_budget in "$max_lifecycle_pss_growth_kib" \ + "$max_lifecycle_pss_anon_growth_kib" "$max_lifecycle_heap_growth_kib"; do + [ -z "$lifecycle_memory_budget" ] || \ + lifecycle_memory_budget_count=$((lifecycle_memory_budget_count + 1)) +done +lifecycle_memory_slope_budget_count=0 +for lifecycle_slope_budget in \ + "$max_late_lifecycle_pss_slope_kib_per_1000" \ + "$max_late_lifecycle_pss_anon_slope_kib_per_1000" \ + "$max_late_lifecycle_heap_slope_kib_per_1000"; do + [ -z "$lifecycle_slope_budget" ] || \ + lifecycle_memory_slope_budget_count=$((lifecycle_memory_slope_budget_count + 1)) +done +if [ "$wasix_lifecycle_memory_checkpoint_every" -gt 0 ]; then + [ "$wasix_lifecycle_plateau" -eq 1 ] || { + echo "--wasix-lifecycle-memory-checkpoint-every requires --wasix-lifecycle-plateau" >&2 + exit 2 + } + if [ "$(uname -s)" != Linux ] || [ ! -r /proc/self/smaps ] || \ + [ ! -r /proc/self/smaps_rollup ]; then + echo "lifecycle memory checkpoints require Linux procfs smaps and smaps_rollup" >&2 + exit 2 + fi + [ "$wasix_lifecycle_memory_checkpoint_every" -lt "$wasix_lifecycle_reconnects" ] || { + echo "lifecycle memory checkpoint interval must be smaller than reconnect count" >&2 + exit 2 + } + lifecycle_memory_checkpoint_count=$(( + (wasix_lifecycle_reconnects - 1) / + wasix_lifecycle_memory_checkpoint_every + 2 + )) + [ "$lifecycle_memory_checkpoint_count" -ge 5 ] || { + echo "lifecycle memory plateau requires at least five checkpoints" >&2 + exit 2 + } + [ "$lifecycle_memory_checkpoint_count" -le 257 ] || { + echo "lifecycle memory checkpoint schedule is too dense for nonintrusive sampling" >&2 + exit 2 + } + [ "$lifecycle_memory_budget_count" -eq 3 ] || { + echo "lifecycle memory checkpoints require all PSS, Pss_Anon, and heap growth ceilings" >&2 + exit 2 + } + [ "$lifecycle_memory_slope_budget_count" -eq 3 ] || { + echo "lifecycle memory checkpoints require all PSS, Pss_Anon, and heap late-tail slope ceilings" >&2 + exit 2 + } + awk -v lifecycle="$wasix_lifecycle_window_seconds" \ + -v memory="$wasix_lifecycle_memory_quiescence_seconds" \ + 'BEGIN { exit !(lifecycle >= memory) }' || { + echo "lifecycle sampling window must be at least the memory quiescence window" >&2 + exit 2 + } +elif [ "$lifecycle_memory_budget_count" -ne 0 ] || \ + [ "$lifecycle_memory_slope_budget_count" -ne 0 ]; then + echo "lifecycle memory growth ceilings require --wasix-lifecycle-memory-checkpoint-every" >&2 + exit 2 +fi +case "$(printf '%s' "$memory_map_snapshots" | tr '[:upper:]' '[:lower:]')" in + 1|true|yes|on) memory_map_snapshots=1 ;; + 0|false|no|off|"") memory_map_snapshots=0 ;; + *) echo "WASIX_MEMORY_MAP_SNAPSHOTS must be 0/1, true/false, yes/no, or on/off" >&2; exit 2 ;; +esac +case "$(printf '%s' "$cold_ownership" | tr '[:upper:]' '[:lower:]')" in + 1|true|yes|on) cold_ownership=1 ;; + 0|false|no|off|"") cold_ownership=0 ;; + *) echo "WASIX_COLD_OWNERSHIP must be 0/1, true/false, yes/no, or on/off" >&2; exit 2 ;; +esac +case "$immutable_carrier_verification_scope" in + full|campaign-fast) ;; + *) + echo '--immutable-carrier-verification-scope requires full or campaign-fast' >&2 + exit 2 + ;; +esac + +[ "$cold_ownership_workloads" -eq 0 ] || { + [ "$cold_ownership" -eq 1 ] && [ "$workloads_explicit" -eq 1 ] || { + echo '--cold-ownership-workloads requires --cold-ownership and explicit --workload(s)' >&2 + exit 2 + } +} + +if [ "$cold_ownership" -eq 1 ]; then + [ "$(uname -s)" = Linux ] || { + echo "--cold-ownership requires Linux" >&2 + exit 2 + } + [ -r /sys/fs/cgroup/cgroup.controllers ] || { + echo "--cold-ownership requires a cgroup-v2 host" >&2 + exit 2 + } + [ "$resource_detail" = full ] || { + echo "--cold-ownership requires --resource-detail full" >&2 + exit 2 + } + [ -n "$cgroup_memory_max" ] && [ -n "$cgroup_memory_high" ] && + [ -n "$cgroup_swap_max" ] || { + echo "--cold-ownership requires explicit cgroup MemoryMax, MemoryHigh, and MemorySwapMax limits" >&2 + exit 2 + } + [ "$libpq_latency_samples" -eq 0 ] && [ "$libpq_latency_only" -eq 0 ] || { + echo "--cold-ownership cannot be combined with the separately instrumented libpq latency lane" >&2 + exit 2 + } + [ "$wasix_lifecycle_plateau" -eq 0 ] || { + echo "--cold-ownership cannot be combined with lifecycle plateau qualification" >&2 + exit 2 + } + [ "$workloads_explicit" -eq 0 ] || [ "$cold_ownership_workloads" -eq 1 ] || { + echo "--cold-ownership is startup-only and cannot be combined with workload selection" >&2 + exit 2 + } + if awk -v value="$quiescence_seconds" 'BEGIN { exit !((value + 0) > 0) }'; then + echo "--cold-ownership forbids post-readiness quiescence" >&2 + exit 2 + fi +fi + +if [ "$wasix_lifecycle_plateau" -eq 1 ]; then + [ "$memory_budget_requested" -eq 0 ] || { + echo "--wasix-lifecycle-plateau cannot be combined with fan-out memory budgets" >&2 + exit 2 + } + [ "$libpq_latency_samples" -eq 0 ] && [ "$libpq_latency_only" -eq 0 ] || { + echo "--wasix-lifecycle-plateau is an untimed lane and cannot be combined with libpq latency" >&2 + exit 2 + } + [ "$workloads_explicit" -eq 0 ] || { + echo "--wasix-lifecycle-plateau cannot be combined with workload selection" >&2 + exit 2 + } + [ "$wasix_perf_stats" -eq 0 ] || { + echo "--wasix-lifecycle-plateau forbids WASIX perf stats" >&2 + exit 2 + } + if [ "$resource_detail_explicit" -eq 1 ] && [ "$resource_detail" != off ]; then + echo "--wasix-lifecycle-plateau requires --resource-detail off" >&2 + exit 2 + fi + resource_detail=off + wasix_wait_dump_max_per_wait=0 + if [ "$wasix_wait_dump_interval_ms" -eq 0 ]; then + wasix_wait_dump_interval_ms=100 + fi + if [ $((wasix_lifecycle_window_seconds * 1000)) -lt $((wasix_wait_dump_interval_ms * 2 + 1000)) ]; then + echo "--wasix-lifecycle-window-seconds is too short for three snapshots spanning one second at the configured wait-dump interval" >&2 + exit 2 + fi + if [[ -v WASIX_WAIT_DUMP_FILE ]] || [[ -v WASIX_PERF_WAIT_DUMP_FILE ]] || + [[ -v WASIX_WAIT_DUMP_FENCE_REQUEST_FILE ]] || + [[ -v WASIX_WAIT_DUMP_FENCE_ACK_FILE ]]; then + echo "--wasix-lifecycle-plateau owns its wait-dump log, fence-request, and committed-ACK paths; unset ambient wait-dump path variables" >&2 + exit 2 + fi + # This lane isolates reconnect ownership from legitimate timed-checkpoint + # state. Checkpoint/recycle behavior is qualified independently; allowing a + # five-minute checkpoint here makes the terminal tuple depend on run speed. + checkpoint_policy=controlled +else + [ "$lifecycle_baseline_policy_explicit" -eq 0 ] || { + echo "--lifecycle-baseline-policy requires --wasix-lifecycle-plateau" >&2 + exit 2 + } + if [ "$wasix_wait_dump_cli_config" -eq 1 ] || + [ "${#ambient_wait_dump_environment[@]}" -ne 0 ]; then + printf 'WASIX wait-dump instrumentation is an untimed lifecycle diagnostic only; remove wait-dump options/environment from timed benchmark lanes (found: %s)\n' \ + "${ambient_wait_dump_environment[*]:-command-line option}" >&2 + exit 2 + fi + # Keep the disabled receipt canonical rather than retaining inert defaults. + wasix_wait_dump_interval_ms=0 + wasix_wait_dump_max_per_wait=0 + wasix_wait_dump_verbose=0 +fi + +fresh_postgres_explicit_rows "${postgres_gucs[@]}" >/dev/null || exit +explicit_postgres_guc_names=() +for guc in "${postgres_gucs[@]}"; do + explicit_postgres_guc_names+=("${guc%%=*}") +done + +profile_resolution_active=0 +if [ -n "$runtime_footprint$durability_profile" ]; then + profile_resolution_active=1 + fresh_resolve_postgres_profiles "$runtime_footprint" "$durability_profile" \ + "${postgres_gucs[@]}" || exit + effective_postgres_gucs=("${FRESH_POSTGRES_PROFILE_GUCS[@]}") +else + effective_postgres_gucs=("${postgres_gucs[@]}") +fi +for guc in "${postgres_gucs[@]}"; do + case "$guc" in + *=*) ;; + *) printf -- '--postgres-guc requires name=value, got: %s\n' "$guc" >&2; exit 2 ;; + esac + if [ "$checkpoint_policy" = "controlled" ]; then + guc_name="$(printf '%s' "${guc%%=*}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" + case "$guc_name" in + checkpoint_timeout|max_wal_size|min_wal_size|fsync|synchronous_commit|full_page_writes) + printf 'controlled checkpoint policy owns PostgreSQL setting %s; remove the conflicting --postgres-guc\n' \ + "$guc_name" >&2 + exit 2 + ;; + esac + fi +done +if [ "$checkpoint_policy" = "controlled" ]; then + effective_postgres_gucs+=( + checkpoint_timeout=1h + max_wal_size=8GB + min_wal_size=1GB + ) + if [ "$durability_profile" != safe ]; then + effective_postgres_gucs+=( + fsync=on + synchronous_commit=on + full_page_writes=on + ) + fi +fi + +normalize_workload() { + case "$1" in + read|indexed-read|iread) echo "indexed-read" ;; + mwrite|mixed-write|multi-write) echo "mixed-write" ;; + iupdate|indexed-update) echo "indexed-update" ;; + indexed|indexed-insert|iinsert) echo "indexed-insert" ;; + *) + printf 'unknown workload: %s\n' "$1" >&2 + return 2 + ;; + esac +} + +normalize_target() { + case "$1" in + native|wasix) echo "$1" ;; + *) + printf 'unknown target: %s\n' "$1" >&2 + return 2 + ;; + esac +} + +sealed_carrier_root="" +sealed_manifest="" +sealed_receipt="" +sealed_initdb_module="" +sealed_postgres_module="" +sealed_lib_dir="" +sealed_manifest_hash="" +sealed_receipt_hash="" +sealed_payload_inventory="" +sealed_payload_inventory_hash="" +sealed_executor_role="" +sealed_executor_receipt_relative="" +sealed_executor_receipt_hash="" +sealed_executor_hash="" +immutable_carrier_receipt_sha256="none" +immutable_carrier_receipt_dev="none" +immutable_carrier_receipt_ino="none" +immutable_carrier_closure_identity="none" +immutable_carrier_core_profile="none" +immutable_carrier_guest_build_recipe_sha256="none" +immutable_carrier_postgres_module_sha256="none" +sealed_full_identity_captured=0 + +verify_immutable_carrier_deployment() { + local receipt_parent receipt_stat receipt_identity + + [ -n "$immutable_carrier_receipt" ] || { + echo '--require-zero-write-aot requires --immutable-carrier-receipt' >&2 + return 2 + } + receipt_parent="$(dirname "$immutable_carrier_receipt")" + [ -d "$receipt_parent" ] && [ ! -L "$receipt_parent" ] || { + printf 'immutable carrier receipt parent must be a non-symlink directory: %s\n' \ + "$receipt_parent" >&2 + return 2 + } + immutable_carrier_receipt="$(cd "$receipt_parent" && pwd -P)/$(basename "$immutable_carrier_receipt")" + [ -f "$immutable_carrier_receipt" ] && [ ! -L "$immutable_carrier_receipt" ] || { + printf 'immutable carrier receipt must be a regular non-symlink file: %s\n' \ + "$immutable_carrier_receipt" >&2 + return 2 + } + case "$immutable_carrier_receipt/" in + "$sealed_carrier_root/"|"$sealed_carrier_root/"*) + echo 'immutable deployment receipt must remain outside the sealed carrier' >&2 + return 2 + ;; + esac + "$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier_root" \ + --receipt "$immutable_carrier_receipt" --fast || return + receipt_identity="$(python3 - "$immutable_carrier_receipt" <<'PY' +import json +import re +import sys + +with open(sys.argv[1], encoding="ascii") as stream: + receipt = json.load(stream) +if receipt.get("schema") != "oliphaunt.wasix-postmaster.immutable-carrier-deployment.v2": + raise SystemExit("immutable carrier receipt schema differs") +carrier = receipt.get("carrier") +if not isinstance(carrier, dict): + raise SystemExit("immutable carrier receipt identity differs") +entries = receipt.get("entries") +if not isinstance(entries, list): + raise SystemExit("immutable carrier receipt entries differ") +postgres_entries = [ + entry for entry in entries + if isinstance(entry, dict) and entry.get("path") == "bin/postgres" +] +if len(postgres_entries) != 1: + raise SystemExit("immutable carrier receipt PostgreSQL module differs") +values = ( + carrier.get("closure-identity"), + carrier.get("manifest-sha256"), + carrier.get("wasmer-build-receipt-sha256"), + carrier.get("payload-inventory-sha256"), + carrier.get("headless-sha256"), + receipt.get("core_profile"), + receipt.get("guest_build_recipe_sha256"), + postgres_entries[0].get("sha256"), +) +if any(not isinstance(value, str) for value in values): + raise SystemExit("immutable carrier receipt identity value differs") +if any(re.fullmatch(r"[0-9a-f]{64}", value) is None for value in values[:5]): + raise SystemExit("immutable carrier receipt SHA-256 differs") +if values[5] not in {"release-o3", "safe-o2"}: + raise SystemExit("immutable carrier receipt core profile differs") +if re.fullmatch(r"[0-9a-f]{64}", values[6]) is None: + raise SystemExit("immutable carrier receipt guest build recipe differs") +if re.fullmatch(r"[0-9a-f]{64}", values[7]) is None: + raise SystemExit("immutable carrier receipt PostgreSQL module differs") +print(*values, sep="\t") +PY + )" || return + IFS=$'\t' read -r immutable_carrier_closure_identity \ + FRESH_QUALIFICATION_CARRIER_MANIFEST_SHA256 \ + FRESH_QUALIFICATION_CARRIER_RECEIPT_SHA256 \ + FRESH_QUALIFICATION_CARRIER_PAYLOAD_SHA256 \ + FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256 \ + immutable_carrier_core_profile \ + immutable_carrier_guest_build_recipe_sha256 \ + immutable_carrier_postgres_module_sha256 <<<"$receipt_identity" + if [ "$immutable_carrier_verification_scope" = full ]; then + [ "$sealed_full_identity_captured" -eq 1 ] || { + echo 'full immutable verification scope has no cryptographic carrier capture' >&2 + return 125 + } + [ "$immutable_carrier_closure_identity" = \ + "$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" ] && + [ "$immutable_carrier_core_profile" = \ + "$FRESH_QUALIFICATION_CORE_PROFILE" ] && + [ "$immutable_carrier_guest_build_recipe_sha256" = \ + "$FRESH_QUALIFICATION_GUEST_BUILD_RECIPE_SHA256" ] || { + echo 'immutable deployment receipt differs from full carrier verification' >&2 + return 125 + } + else + FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY="$immutable_carrier_closure_identity" + FRESH_QUALIFICATION_CORE_PROFILE="$immutable_carrier_core_profile" + FRESH_QUALIFICATION_GUEST_BUILD_RECIPE_SHA256="$immutable_carrier_guest_build_recipe_sha256" + fi + immutable_carrier_receipt_sha256="$(fresh_wasmer_bin_hash "$immutable_carrier_receipt")" || return + receipt_stat="$(stat -c '%d %i' -- "$immutable_carrier_receipt")" || return + read -r immutable_carrier_receipt_dev immutable_carrier_receipt_ino \ + <<<"$receipt_stat" + [[ "$immutable_carrier_receipt_dev" =~ ^[0-9]+$ ]] && + [[ "$immutable_carrier_receipt_ino" =~ ^[0-9]+$ ]] || { + echo 'immutable carrier receipt device/inode identity is invalid' >&2 + return 125 + } +} + +reject_sealed_compiler_configuration() { + local arg env_name env_value + + if [ "$skip_precompile_explicit" -eq 1 ]; then + echo "--skip-precompile cannot be combined with --sealed-carrier; sealed mode never precompiles" >&2 + return 2 + fi + case "$(printf '%s' "$skip_precompile" | tr '[:upper:]' '[:lower:]')" in + 0|false|no|off|"") ;; + 1|true|yes|on) + echo "WASIX_SKIP_PRECOMPILE cannot be enabled with --sealed-carrier; sealed mode has no compiler cache" >&2 + return 2 + ;; + *) + echo "WASIX_SKIP_PRECOMPILE must be 0/1, true/false, yes/no, or on/off" >&2 + return 2 + ;; + esac + + for env_name in \ + WASMER_BIN \ + WASMER_BUILD_RECEIPT \ + WASMER_COMPILER \ + WASMER_BACKEND \ + WASMER_COMPILER_THREADS \ + WASMER_LLVM_OPT_LEVEL \ + WASMER_LLVM_NATIVE_CPU \ + WASMER_LLVM_FULL_O3_PIPELINE \ + WASMER_LLVM_INDIRECT_CALL_CACHE \ + WASMER_LLVM_VOLATILE_MEMOPS \ + WASMER_DIR \ + WASMER_CACHE_DIR + do + env_value="$(printenv "$env_name" 2>/dev/null || true)" + if [ -n "$env_value" ]; then + printf '%s is incompatible with --sealed-carrier; the carrier fixes runtime, receipt, compiler, and cache provenance\n' \ + "$env_name" >&2 + return 2 + fi + done + + for arg in "${wasmer_extra_args[@]}"; do + case "$arg" in + --llvm|--llvm=*|--cranelift|--cranelift=*|--singlepass|--singlepass=*|\ + --compiler|--compiler=*|--compiler-threads|--compiler-threads=*|\ + --llvm-opt-level|--llvm-opt-level=*|--llvm-full-o3-pipeline|\ + --llvm-indirect-call-cache|--disable-non-volatile-memops|\ + --enable-verifier|--compiler-debug-dir|--compiler-debug-dir=*|\ + --profiler|--profiler=*|--enable-nan-canonicalization|\ + --wasmer-dir|--wasmer-dir=*|--cache-dir|--cache-dir=*|--disable-cache|\ + --engine|--engine=*|--stack-size|--stack-size=*|--disable-threads|\ + --net|--net=*|\ + --use|--use=*|--include-webc|--include-webc=*|--map-command|--map-command=*|\ + --sealed-module-manifest|--sealed-module-manifest=*) + printf 'compiler/sealed-loader option cannot be supplied through --wasmer-arg in sealed mode: %s\n' \ + "$arg" >&2 + return 2 + ;; + esac + done +} + +require_sealed_regular_file() { + local label="$1" + local path="$2" + + if [ ! -f "$path" ] || [ -L "$path" ]; then + printf 'sealed carrier %s must be a regular non-symlink file: %s\n' "$label" "$path" >&2 + return 2 + fi +} + +require_sealed_directory() { + local label="$1" + local path="$2" + + if [ ! -d "$path" ] || [ -L "$path" ]; then + printf 'sealed carrier %s must be a non-symlink directory: %s\n' "$label" "$path" >&2 + return 2 + fi +} + +validate_sealed_carrier_layout() { + local carrier="$1" + local artifact artifact_name symlink_path unexpected_aot_entry + local artifacts=() + + case "$carrier" in + *$'\n'*|*$'\r'*|*$'\t'*) + echo "--sealed-carrier may not contain tabs or newlines" >&2 + return 2 + ;; + esac + require_sealed_directory root "$carrier" || return + sealed_carrier_root="$(cd "$carrier" && pwd -P)" + if [ "$require_zero_write_aot" -eq 1 ]; then + if [ "$immutable_carrier_verification_scope" = full ]; then + fresh_capture_qualification_carrier_identity "$sealed_carrier_root" || return + sealed_full_identity_captured=1 + fi + else + fresh_verify_sealed_headless_carrier "$sealed_carrier_root" || return + fi + + if ! symlink_path="$(find "$sealed_carrier_root" -type l -print -quit 2>/dev/null)"; then + printf 'unable to inspect sealed carrier for symlinks: %s\n' "$sealed_carrier_root" >&2 + return 2 + fi + if [ -n "$symlink_path" ]; then + printf 'sealed carrier must not contain symlinks: %s\n' "$symlink_path" >&2 + return 2 + fi + + sealed_manifest="$sealed_carrier_root/manifest.json" + sealed_receipt="$sealed_carrier_root/wasmer-build.receipt" + sealed_payload_inventory="$sealed_carrier_root/payload.files" + sealed_initdb_module="$sealed_carrier_root/bin/initdb" + sealed_postgres_module="$sealed_carrier_root/bin/postgres" + sealed_lib_dir="$sealed_carrier_root/lib" + + fresh_sealed_executor_selection "$sealed_carrier_root" || return + sealed_executor_role="$FRESH_SEALED_EXECUTOR_ROLE" + sealed_executor_receipt_relative="$FRESH_SEALED_EXECUTOR_RECEIPT_RELATIVE" + sealed_executor_receipt_hash="$FRESH_SEALED_EXECUTOR_RECEIPT_SHA256" + sealed_executor_hash="$FRESH_SEALED_EXECUTOR_SHA256" + require_sealed_regular_file selected-executor-receipt \ + "$sealed_carrier_root/$sealed_executor_receipt_relative" || return + + require_sealed_regular_file headless-runtime "$sealed_carrier_root/bin/wasmer-headless" || return + [ -x "$sealed_carrier_root/bin/wasmer-headless" ] || { + printf 'sealed carrier headless runtime is not executable: %s\n' \ + "$sealed_carrier_root/bin/wasmer-headless" >&2 + return 2 + } + require_sealed_regular_file manifest "$sealed_manifest" || return + require_sealed_regular_file build-receipt "$sealed_receipt" || return + require_sealed_regular_file initdb-module "$sealed_initdb_module" || return + require_sealed_regular_file postgres-module "$sealed_postgres_module" || return + require_sealed_regular_file libpq-module "$sealed_lib_dir/libpq.so.5.18" || return + require_sealed_regular_file dict-snowball-module "$sealed_lib_dir/postgresql/dict_snowball.so" || return + require_sealed_regular_file plpgsql-module "$sealed_lib_dir/postgresql/plpgsql.so" || return + + require_sealed_directory postgres-share "$sealed_carrier_root/share/postgresql" || return + require_sealed_regular_file postgres-bootstrap "$sealed_carrier_root/share/postgresql/postgres.bki" || return + require_sealed_regular_file postgres-config-sample "$sealed_carrier_root/share/postgresql/postgresql.conf.sample" || return + require_sealed_regular_file postgres-hba-sample "$sealed_carrier_root/share/postgresql/pg_hba.conf.sample" || return + + require_sealed_directory aot-artifacts "$sealed_carrier_root/aot" || return + if ! unexpected_aot_entry="$(find "$sealed_carrier_root/aot" -mindepth 1 ! -type f -print -quit 2>/dev/null)"; then + printf 'unable to inspect sealed carrier AOT directory: %s\n' "$sealed_carrier_root/aot" >&2 + return 2 + fi + if [ -n "$unexpected_aot_entry" ]; then + printf 'sealed carrier aot/ may only contain regular artifact files: %s\n' \ + "$unexpected_aot_entry" >&2 + return 2 + fi + shopt -s nullglob + artifacts=("$sealed_carrier_root"/aot/*.bin) + shopt -u nullglob + if [ "${#artifacts[@]}" -ne 5 ]; then + printf 'sealed carrier must contain exactly the signed five-module AOT closure; found %s artifacts in %s\n' \ + "${#artifacts[@]}" "$sealed_carrier_root/aot" >&2 + return 2 + fi + for artifact in "${artifacts[@]}"; do + require_sealed_regular_file AOT-artifact "$artifact" || return + artifact_name="$(basename "$artifact")" + if [[ ! "$artifact_name" =~ ^[0-9A-F]{64}\.bin$ ]]; then + printf 'sealed carrier AOT artifact name must be an uppercase module SHA-256: %s\n' \ + "$artifact" >&2 + return 2 + fi + done + + sealed_manifest_hash="$(fresh_wasmer_bin_hash "$sealed_manifest")" + sealed_receipt_hash="$(fresh_wasmer_bin_hash "$sealed_receipt")" + sealed_payload_inventory_hash="$(fresh_wasmer_bin_hash "$sealed_payload_inventory")" + sealed_carrier_core_profile="$(python3 - "$sealed_manifest" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="ascii") as stream: + manifest = json.load(stream) +profile = manifest.get("core-profile") +if profile not in {"release-o3", "safe-o2"}: + raise SystemExit("sealed manifest core profile differs") +print(profile) +PY + )" || return + # A sealed carrier is the execution authority. Do not let the ambient + # source-build default mislabel evidence from its receipt-bound guest/AOT. + WASIX_CORE_PROFILE="$sealed_carrier_core_profile" +} + +require_sealed_output_disjoint() { + local label="$1" + local mutable_path="$2" + + case "$mutable_path/" in + "$sealed_carrier_root/"|"$sealed_carrier_root/"*) + printf 'sealed carrier overlaps mutable %s output: carrier=%s output=%s\n' \ + "$label" "$sealed_carrier_root" "$mutable_path" >&2 + return 2 + ;; + esac + case "$sealed_carrier_root/" in + "$mutable_path/"|"$mutable_path/"*) + printf 'sealed carrier is nested inside mutable %s output: carrier=%s output=%s\n' \ + "$label" "$sealed_carrier_root" "$mutable_path" >&2 + return 2 + ;; + esac +} + +if [ "$cold_ownership" -eq 1 ] && [ "$cold_ownership_workloads" -eq 0 ]; then + workloads=() +elif [ "$wasix_lifecycle_plateau" -eq 1 ]; then + workloads=() +elif [ "$libpq_latency_only" -eq 1 ]; then + workloads=() +elif [ "${#workloads[@]}" -eq 0 ]; then + workloads=("${default_workloads[@]}") +fi +if [ "${#targets[@]}" -eq 0 ]; then + if [ "$wasix_lifecycle_plateau" -eq 1 ] || [ "$cold_ownership" -eq 1 ]; then + targets=(wasix) + else + targets=(native wasix) + fi +fi + +normalized_workloads=() +seen_values="" +for workload in "${workloads[@]}"; do + workload="$(normalize_workload "$workload")" + case " $seen_values " in + *" $workload "*) printf 'duplicate workload: %s\n' "$workload" >&2; exit 2 ;; + esac + seen_values="$seen_values $workload" + normalized_workloads+=("$workload") +done +workloads=("${normalized_workloads[@]}") + +normalized_targets=() +seen_values="" +for target in "${targets[@]}"; do + target="$(normalize_target "$target")" + case " $seen_values " in + *" $target "*) printf 'duplicate target: %s\n' "$target" >&2; exit 2 ;; + esac + seen_values="$seen_values $target" + normalized_targets+=("$target") +done +targets=("${normalized_targets[@]}") +if [ "$wasix_lifecycle_plateau" -eq 1 ] && + { [ "${#targets[@]}" -ne 1 ] || [ "${targets[0]}" != wasix ]; }; then + echo "--wasix-lifecycle-plateau requires exactly --target wasix" >&2 + exit 2 +fi +if [ "$cold_ownership" -eq 1 ] && + { [ "${#targets[@]}" -ne 1 ] || [ "${targets[0]}" != wasix ]; }; then + echo "--cold-ownership requires exactly --target wasix" >&2 + exit 2 +fi +if [ $((start_port + ${#targets[@]} - 1)) -gt 65535 ]; then + echo "target ports exceed 65535; choose a lower --start-port" >&2 + exit 2 +fi + +need_wasix=0 +for target in "${targets[@]}"; do + [ "$target" = "wasix" ] && need_wasix=1 +done +if [ "$shared_memory_provider_explicit" -eq 1 ] && [ "$need_wasix" -ne 1 ]; then + echo "--shared-memory-provider requires the wasix target" >&2 + exit 2 +fi +if [ "$shared_memory_provider" = linux-tmpfs-v1 ] && + [ "$(uname -s)" != Linux ]; then + echo "linux-tmpfs-v1 requires Linux" >&2 + exit 2 +fi + +wasix_runtime_mode="compiler" +if [ -n "$sealed_carrier" ]; then + [ "$need_wasix" -eq 1 ] || { + echo "--sealed-carrier requires the wasix target" >&2 + exit 2 + } + reject_sealed_compiler_configuration + validate_sealed_carrier_layout "$sealed_carrier" + wasix_runtime_mode="sealed-headless" +fi +if [ "$require_zero_write_aot" -eq 1 ]; then + [ "$wasix_runtime_mode" = sealed-headless ] || { + echo '--require-zero-write-aot requires --sealed-carrier' >&2 + exit 2 + } + [ "$(uname -s)" = Linux ] || { + echo '--require-zero-write-aot requires Linux intrinsic immutability evidence' >&2 + exit 2 + } + [ "$(id -u)" -ne 0 ] || { + echo '--require-zero-write-aot benchmark execution must be unprivileged' >&2 + exit 2 + } + cap_eff="$(awk '$1 == "CapEff:" { print $2 }' /proc/self/status)" + [[ "$cap_eff" =~ ^[0-9a-fA-F]+$ ]] || { + echo 'could not read exact CapEff for zero-write qualification' >&2 + exit 2 + } + if (( (16#$cap_eff & (1 << 9)) != 0 )); then + echo '--require-zero-write-aot refuses a caller with effective CAP_LINUX_IMMUTABLE' >&2 + exit 2 + fi + verify_immutable_carrier_deployment +elif [ "$immutable_carrier_receipt_explicit" -eq 1 ]; then + echo '--immutable-carrier-receipt requires --require-zero-write-aot' >&2 + exit 2 +fi +if [ "$immutable_carrier_verification_scope" = campaign-fast ] && + [ "$require_zero_write_aot" -ne 1 ]; then + echo '--immutable-carrier-verification-scope campaign-fast requires --require-zero-write-aot' >&2 + exit 2 +fi +if [ "$cold_ownership" -eq 1 ] && [ "$wasix_runtime_mode" != sealed-headless ]; then + echo "--cold-ownership requires --sealed-carrier" >&2 + exit 2 +fi +if [ "$adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ]; then + [ "$need_wasix" -eq 1 ] || { + echo "constrained-linux-wal-action-v1 requires the wasix target" >&2 + exit 2 + } + [ "$wasix_runtime_mode" = sealed-headless ] || { + echo "constrained-linux-wal-action-v1 requires --sealed-carrier" >&2 + exit 2 + } + [ "${#workloads[@]}" -gt 0 ] || { + echo "constrained-linux-wal-action-v1 requires a workload lane" >&2 + exit 2 + } +fi + +fresh_ensure_dirs + +if ! command -v perl >/dev/null 2>&1 || + ! perl -MTime::HiRes=clock_gettime,CLOCK_MONOTONIC -e 'clock_gettime(CLOCK_MONOTONIC)' \ + >/dev/null 2>&1; then + echo "benchmark requires Perl Time::HiRes with CLOCK_MONOTONIC" >&2 + exit 127 +fi + +setup_rows="$row_count" +min_setup_rows=$((connections * iterations)) +if [ "$setup_rows" -lt "$min_setup_rows" ]; then + setup_rows="$min_setup_rows" +fi + +if [ ! -x "$NATIVE_INSTALL_DIR/bin/psql" ] || [ ! -x "$NATIVE_INSTALL_DIR/bin/postgres" ]; then + if [ "$skip_build" -eq 1 ]; then + printf 'missing native install with --skip-build: %s\n' "$NATIVE_INSTALL_DIR" >&2 + exit 2 + fi + "$FRESH_ROOT/bin/build-native-oracle.sh" >/dev/null +fi + +if [ "$need_wasix" -eq 1 ]; then + if [ "$wasix_runtime_mode" = "compiler" ] && + { [ ! -x "$WASIX_INSTALL_DIR/bin/postgres" ] || [ ! -x "$WASIX_INSTALL_DIR/bin/initdb" ]; }; then + if [ "$skip_build" -eq 1 ]; then + printf 'missing WASIX install with --skip-build: %s\n' "$WASIX_INSTALL_DIR" >&2 + exit 2 + fi + "$FRESH_ROOT/bin/build-wasix-core.sh" >/dev/null + fi +fi + +libpq_latency_probe_bin="" +libpq_latency_probe_sha256="" +libpq_latency_probe_source_sha256="" +libpq_latency_compiler="" +libpq_latency_libpq_path="" +libpq_latency_libpq_sha256="" + +build_libpq_latency_probe() { + local output="$1" + local source="$FRESH_ROOT/probes/libpq_latency_probe.c" + local pg_config="$NATIVE_INSTALL_DIR/bin/pg_config" + local cc_bin="${CC:-cc}" + local include_dir lib_dir pending linker_lib canonical_lib linked_lib linked_canonical + local host_os + + command -v "$cc_bin" >/dev/null 2>&1 || { + printf 'libpq latency lane requires a C compiler executable: %s\n' "$cc_bin" >&2 + return 127 + } + [ -x "$pg_config" ] || { + printf 'libpq latency lane requires native pg_config: %s\n' "$pg_config" >&2 + return 2 + } + if [ ! -f "$source" ] || [ -L "$source" ]; then + printf 'libpq latency probe source must be a regular non-symlink file: %s\n' "$source" >&2 + return 2 + fi + include_dir="$($pg_config --includedir)" + lib_dir="$($pg_config --libdir)" + [ -f "$include_dir/libpq-fe.h" ] || { + printf 'native libpq header is missing: %s\n' "$include_dir/libpq-fe.h" >&2 + return 2 + } + [ -d "$lib_dir" ] || { + printf 'native libpq directory is missing: %s\n' "$lib_dir" >&2 + return 2 + } + case "$include_dir$lib_dir$output" in + *$'\t'*|*$'\n'*|*$'\r'*) + echo "libpq latency build paths may not contain tabs or newlines" >&2 + return 2 + ;; + esac + + host_os="$(uname -s)" + case "$host_os" in + Linux) linker_lib="$lib_dir/libpq.so" ;; + Darwin) linker_lib="$lib_dir/libpq.dylib" ;; + *) + printf 'libpq latency shared-library provenance is unsupported on host: %s\n' "$host_os" >&2 + return 2 + ;; + esac + [ -e "$linker_lib" ] || { + printf 'native libpq linker library is missing: %s\n' "$linker_lib" >&2 + return 2 + } + if ! canonical_lib="$(perl -MCwd=abs_path -e ' + my $path = abs_path($ARGV[0]); + exit 1 unless defined($path); + print $path; + ' "$linker_lib")" + then + printf 'could not canonicalize native libpq linker library: %s\n' "$linker_lib" >&2 + return 2 + fi + if [ ! -f "$canonical_lib" ] || [ -L "$canonical_lib" ]; then + printf 'native libpq canonical library is not a regular file: %s\n' "$canonical_lib" >&2 + return 2 + fi + + pending="$(mktemp "$(dirname "$output")/.libpq-latency-probe.XXXXXX")" + if ! "$cc_bin" -std=c11 -O2 -g0 -Wall -Wextra -Werror -Wpedantic \ + -Wconversion -Wshadow -I"$include_dir" "$source" \ + -L"$lib_dir" "-Wl,-rpath,$lib_dir" -lpq -o "$pending" + then + rm -f -- "$pending" + return 1 + fi + chmod 0755 "$pending" + if [ "$host_os" = "Linux" ]; then + command -v ldd >/dev/null 2>&1 || { + rm -f -- "$pending" + echo "libpq latency provenance verification requires ldd on Linux" >&2 + return 127 + } + linked_lib="$(env -u LD_LIBRARY_PATH -u LD_PRELOAD -u LD_AUDIT \ + ldd "$pending" | awk '$1 ~ /^libpq[.]so/ && $2 == "=>" { print $3 }')" + case "$linked_lib" in + ""|*$'\n'*) + rm -f -- "$pending" + echo "could not resolve one exact libpq dependency for latency probe" >&2 + return 2 + ;; + esac + if ! linked_canonical="$(perl -MCwd=abs_path -e ' + my $path = abs_path($ARGV[0]); + exit 1 unless defined($path); + print $path; + ' "$linked_lib")" + then + rm -f -- "$pending" + printf 'could not canonicalize linked libpq dependency: %s\n' "$linked_lib" >&2 + return 2 + fi + if [ "$linked_canonical" != "$canonical_lib" ]; then + rm -f -- "$pending" + printf 'latency probe resolved unexpected libpq: expected=%s actual=%s\n' \ + "$canonical_lib" "$linked_canonical" >&2 + return 2 + fi + elif [ "$host_os" = "Darwin" ]; then + command -v otool >/dev/null 2>&1 || { + rm -f -- "$pending" + echo "libpq latency provenance verification requires otool on macOS" >&2 + return 127 + } + linked_lib="$(otool -L "$pending" | awk 'NR > 1 && $1 ~ /libpq/ { print $1 }')" + case "$linked_lib" in + ""|*$'\n'*) + rm -f -- "$pending" + echo "could not resolve one exact libpq dependency for latency probe" >&2 + return 2 + ;; + @rpath/*) linked_lib="$lib_dir/${linked_lib##*/}" ;; + esac + if ! linked_canonical="$(perl -MCwd=abs_path -e ' + my $path = abs_path($ARGV[0]); + exit 1 unless defined($path); + print $path; + ' "$linked_lib")" + then + rm -f -- "$pending" + printf 'could not canonicalize linked libpq dependency: %s\n' "$linked_lib" >&2 + return 2 + fi + if [ "$linked_canonical" != "$canonical_lib" ]; then + rm -f -- "$pending" + printf 'latency probe resolved unexpected libpq: expected=%s actual=%s\n' \ + "$canonical_lib" "$linked_canonical" >&2 + return 2 + fi + fi + mv "$pending" "$output" + libpq_latency_probe_bin="$output" + libpq_latency_probe_sha256="$(fresh_wasmer_bin_hash "$output")" + libpq_latency_probe_source_sha256="$(fresh_wasmer_bin_hash "$source")" + libpq_latency_compiler="$($cc_bin --version 2>/dev/null | awk 'NR == 1 { print }')" + libpq_latency_libpq_path="$canonical_lib" + libpq_latency_libpq_sha256="$(fresh_wasmer_bin_hash "$canonical_lib")" +} + +now_ms() { + perl -MTime::HiRes=clock_gettime,CLOCK_MONOTONIC \ + -e 'printf "%.0f\n", clock_gettime(CLOCK_MONOTONIC) * 1000' +} + +now_ns() { + perl -MTime::HiRes=clock_gettime,CLOCK_MONOTONIC \ + -e 'printf "%.0f\n", clock_gettime(CLOCK_MONOTONIC) * 1000000000' +} + +new_lifecycle_nonce() { + local nonce + + nonce="$(od -An -N16 -tx1 /dev/urandom 2>/dev/null | tr -d '[:space:]')" + if [[ "$nonce" =~ ^[0-9a-f]{32}$ ]]; then + printf '%s\n' "$nonce" + return 0 + fi + echo "could not create a 128-bit lifecycle evidence nonce" >&2 + return 1 +} + +calc_rate() { + local operations="$1" + local wall_ms="$2" + perl -e 'my ($ops, $ms) = @ARGV; printf "%.3f", $ms > 0 ? ($ops * 1000.0 / $ms) : 0' \ + "$operations" "$wall_ms" +} + +float_gt_zero() { + perl -e 'exit !(($ARGV[0] + 0) > 0)' "$1" +} + +extract_psql_time_sum_ms() { + local log="$1" + awk '/^Time: [0-9.]+ ms/ { sum += $2; count += 1 } END { if (count == 0) printf ""; else printf "%.3f", sum }' "$log" +} + +extract_psql_time_count() { + local log="$1" + awk '/^Time: [0-9.]+ ms/ { count += 1 } END { printf "%d", count }' "$log" +} + +run_logged_timeout() { + local timeout="$1" + shift + local log="$1" + shift + + fresh_run_process_group_timeout "$timeout" -- "$@" >"$log" 2>&1 +} + +collect_linux_process_tree() ( + local root_pid="$1" + local proc_root="$2" + local pid task_dir children_file children child + local queue_index=0 + local -a queue task_dirs + local -A discovered queued + + case "$root_pid" in + ''|0|0*|*[!0-9]*) return 2 ;; + esac + case "$proc_root" in + /*) ;; + *) return 2 ;; + esac + + # Linux exposes each task's immediate children directly. Walking those + # files is O(the measured tree), unlike `ps -axo`, whose two full-system + # scans made one nominal 100 ms sample take seconds on a busy host. Scan + # every thread because a child belongs to the specific task that created it. + shopt -s nullglob + queue=("$root_pid") + queued["$root_pid"]=1 + while [ "$queue_index" -lt "${#queue[@]}" ]; do + pid="${queue[$queue_index]}" + queue_index=$((queue_index + 1)) + [ -z "${discovered[$pid]+x}" ] || continue + discovered["$pid"]=1 + if [ ! -d "$proc_root/$pid" ]; then + [ "$pid" != "$root_pid" ] || return 1 + continue + fi + printf '%s\n' "$pid" + task_dirs=("$proc_root/$pid/task/"[0-9]*) + [ "${#task_dirs[@]}" -gt 0 ] || return 1 + for task_dir in "${task_dirs[@]}"; do + children_file="$task_dir/children" + if [ ! -r "$children_file" ]; then + # A non-leader thread can exit between glob expansion and the read. + [ ! -d "$task_dir" ] && continue + return 1 + fi + children="$(<"$children_file")" || return 1 + for child in $children; do + case "$child" in + ''|0|0*|*[!0-9]*) return 1 ;; + esac + if [ -z "${queued[$child]+x}" ]; then + queue+=("$child") + queued["$child"]=1 + fi + done + done + done +) + +collect_process_tree() { + local root_pid="$1" + + if [ -r "/proc/$root_pid/task/$root_pid/children" ]; then + collect_linux_process_tree "$root_pid" /proc + return + fi + ps -axo pid=,ppid= | awk -v root="$root_pid" ' + { + pid = $1 + ppid = $2 + seen[pid] = 1 + parent[pid] = ppid + } + END { + if (!(root in seen)) { + exit + } + for (pid in seen) { + cur = pid + depth = 0 + while (cur != "" && depth < 10000) { + if (cur == root) { + print pid + break + } + cur = parent[cur] + depth++ + } + } + } + ' +} + +collect_cgroup_process_set() { + local root_pid="$1" + local cgroup_dir="$2" + local pid + local root_seen=0 + local -A seen + + case "$root_pid" in + ''|0|0*|*[!0-9]*) return 2 ;; + esac + case "$cgroup_dir" in + /*) ;; + *) return 2 ;; + esac + [ -d "$cgroup_dir" ] && [ -r "$cgroup_dir/cgroup.procs" ] || return 1 + while IFS= read -r pid; do + case "$pid" in + ''|0|0*|*[!0-9]*) return 1 ;; + esac + [ -z "${seen[$pid]+x}" ] || return 1 + seen["$pid"]=1 + [ "$pid" != "$root_pid" ] || root_seen=1 + printf '%s\n' "$pid" + done <"$cgroup_dir/cgroup.procs" + [ "$root_seen" -eq 1 ] +} + +collect_process_tree_snapshot() { + local root_pid="$1" + local cgroup_dir="${2:-}" + local pid identity + local failed=0 + local process_tree + + if [ -n "$cgroup_dir" ]; then + # The benchmark-created transient scope contains only the measured server + # tree. Its cgroup.procs file is both stronger and O(scope), avoiding the + # kernel-documented sibling-omission race in procfs `children` during exit. + process_tree="$(collect_cgroup_process_set "$root_pid" "$cgroup_dir")" || return 1 + else + process_tree="$(collect_process_tree "$root_pid")" || return 1 + fi + while IFS= read -r pid; do + [ -n "$pid" ] || continue + identity="$(fresh_process_birth_identity "$pid" 2>/dev/null || true)" + if [ -z "$identity" ]; then + failed=1 + continue + fi + printf '%s\t%s\n' "$pid" "$identity" + done < <(printf '%s\n' "$process_tree" | sort -n) + [ "$failed" -eq 0 ] +} + +collect_linux_smaps_rollup() { + local pids="$1" + local pid rollup + + [ -d /proc ] || { + printf '0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\n' + return + } + + { + for pid in $pids; do + rollup="/proc/$pid/smaps_rollup" + [ -r "$rollup" ] || continue + awk ' + /^Pss:[[:space:]]/ { pss = $2 } + /^Pss_Anon:[[:space:]]/ { pss_anon = $2 } + /^Pss_File:[[:space:]]/ { pss_file = $2 } + /^Pss_Shmem:[[:space:]]/ { pss_shmem = $2 } + /^Private_Clean:[[:space:]]/ { private_clean += $2; private += $2 } + /^Private_Dirty:[[:space:]]/ { private_dirty += $2; private += $2 } + /^Private_Hugetlb:[[:space:]]/ { private += $2 } + /^Shared_Clean:[[:space:]]/ { shared += $2 } + /^Shared_Dirty:[[:space:]]/ { shared += $2 } + /^Shared_Hugetlb:[[:space:]]/ { shared += $2 } + /^Anonymous:[[:space:]]/ { anonymous = $2 } + /^Swap:[[:space:]]/ { swap = $2 } + /^Threads:[[:space:]]/ { threads = $2 } + /^VmPTE:[[:space:]]/ { vm_pte = $2 } + END { + printf "%.0f\t%.0f\t%.0f\t%.0f\t%.0f\t%.0f\t%.0f\t%.0f\t1\t%.0f\t%.0f\t%.0f\t%.0f\n", + pss, + pss_anon, + pss_file, + pss_shmem, + private, + shared, + anonymous, + swap, + threads, + vm_pte, + private_clean, + private_dirty + } + ' "$rollup" "/proc/$pid/status" 2>/dev/null || true + done + } | awk -F '\t' ' + { + pss += $1 + pss_anon += $2 + pss_file += $3 + pss_shmem += $4 + private += $5 + shared += $6 + anonymous += $7 + swap += $8 + processes += $9 + threads += $10 + vm_pte += $11 + private_clean += $12 + private_dirty += $13 + } + END { + printf "%.0f\t%.0f\t%.0f\t%.0f\t%.0f\t%.0f\t%.0f\t%.0f\t%d\t%d\t%.0f\t%.0f\t%.0f\n", + pss, + pss_anon, + pss_file, + pss_shmem, + private, + shared, + anonymous, + swap, + processes, + threads, + vm_pte, + private_clean, + private_dirty + } + ' +} + +set_resource_phase() { + local phase_file="$1" + local phase="$2" + printf '%s\n' "$phase" >"$phase_file" 2>/dev/null || true +} + +print_empty_linux_cgroup_metrics() { + local column + + for ((column = 1; column < 34; column++)); do + printf '\t' + done + printf '\n' +} + +cgroup_read_value="" +read_optional_cgroup_numeric() { + local path="$1" + local label="$2" + local value="" + + cgroup_read_value="" + [ -r "$path" ] || return 0 + if ! value="$(<"$path")"; then + # A transient scope can disappear between the readability check and read. + [ ! -e "$path" ] && return 0 + printf 'unable to read cgroup %s: %s\n' "$label" "$path" >&2 + return 1 + fi + case "$value" in + ""|*[!0-9]*) + printf 'malformed cgroup %s (expected an unsigned integer): %s\n' \ + "$label" "$path" >&2 + return 1 + ;; + esac + cgroup_read_value="$value" +} + +cgroup_read_limit="" +read_optional_cgroup_limit() { + local path="$1" + local label="$2" + local value="" + + cgroup_read_limit="" + [ -r "$path" ] || return 0 + if ! value="$(<"$path")"; then + [ ! -e "$path" ] && return 0 + printf 'unable to read cgroup %s: %s\n' "$label" "$path" >&2 + return 1 + fi + case "$value" in + max) ;; + ""|*[!0-9]*) + printf 'malformed cgroup %s (expected an unsigned integer or max): %s\n' \ + "$label" "$path" >&2 + return 1 + ;; + esac + cgroup_read_limit="$value" +} + +parse_cgroup_memory_stat() { + awk ' + BEGIN { + required["anon"] = 1 + required["file"] = 1 + required["shmem"] = 1 + required["kernel"] = 1 + required["pagetables"] = 1 + required["slab"] = 1 + required["file_dirty"] = 1 + required["file_writeback"] = 1 + } + $1 in required { + if (NF != 2 || $2 !~ /^[0-9]+$/ || seen[$1]++) { + malformed = 1 + } else { + value[$1] = $2 + } + } + END { + for (key in required) { + if (!seen[key]) { + malformed = 1 + } + } + if (malformed) { + exit 1 + } + printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + value["anon"], + value["file"], + value["shmem"], + value["kernel"], + value["pagetables"], + value["slab"], + value["file_dirty"], + value["file_writeback"] + } + ' +} + +parse_cgroup_memory_stat_file_cache() { + # These are exact memory.stat keys. In particular, pgscan and pgsteal are + # already aggregate counters; their overlapping component keys must not be + # summed into them. Kernel/configuration-dependent absence remains explicit. + awk ' + BEGIN { + requested[1] = "active_file" + requested[2] = "inactive_file" + requested[3] = "file_mapped" + requested[4] = "workingset_refault_file" + requested[5] = "workingset_activate_file" + requested[6] = "workingset_restore_file" + requested[7] = "pgscan" + requested[8] = "pgsteal" + for (i = 1; i <= 8; i++) selected[requested[i]] = 1 + } + $1 in selected { + if (NF != 2 || $2 !~ /^[0-9]+$/ || seen[$1]++) { + malformed = 1 + } else { + value[$1] = $2 + } + } + END { + if (malformed) exit 1 + for (i = 1; i <= 8; i++) { + key = requested[i] + if (i > 1) printf "\t" + if (seen[key]) { + printf "%s", value[key] + } else { + if (missing != "") missing = missing "," + missing = missing key + } + } + printf "\t%s\t%s\n", (missing == "" ? "complete" : "partial"), + (missing == "" ? "none" : missing) + } + ' +} + +cgroup_has_child_cgroup() { + local root="$1" + local entry + + for entry in "$root"/*; do + [ -d "$entry" ] || continue + return 0 + done + return 1 +} + +parse_cgroup_memory_pressure() { + awk ' + $1 == "some" || $1 == "full" { + kind = $1 + if (seen[kind]++) { + malformed = 1 + } + found_total = 0 + for (i = 2; i <= NF; i++) { + part_count = split($i, part, "=") + if (part[1] == "total") { + if (found_total || part_count != 2 || part[2] !~ /^[0-9]+$/) { + malformed = 1 + } else { + total[kind] = part[2] + found_total = 1 + } + } + } + if (!found_total) { + malformed = 1 + } + } + END { + if (!seen["some"] || !seen["full"] || malformed) { + exit 1 + } + printf "%s\t%s\n", total["some"], total["full"] + } + ' +} + +parse_cgroup_memory_events() { + awk ' + BEGIN { + required["high"] = 1 + required["max"] = 1 + required["oom"] = 1 + required["oom_kill"] = 1 + } + $1 in required { + if (NF != 2 || $2 !~ /^[0-9]+$/ || seen[$1]++) { + malformed = 1 + } else { + value[$1] = $2 + } + } + END { + for (key in required) { + if (!seen[key]) { + malformed = 1 + } + } + if (malformed) { + exit 1 + } + printf "%s\t%s\t%s\t%s\n", + value["high"], value["max"], value["oom"], value["oom_kill"] + } + ' +} + +collect_linux_cgroup_metrics() { + local root_pid="$1" + local cgroup_relative cgroup_dir + local memory_current="" memory_peak="" swap_current="" swap_peak="" pids_current="" + local event_high="" event_max="" event_oom="" event_oom_kill="" events event_metrics + local memory_events_path="" memory_events_source="" + local memory_max="" memory_high="" swap_max="" + local memory_stat="" memory_stat_metrics memory_stat_file_cache_metrics + local memory_pressure="" memory_pressure_metrics + + event_metrics="$(printf '\t\t\t')" + memory_stat_metrics="$(printf '\t\t\t\t\t\t\t')" + memory_stat_file_cache_metrics="$(printf '\t\t\t\t\t\t\t\tpartial\tactive_file,inactive_file,file_mapped,workingset_refault_file,workingset_activate_file,workingset_restore_file,pgscan,pgsteal')" + memory_pressure_metrics="$(printf '\t')" + + if [ -z "$cgroup_memory_max$cgroup_memory_high$cgroup_swap_max" ] || + [ ! -r "/proc/$root_pid/cgroup" ]; then + print_empty_linux_cgroup_metrics + return + fi + cgroup_relative="$(awk -F: '$1 == "0" { print $3; exit }' "/proc/$root_pid/cgroup")" + if [ -z "$cgroup_relative" ]; then + [ ! -e "/proc/$root_pid/cgroup" ] && { + print_empty_linux_cgroup_metrics + return + } + printf 'malformed cgroup-v2 membership for pid %s: %s\n' \ + "$root_pid" "/proc/$root_pid/cgroup" >&2 + return 1 + fi + case "$cgroup_relative" in + /*) ;; + *) + printf 'unsafe cgroup-v2 relative path for pid %s: %s\n' \ + "$root_pid" "$cgroup_relative" >&2 + return 1 + ;; + esac + cgroup_dir="/sys/fs/cgroup$cgroup_relative" + [ -d "$cgroup_dir" ] || { + print_empty_linux_cgroup_metrics + return + } + read_optional_cgroup_numeric "$cgroup_dir/memory.current" memory.current || return + memory_current="$cgroup_read_value" + read_optional_cgroup_numeric "$cgroup_dir/memory.peak" memory.peak || return + memory_peak="$cgroup_read_value" + read_optional_cgroup_numeric "$cgroup_dir/memory.swap.current" memory.swap.current || return + swap_current="$cgroup_read_value" + read_optional_cgroup_numeric "$cgroup_dir/memory.swap.peak" memory.swap.peak || return + swap_peak="$cgroup_read_value" + read_optional_cgroup_numeric "$cgroup_dir/pids.current" pids.current || return + pids_current="$cgroup_read_value" + read_optional_cgroup_limit "$cgroup_dir/memory.max" memory.max || return + memory_max="$cgroup_read_limit" + read_optional_cgroup_limit "$cgroup_dir/memory.high" memory.high || return + memory_high="$cgroup_read_limit" + read_optional_cgroup_limit "$cgroup_dir/memory.swap.max" memory.swap.max || return + swap_max="$cgroup_read_limit" + memory_events_path="$cgroup_dir/memory.events" + memory_events_source="memory.events" + if ! cgroup_has_child_cgroup "$cgroup_dir" && + [ -e "$cgroup_dir/memory.events.local" ]; then + memory_events_path="$cgroup_dir/memory.events.local" + memory_events_source="memory.events.local" + fi + if [ -r "$memory_events_path" ]; then + events="$(<"$memory_events_path")" + if ! event_metrics="$(printf '%s\n' "$events" | parse_cgroup_memory_events)"; then + [ ! -e "$memory_events_path" ] || { + printf 'malformed cgroup memory events: %s\n' "$memory_events_path" >&2 + return 1 + } + event_metrics="$(printf '\t\t\t')" + fi + IFS=$'\t' read -r event_high event_max event_oom event_oom_kill <<<"$event_metrics" + fi + if [ -r "$cgroup_dir/memory.stat" ]; then + memory_stat="$(<"$cgroup_dir/memory.stat")" + if ! memory_stat_metrics="$(printf '%s\n' "$memory_stat" | parse_cgroup_memory_stat)"; then + [ ! -e "$cgroup_dir/memory.stat" ] || { + printf 'malformed cgroup memory.stat: %s\n' "$cgroup_dir/memory.stat" >&2 + return 1 + } + memory_stat_metrics="$(printf '\t\t\t\t\t\t\t')" + fi + if ! memory_stat_file_cache_metrics="$( + printf '%s\n' "$memory_stat" | parse_cgroup_memory_stat_file_cache + )"; then + [ ! -e "$cgroup_dir/memory.stat" ] || { + printf 'malformed cgroup memory.stat file-cache fields: %s\n' \ + "$cgroup_dir/memory.stat" >&2 + return 1 + } + memory_stat_file_cache_metrics="$(printf '\t\t\t\t\t\t\t\tpartial\tactive_file,inactive_file,file_mapped,workingset_refault_file,workingset_activate_file,workingset_restore_file,pgscan,pgsteal')" + fi + fi + if [ -r "$cgroup_dir/memory.pressure" ]; then + memory_pressure="$(<"$cgroup_dir/memory.pressure")" + if ! memory_pressure_metrics="$(printf '%s\n' "$memory_pressure" | parse_cgroup_memory_pressure)"; then + [ ! -e "$cgroup_dir/memory.pressure" ] || { + printf 'malformed cgroup memory.pressure: %s\n' "$cgroup_dir/memory.pressure" >&2 + return 1 + } + memory_pressure_metrics="$(printf '\t')" + fi + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$cgroup_relative" "$memory_current" "$memory_peak" "$swap_current" "$swap_peak" \ + "$pids_current" "${event_high:-}" "${event_max:-}" "${event_oom:-}" \ + "${event_oom_kill:-}" "$memory_max" "$memory_high" "$swap_max" \ + "$memory_stat_metrics" "$memory_pressure_metrics" \ + "$memory_stat_file_cache_metrics" "$memory_events_source" +} + +monitor_resource_usage() { + local target="$1" + local root_pid="$2" + local phase_file="$3" + local stop_file="$4" + local samples_tsv="$5" + local interval="$6" + local detail="$7" + local server_cgroup_dir="${8:-}" + local now phase phase_after pids pid_csv metrics cgroup_metrics_all cgroup_metrics + local cgroup_file_cache_metrics host_fd_metrics + local host_kernel snapshot_before snapshot_after tree_status expected_processes + local observed_smaps smaps_status cgroup_status empty_smaps empty_cgroup + local empty_cgroup_file_cache + local process_tree_race_retries=0 + local process_tree_race_retry_limit=3 + local phase_transition_retries=0 + local phase_transition_retry_limit=3 + + host_kernel="$(uname -s 2>/dev/null || printf 'unknown')" + empty_smaps="$(printf '\t\t\t\t\t\t\t\t\t\t\t\t')" + empty_cgroup="$(printf '\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t')" + empty_cgroup_file_cache="$(printf '\t\t\t\t\t\t\t\t\t\t')" + printf 'monotonic_ms\ttarget\tphase\troot_pid\tprocess_count\trss_kb_total\tvsz_kb_total\tcpu_percent_total\tmax_rss_kb_per_pid\tpids\tpss_kb_total\tpss_anon_kb_total\tpss_file_kb_total\tpss_shmem_kb_total\tprivate_kb_total\tshared_mapped_kb_total\tanonymous_mapped_kb_total\tswap_kb_total\tsmaps_process_count\thost_thread_count_total\tpage_table_kb_total\tprivate_clean_kb_total\tprivate_dirty_kb_total\tcgroup_path\tcgroup_memory_current_bytes\tcgroup_scope_memory_peak_bytes\tcgroup_swap_current_bytes\tcgroup_scope_swap_peak_bytes\tcgroup_pids_current\tcgroup_scope_event_high_total\tcgroup_scope_event_max_total\tcgroup_scope_event_oom_total\tcgroup_scope_event_oom_kill_total\tcgroup_memory_max\tcgroup_memory_high\tcgroup_swap_max\tcgroup_memory_stat_anon_bytes\tcgroup_memory_stat_file_bytes\tcgroup_memory_stat_shmem_bytes\tcgroup_memory_stat_kernel_bytes\tcgroup_memory_stat_pagetables_bytes\tcgroup_memory_stat_slab_bytes\tcgroup_memory_stat_file_dirty_bytes\tcgroup_memory_stat_file_writeback_bytes\tcgroup_memory_pressure_some_total_usec\tcgroup_memory_pressure_full_total_usec\thost_open_fd_count_total\thost_open_fd_observed_process_count\thost_open_fd_expected_process_count\thost_open_fd_status\tsmaps_expected_process_count\tsmaps_observed_process_count\tsmaps_status\tcgroup_status\tcgroup_memory_stat_active_file_bytes\tcgroup_memory_stat_inactive_file_bytes\tcgroup_memory_stat_file_mapped_bytes\tcgroup_memory_stat_workingset_refault_file_pages_total\tcgroup_memory_stat_workingset_activate_file_pages_total\tcgroup_memory_stat_workingset_restore_file_pages_total\tcgroup_memory_stat_pgscan_pages_total\tcgroup_memory_stat_pgsteal_pages_total\tcgroup_memory_stat_file_cache_status\tcgroup_memory_stat_file_cache_missing_keys\tcgroup_memory_events_source\tprocess_tree_status\n' >"$samples_tsv" + while :; do + now="$(now_ms)" + phase="$(tr -d '[:space:]' <"$phase_file" 2>/dev/null || true)" + [ -n "$phase" ] || phase="unknown" + tree_status="ok" + snapshot_before="" + if ! snapshot_before="$(collect_process_tree_snapshot "$root_pid" "$server_cgroup_dir")"; then + tree_status="raced" + fi + pids="$(printf '%s\n' "$snapshot_before" | awk -F '\t' 'NF >= 2 { print $1 }' | tr '\n' ' ')" + expected_processes="$(printf '%s\n' "$snapshot_before" | awk -F '\t' 'NF >= 2 { count++ } END { print count + 0 }')" + if [ -n "$pids" ]; then + if ! cgroup_metrics_all="$(collect_linux_cgroup_metrics "$root_pid")"; then + return 1 + fi + if ! host_fd_metrics="$(fresh_collect_host_fd_occupancy "$host_kernel" /proc "$pids")"; then + return 1 + fi + pid_csv="$(printf '%s\n' "$pids" | awk '{$1=$1; gsub(/ /, ","); print}')" + metrics="$(ps -o rss= -o vsz= -o %cpu= -p "$pid_csv" 2>/dev/null | awk ' + { + rss += $1 + vsz += $2 + cpu += $3 + if ($1 > maxrss) { + maxrss = $1 + } + count += 1 + } + END { + printf "%d\t%d\t%d\t%.1f\t%d", count, rss, vsz, cpu, maxrss + } + ')" + [ -n "$metrics" ] || metrics="0 0 0 0.0 0" + if [ "$(printf '%s\n' "$metrics" | awk -F '\t' '{ print $1 }')" != "$expected_processes" ]; then + tree_status="raced" + fi + if [ "$detail" = "full" ]; then + smaps_metrics="$(collect_linux_smaps_rollup "$pids")" + observed_smaps="$(printf '%s\n' "$smaps_metrics" | awk -F '\t' '{ print $9 }')" + smaps_status="ok" + else + smaps_metrics="$empty_smaps" + observed_smaps=0 + smaps_status="disabled" + fi + snapshot_after="" + if ! snapshot_after="$(collect_process_tree_snapshot "$root_pid" "$server_cgroup_dir")" || + [ "$snapshot_before" != "$snapshot_after" ]; then + tree_status="raced" + fi + phase_after="$(tr -d '[:space:]' <"$phase_file" 2>/dev/null || true)" + [ -n "$phase_after" ] || phase_after="unknown" + if [ "$phase_after" != "$phase" ]; then + if [ "$phase_transition_retries" -lt "$phase_transition_retry_limit" ]; then + phase_transition_retries=$((phase_transition_retries + 1)) + printf 'resource sample phase changed during capture; retry=%s/%s target=%s from=%s to=%s\n' \ + "$phase_transition_retries" "$phase_transition_retry_limit" \ + "$target" "$phase" "$phase_after" >&2 + continue + fi + printf 'resource sample phase remained unstable after %s retries; target=%s from=%s to=%s\n' \ + "$phase_transition_retry_limit" "$target" "$phase" \ + "$phase_after" >&2 + return 1 + fi + if [ "$tree_status" != "ok" ] && + [ "$process_tree_race_retries" -lt "$process_tree_race_retry_limit" ]; then + process_tree_race_retries=$((process_tree_race_retries + 1)) + printf 'resource sample process-tree race; retry=%s/%s target=%s phase=%s\n' \ + "$process_tree_race_retries" "$process_tree_race_retry_limit" \ + "$target" "$phase" >&2 + # A native PostgreSQL backend may enter or exit between the two tree + # snapshots. Discard every metric from that non-atomic attempt and + # retry immediately; only a stable capture, or an explicitly exhausted + # bounded retry, is eligible for a samples.tsv row. + continue + fi + process_tree_race_retries=0 + if [ "$tree_status" != "ok" ]; then + smaps_status="raced" + smaps_metrics="$empty_smaps" + host_fd_metrics="$(printf '\t0\t%s\traced' "$expected_processes")" + metrics=$'0\t0\t0\t0.0\t0' + pids="" + elif [ "$detail" = "full" ]; then + if [ "$host_kernel" != "Linux" ] || [ ! -d /proc ]; then + smaps_status="unsupported" + elif [ "$observed_smaps" != "$expected_processes" ]; then + smaps_status="unreadable" + fi + if [ "$smaps_status" != "ok" ]; then + smaps_metrics="$empty_smaps" + fi + fi + if [ -z "$cgroup_memory_max$cgroup_memory_high$cgroup_swap_max" ]; then + cgroup_status="disabled" + cgroup_metrics="$empty_cgroup" + cgroup_file_cache_metrics="$empty_cgroup_file_cache" + elif printf '%s\n' "$cgroup_metrics_all" | awk -F '\t' ' + BEGIN { + key[24] = "active_file" + key[25] = "inactive_file" + key[26] = "file_mapped" + key[27] = "workingset_refault_file" + key[28] = "workingset_activate_file" + key[29] = "workingset_restore_file" + key[30] = "pgscan" + key[31] = "pgsteal" + } + NF != 34 { exit 1 } + { + for (i = 1; i <= 23; i++) if ($i == "") exit 1 + for (i = 24; i <= 31; i++) + if ($i != "" && $i !~ /^[0-9]+$/) exit 1 + if ($32 == "complete") { + if ($33 != "none") exit 1 + for (i = 24; i <= 31; i++) if ($i == "") exit 1 + } else if ($32 == "partial") { + if ($33 == "" || $33 == "none") exit 1 + count = split($33, listed, ",") + for (i = 1; i <= count; i++) { + if (!(listed[i] ~ /^(active_file|inactive_file|file_mapped|workingset_refault_file|workingset_activate_file|workingset_restore_file|pgscan|pgsteal)$/) || + missing[listed[i]]++) exit 1 + } + for (i = 24; i <= 31; i++) + if (($i == "") != (key[i] in missing)) exit 1 + } else { + exit 1 + } + if ($34 !~ /^memory[.]events([.]local)?$/) exit 1 + } + '; then + cgroup_status="ok" + cgroup_metrics="$(printf '%s\n' "$cgroup_metrics_all" | cut -f 1-23)" + cgroup_file_cache_metrics="$(printf '%s\n' "$cgroup_metrics_all" | cut -f 24-34)" + else + cgroup_status="unavailable" + cgroup_metrics="$empty_cgroup" + cgroup_file_cache_metrics="$empty_cgroup_file_cache" + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$now" "$target" "$phase" "$root_pid" "$metrics" "$pids" "$smaps_metrics" \ + "$cgroup_metrics" "$host_fd_metrics" "$expected_processes" \ + "$observed_smaps" "$smaps_status" "$cgroup_status" \ + "$cgroup_file_cache_metrics" "$tree_status" \ + >>"$samples_tsv" + phase_transition_retries=0 + else + if [ "$process_tree_race_retries" -lt "$process_tree_race_retry_limit" ]; then + process_tree_race_retries=$((process_tree_race_retries + 1)) + printf 'resource sample empty process tree; retry=%s/%s target=%s phase=%s\n' \ + "$process_tree_race_retries" "$process_tree_race_retry_limit" \ + "$target" "$phase" >&2 + continue + fi + process_tree_race_retries=0 + if [ -z "$cgroup_memory_max$cgroup_memory_high$cgroup_swap_max" ]; then + cgroup_status="disabled" + else + cgroup_status="unavailable" + fi + printf '%s\t%s\t%s\t%s\t0\t0\t0\t0.0\t0\t\t%s\t%s\t\t0\t0\traced\t0\t0\traced\t%s\t%s\traced\n' \ + "$now" "$target" "$phase" "$root_pid" "$empty_smaps" "$empty_cgroup" \ + "$cgroup_status" "$empty_cgroup_file_cache" >>"$samples_tsv" + fi + [ -f "$stop_file" ] && break + sleep "$interval" + done +} + +summarize_resource_usage() { + local target="$1" + local samples_tsv="$2" + local out_tsv="$3" + awk -F '\t' -v target="$target" -v samples="$samples_tsv" ' + function observe_file_cache_counter(phase, name, value, timestamp, key) { + if (value == "") return + key = phase SUBSEP name + if (!counter_samples[key]) { + counter_first[key] = value + 0 + counter_first_ms[key] = timestamp + 0 + } else if ((value + 0) < counter_last[key]) { + malformed_cgroup = 1 + } + counter_last[key] = value + 0 + counter_last_ms[key] = timestamp + 0 + counter_samples[key] += 1 + } + function counter_summary_value(phase, name, kind, key, elapsed, delta) { + key = phase SUBSEP name + if (!cgroup_valid_samples[phase] || + counter_samples[key] != cgroup_valid_samples[phase]) return "" + delta = counter_last[key] - counter_first[key] + if (kind == "start") return sprintf("%.0f", counter_first[key]) + if (kind == "end") return sprintf("%.0f", counter_last[key]) + if (kind == "delta") return sprintf("%.0f", delta) + elapsed = counter_last_ms[key] - counter_first_ms[key] + if (kind == "rate" && elapsed > 0) + return sprintf("%.6f", delta * 1000.0 / elapsed) + return "" + } + function phase_file_cache_missing_keys(phase, result, item_index, name) { + ordered[1] = "active_file" + ordered[2] = "inactive_file" + ordered[3] = "file_mapped" + ordered[4] = "workingset_refault_file" + ordered[5] = "workingset_activate_file" + ordered[6] = "workingset_restore_file" + ordered[7] = "pgscan" + ordered[8] = "pgsteal" + result = "" + for (item_index = 1; item_index <= 8; item_index++) { + name = ordered[item_index] + if (phase_missing[phase SUBSEP name]) + result = result (result == "" ? "" : ",") name + } + return (result == "" ? "none" : result) + } + NR == 1 { next } + { + phase = $3 + if (phase == "") { + phase = "unknown" + } + host_fd_count = $47 + host_fd_observed = $48 + host_fd_expected = $49 + host_fd_status = $50 + smaps_expected = $51 + smaps_observed = $52 + smaps_status = $53 + cgroup_status = $54 + file_cache_status = $63 + file_cache_missing = $64 + memory_events_source = $65 + process_tree_status = $66 + if (process_tree_status == "ok") { + process_pid_text = $10 + sub(/[[:space:]]+$/, "", process_pid_text) + process_pid_count = split(process_pid_text, process_pids, /[[:space:]]+/) + if ($5 !~ /^[1-9][0-9]*$/ || + $6 !~ /^[0-9]+$/ || $7 !~ /^[0-9]+$/ || + $8 !~ /^[0-9]+([.][0-9]+)?$/ || $9 !~ /^[0-9]+$/ || + $10 !~ /^[0-9]+([[:space:]]+[0-9]+)*[[:space:]]*$/ || + process_pid_count != ($5 + 0) || + smaps_expected !~ /^[0-9]+$/ || + ($5 + 0) != (smaps_expected + 0)) { + malformed_process_tree = 1 + } + } else if (process_tree_status == "raced") { + if ($5 != "0" || $6 != "0" || $7 != "0" || + $8 !~ /^0([.]0+)?$/ || $9 != "0" || $10 != "") { + malformed_process_tree = 1 + } + } else { + malformed_process_tree = 1 + } + if (host_fd_status == "ok") { + if (host_fd_count !~ /^[0-9]+$/ || + host_fd_observed !~ /^[0-9]+$/ || + host_fd_expected !~ /^[0-9]+$/ || + (host_fd_expected + 0) < 1 || + (host_fd_observed + 0) != (host_fd_expected + 0)) { + malformed_host_fd = 1 + } else { + host_fd_valid_samples[phase] += 1 + if (!have_peak_host_fd[phase] || + (host_fd_count + 0) > peak_host_fd[phase]) { + peak_host_fd[phase] = host_fd_count + 0 + } + have_peak_host_fd[phase] = 1 + } + } else if (host_fd_status == "unsupported") { + if (host_fd_count != "" || host_fd_observed != "0" || + host_fd_expected !~ /^[0-9]+$/) { + malformed_host_fd = 1 + } + host_fd_unsupported_samples[phase] += 1 + } else if (host_fd_status == "unreadable") { + if (host_fd_count != "" || + host_fd_observed !~ /^[0-9]+$/ || + host_fd_expected !~ /^[0-9]+$/ || + (host_fd_expected + 0) < 1 || + (host_fd_observed + 0) >= (host_fd_expected + 0)) { + malformed_host_fd = 1 + } + host_fd_unreadable_samples[phase] += 1 + } else if (host_fd_status == "raced") { + if (host_fd_count != "" || + host_fd_observed !~ /^[0-9]+$/ || + host_fd_expected !~ /^[0-9]+$/ || + (host_fd_observed + 0) > (host_fd_expected + 0) || + ((host_fd_expected + 0) > 0 && + (host_fd_observed + 0) == (host_fd_expected + 0))) { + malformed_host_fd = 1 + } + host_fd_raced_samples[phase] += 1 + } else { + malformed_host_fd = 1 + } + smaps_fields_blank = 1 + smaps_fields_numeric = 1 + for (column = 11; column <= 23; column++) { + if ($column != "") { + smaps_fields_blank = 0 + } + if ($column !~ /^[0-9]+$/) { + smaps_fields_numeric = 0 + } + } + if (smaps_status == "ok") { + if (smaps_expected !~ /^[0-9]+$/ || + smaps_observed !~ /^[0-9]+$/ || + (smaps_expected + 0) < 1 || + (smaps_observed + 0) != (smaps_expected + 0) || + ($19 + 0) != (smaps_observed + 0) || + !smaps_fields_numeric) { + malformed_smaps = 1 + } else { + smaps_valid_samples[phase] += 1 + } + } else if (smaps_status == "unsupported" || smaps_status == "disabled") { + if (smaps_expected !~ /^[0-9]+$/ || + (smaps_expected + 0) < 1 || smaps_observed != "0" || + !smaps_fields_blank) { + malformed_smaps = 1 + } + if (smaps_status == "unsupported") { + smaps_unsupported_samples[phase] += 1 + } else { + smaps_disabled_samples[phase] += 1 + } + } else if (smaps_status == "unreadable") { + if (smaps_expected !~ /^[0-9]+$/ || + smaps_observed !~ /^[0-9]+$/ || + (smaps_expected + 0) < 1 || + (smaps_observed + 0) >= (smaps_expected + 0) || + !smaps_fields_blank) { + malformed_smaps = 1 + } + smaps_unreadable_samples[phase] += 1 + } else if (smaps_status == "raced") { + if (smaps_expected !~ /^[0-9]+$/ || + smaps_observed !~ /^[0-9]+$/ || + (smaps_observed + 0) > (smaps_expected + 0) || + !smaps_fields_blank) { + malformed_smaps = 1 + } + smaps_raced_samples[phase] += 1 + } else { + malformed_smaps = 1 + } + cgroup_fields_blank = 1 + cgroup_fields_complete = 1 + for (column = 24; column <= 46; column++) { + if ($column != "") { + cgroup_fields_blank = 0 + } else { + cgroup_fields_complete = 0 + } + } + file_cache_fields_blank = 1 + for (column = 55; column <= 65; column++) { + if ($column != "") file_cache_fields_blank = 0 + } + if (cgroup_status == "ok") { + if (!cgroup_fields_complete || $24 !~ /^\// || + $25 !~ /^[0-9]+$/ || $26 !~ /^[0-9]+$/ || + $27 !~ /^[0-9]+$/ || $28 !~ /^[0-9]+$/ || + $29 !~ /^[0-9]+$/ || $30 !~ /^[0-9]+$/ || + $31 !~ /^[0-9]+$/ || $32 !~ /^[0-9]+$/ || + $33 !~ /^[0-9]+$/ || + $34 !~ /^(max|[0-9]+)$/ || + $35 !~ /^(max|[0-9]+)$/ || + $36 !~ /^(max|[0-9]+)$/) { + malformed_cgroup = 1 + } + for (column = 37; column <= 46; column++) { + if ($column !~ /^[0-9]+$/) { + malformed_cgroup = 1 + } + } + optional_key[55] = "active_file" + optional_key[56] = "inactive_file" + optional_key[57] = "file_mapped" + optional_key[58] = "workingset_refault_file" + optional_key[59] = "workingset_activate_file" + optional_key[60] = "workingset_restore_file" + optional_key[61] = "pgscan" + optional_key[62] = "pgsteal" + for (column = 55; column <= 62; column++) + delete optional_missing[optional_key[column]] + for (column = 55; column <= 62; column++) { + if ($column != "" && $column !~ /^[0-9]+$/) malformed_cgroup = 1 + } + if (file_cache_status == "complete") { + if (file_cache_missing != "none") malformed_cgroup = 1 + for (column = 55; column <= 62; column++) + if ($column == "") malformed_cgroup = 1 + file_cache_complete_samples[phase] += 1 + } else if (file_cache_status == "partial") { + if (file_cache_missing == "" || file_cache_missing == "none") { + malformed_cgroup = 1 + } else { + optional_count = split(file_cache_missing, listed_optional, ",") + for (optional_index = 1; optional_index <= optional_count; optional_index++) { + optional_name = listed_optional[optional_index] + if (!(optional_name ~ /^(active_file|inactive_file|file_mapped|workingset_refault_file|workingset_activate_file|workingset_restore_file|pgscan|pgsteal)$/) || + optional_missing[optional_name]++) malformed_cgroup = 1 + phase_missing[phase SUBSEP optional_name] = 1 + } + for (column = 55; column <= 62; column++) + if (($column == "") != (optional_key[column] in optional_missing)) + malformed_cgroup = 1 + } + file_cache_partial_samples[phase] += 1 + } else { + malformed_cgroup = 1 + } + if (memory_events_source !~ /^memory[.]events([.]local)?$/) { + malformed_cgroup = 1 + } else if (whole_run_memory_events_source == "") { + whole_run_memory_events_source = memory_events_source + } else if (whole_run_memory_events_source != memory_events_source) { + malformed_cgroup = 1 + } + cgroup_valid_samples[phase] += 1 + } else if (cgroup_status == "disabled" || cgroup_status == "unavailable") { + if (!cgroup_fields_blank || !file_cache_fields_blank) { + malformed_cgroup = 1 + } + if (cgroup_status == "disabled") { + cgroup_disabled_samples[phase] += 1 + } else { + cgroup_unavailable_samples[phase] += 1 + } + } else { + malformed_cgroup = 1 + } + samples_count[phase] += 1 + if (process_tree_status == "ok") { + if (($6 + 0) > peak_rss[phase]) { + peak_rss[phase] = $6 + 0 + } + if (($7 + 0) > peak_vsz[phase]) { + peak_vsz[phase] = $7 + 0 + } + if (($8 + 0) > peak_cpu[phase]) { + peak_cpu[phase] = $8 + 0 + } + if (($5 + 0) > peak_processes[phase]) { + peak_processes[phase] = $5 + 0 + } + } + if (smaps_status == "ok" && ($11 + 0) > peak_pss[phase]) { + peak_pss[phase] = $11 + 0 + } + if (smaps_status == "ok" && ($12 + 0) > peak_pss_anon[phase]) { + peak_pss_anon[phase] = $12 + 0 + } + if (smaps_status == "ok" && ($13 + 0) > peak_pss_file[phase]) { + peak_pss_file[phase] = $13 + 0 + } + if (smaps_status == "ok" && ($14 + 0) > peak_pss_shmem[phase]) { + peak_pss_shmem[phase] = $14 + 0 + } + if (smaps_status == "ok" && ($15 + 0) > peak_private[phase]) { + peak_private[phase] = $15 + 0 + } + if (smaps_status == "ok" && ($16 + 0) > peak_shared[phase]) { + peak_shared[phase] = $16 + 0 + } + if (smaps_status == "ok" && ($17 + 0) > peak_anonymous[phase]) { + peak_anonymous[phase] = $17 + 0 + } + if (smaps_status == "ok" && ($18 + 0) > peak_swap[phase]) { + peak_swap[phase] = $18 + 0 + } + if (smaps_status == "ok") { + smaps_samples[phase] += 1 + } + if (smaps_status == "ok" && ($20 + 0) > peak_threads[phase]) { + peak_threads[phase] = $20 + 0 + } + if (smaps_status == "ok" && ($21 + 0) > peak_page_tables[phase]) { + peak_page_tables[phase] = $21 + 0 + } + if (smaps_status == "ok" && ($22 + 0) > peak_private_clean[phase]) { + peak_private_clean[phase] = $22 + 0 + } + if (smaps_status == "ok" && ($23 + 0) > peak_private_dirty[phase]) { + peak_private_dirty[phase] = $23 + 0 + } + if (cgroup_status == "ok" && $24 != "") { + whole_run_cgroup_path = $24 + } + if (cgroup_status == "ok" && ($25 + 0) > peak_cgroup_memory_current[phase]) { + peak_cgroup_memory_current[phase] = $25 + 0 + } + if (cgroup_status == "ok" && ($26 + 0) > whole_run_cgroup_memory_peak) { + whole_run_cgroup_memory_peak = $26 + 0 + } + if (cgroup_status == "ok" && ($27 + 0) > peak_cgroup_swap_current[phase]) { + peak_cgroup_swap_current[phase] = $27 + 0 + } + if (cgroup_status == "ok" && ($28 + 0) > whole_run_cgroup_swap_peak) { + whole_run_cgroup_swap_peak = $28 + 0 + } + if (cgroup_status == "ok" && ($29 + 0) > peak_cgroup_pids[phase]) { + peak_cgroup_pids[phase] = $29 + 0 + } + if (cgroup_status == "ok" && ($30 + 0) > whole_run_cgroup_event_high) { + whole_run_cgroup_event_high = $30 + 0 + } + if (cgroup_status == "ok" && ($31 + 0) > whole_run_cgroup_event_max) { + whole_run_cgroup_event_max = $31 + 0 + } + if (cgroup_status == "ok" && ($32 + 0) > whole_run_cgroup_event_oom) { + whole_run_cgroup_event_oom = $32 + 0 + } + if (cgroup_status == "ok" && ($33 + 0) > whole_run_cgroup_event_oom_kill) { + whole_run_cgroup_event_oom_kill = $33 + 0 + } + if (cgroup_status == "ok" && $34 != "") { + whole_run_cgroup_memory_max = $34 + } + if (cgroup_status == "ok" && $35 != "") { + whole_run_cgroup_memory_high = $35 + } + if (cgroup_status == "ok" && $36 != "") { + whole_run_cgroup_swap_max = $36 + } + if (cgroup_status == "ok" && ($37 + 0) > peak_cgroup_memory_stat_anon[phase]) { + peak_cgroup_memory_stat_anon[phase] = $37 + 0 + } + if (cgroup_status == "ok" && ($38 + 0) > peak_cgroup_memory_stat_file[phase]) { + peak_cgroup_memory_stat_file[phase] = $38 + 0 + } + if (cgroup_status == "ok" && ($39 + 0) > peak_cgroup_memory_stat_shmem[phase]) { + peak_cgroup_memory_stat_shmem[phase] = $39 + 0 + } + if (cgroup_status == "ok" && ($40 + 0) > peak_cgroup_memory_stat_kernel[phase]) { + peak_cgroup_memory_stat_kernel[phase] = $40 + 0 + } + if (cgroup_status == "ok" && ($41 + 0) > peak_cgroup_memory_stat_pagetables[phase]) { + peak_cgroup_memory_stat_pagetables[phase] = $41 + 0 + } + if (cgroup_status == "ok" && ($42 + 0) > peak_cgroup_memory_stat_slab[phase]) { + peak_cgroup_memory_stat_slab[phase] = $42 + 0 + } + if (cgroup_status == "ok" && ($43 + 0) > peak_cgroup_memory_stat_file_dirty[phase]) { + peak_cgroup_memory_stat_file_dirty[phase] = $43 + 0 + } + if (cgroup_status == "ok" && ($44 + 0) > peak_cgroup_memory_stat_file_writeback[phase]) { + peak_cgroup_memory_stat_file_writeback[phase] = $44 + 0 + } + if (cgroup_status == "ok" && ($45 + 0) > whole_run_cgroup_memory_pressure_some_total) { + whole_run_cgroup_memory_pressure_some_total = $45 + 0 + } + if (cgroup_status == "ok" && ($46 + 0) > whole_run_cgroup_memory_pressure_full_total) { + whole_run_cgroup_memory_pressure_full_total = $46 + 0 + } + if (cgroup_status == "ok") { + if ($1 !~ /^[0-9]+$/) { + malformed_cgroup = 1 + } else { + if (!have_file_cache_window[phase]) { + file_cache_first_ms[phase] = $1 + 0 + have_file_cache_window[phase] = 1 + } else if (($1 + 0) <= file_cache_last_ms[phase]) { + malformed_cgroup = 1 + } + file_cache_last_ms[phase] = $1 + 0 + if ($55 != "" && (!have_peak_active_file[phase] || + ($55 + 0) > peak_active_file[phase])) { + peak_active_file[phase] = $55 + 0 + have_peak_active_file[phase] = 1 + } + if ($56 != "" && (!have_peak_inactive_file[phase] || + ($56 + 0) > peak_inactive_file[phase])) { + peak_inactive_file[phase] = $56 + 0 + have_peak_inactive_file[phase] = 1 + } + if ($57 != "" && (!have_peak_file_mapped[phase] || + ($57 + 0) > peak_file_mapped[phase])) { + peak_file_mapped[phase] = $57 + 0 + have_peak_file_mapped[phase] = 1 + } + observe_file_cache_counter(phase, "workingset_refault_file", $58, $1) + observe_file_cache_counter(phase, "workingset_activate_file", $59, $1) + observe_file_cache_counter(phase, "workingset_restore_file", $60, $1) + observe_file_cache_counter(phase, "pgscan", $61, $1) + observe_file_cache_counter(phase, "pgsteal", $62, $1) + } + } + } + END { + if (malformed_process_tree || malformed_host_fd || malformed_smaps || + malformed_cgroup) { + exit 1 + } + for (phase in samples_count) { + active_file_peak = (have_peak_active_file[phase] ? + sprintf("%.0f", peak_active_file[phase]) : "") + active_file_peak_mb = (have_peak_active_file[phase] ? + sprintf("%.3f", peak_active_file[phase] / 1048576.0) : "") + inactive_file_peak = (have_peak_inactive_file[phase] ? + sprintf("%.0f", peak_inactive_file[phase]) : "") + inactive_file_peak_mb = (have_peak_inactive_file[phase] ? + sprintf("%.3f", peak_inactive_file[phase] / 1048576.0) : "") + file_mapped_peak = (have_peak_file_mapped[phase] ? + sprintf("%.0f", peak_file_mapped[phase]) : "") + file_mapped_peak_mb = (have_peak_file_mapped[phase] ? + sprintf("%.3f", peak_file_mapped[phase] / 1048576.0) : "") + if (!cgroup_valid_samples[phase]) { + phase_file_cache_status = "unavailable" + phase_file_cache_missing = "not-sampled" + phase_events_source = "unavailable" + phase_file_cache_elapsed_ms = "" + } else { + phase_file_cache_status = (file_cache_partial_samples[phase] ? + "partial" : "complete") + phase_file_cache_missing = phase_file_cache_missing_keys(phase) + phase_events_source = whole_run_memory_events_source + phase_file_cache_elapsed_ms = sprintf("%.0f", + file_cache_last_ms[phase] - file_cache_first_ms[phase]) + } + printf "%s\t%s\t%d\t%.3f\t%d\t%.1f\t%d\t%d\t%s\t%d\t%.3f\t%d\t%.3f\t%d\t%.3f\t%d\t%.3f\t%d\t%.3f\t%d\t%.3f\t%d\t%.3f\t%d\t%d\t%d\t%d\t%.3f\t%d\t%.3f\t%d\t%.3f\t%s\t%.0f\t%.3f\t%.0f\t%.3f\t%.0f\t%.3f\t%.0f\t%.3f\t%.0f\t%.0f\t%.0f\t%.0f\t%.0f\t%s\t%s\t%s", + target, + phase, + peak_rss[phase], + peak_rss[phase] / 1024.0, + peak_vsz[phase], + peak_cpu[phase], + peak_processes[phase], + samples_count[phase], + samples, + peak_pss[phase], + peak_pss[phase] / 1024.0, + peak_pss_anon[phase], + peak_pss_anon[phase] / 1024.0, + peak_pss_file[phase], + peak_pss_file[phase] / 1024.0, + peak_pss_shmem[phase], + peak_pss_shmem[phase] / 1024.0, + peak_private[phase], + peak_private[phase] / 1024.0, + peak_shared[phase], + peak_shared[phase] / 1024.0, + peak_anonymous[phase], + peak_anonymous[phase] / 1024.0, + peak_swap[phase], + smaps_samples[phase], + peak_threads[phase], + peak_page_tables[phase], + peak_page_tables[phase] / 1024.0, + peak_private_clean[phase], + peak_private_clean[phase] / 1024.0, + peak_private_dirty[phase], + peak_private_dirty[phase] / 1024.0, + whole_run_cgroup_path, + peak_cgroup_memory_current[phase], + peak_cgroup_memory_current[phase] / 1048576.0, + whole_run_cgroup_memory_peak, + whole_run_cgroup_memory_peak / 1048576.0, + peak_cgroup_swap_current[phase], + peak_cgroup_swap_current[phase] / 1048576.0, + whole_run_cgroup_swap_peak, + whole_run_cgroup_swap_peak / 1048576.0, + peak_cgroup_pids[phase], + whole_run_cgroup_event_high, + whole_run_cgroup_event_max, + whole_run_cgroup_event_oom, + whole_run_cgroup_event_oom_kill, + whole_run_cgroup_memory_max, + whole_run_cgroup_memory_high, + whole_run_cgroup_swap_max + printf "\t%.0f\t%.3f\t%.0f\t%.3f\t%.0f\t%.3f\t%.0f\t%.3f\t%.0f\t%.3f\t%.0f\t%.3f\t%.0f\t%.3f\t%.0f\t%.3f\t%.0f\t%.0f", + peak_cgroup_memory_stat_anon[phase], + peak_cgroup_memory_stat_anon[phase] / 1048576.0, + peak_cgroup_memory_stat_file[phase], + peak_cgroup_memory_stat_file[phase] / 1048576.0, + peak_cgroup_memory_stat_shmem[phase], + peak_cgroup_memory_stat_shmem[phase] / 1048576.0, + peak_cgroup_memory_stat_kernel[phase], + peak_cgroup_memory_stat_kernel[phase] / 1048576.0, + peak_cgroup_memory_stat_pagetables[phase], + peak_cgroup_memory_stat_pagetables[phase] / 1048576.0, + peak_cgroup_memory_stat_slab[phase], + peak_cgroup_memory_stat_slab[phase] / 1048576.0, + peak_cgroup_memory_stat_file_dirty[phase], + peak_cgroup_memory_stat_file_dirty[phase] / 1048576.0, + peak_cgroup_memory_stat_file_writeback[phase], + peak_cgroup_memory_stat_file_writeback[phase] / 1048576.0, + whole_run_cgroup_memory_pressure_some_total, + whole_run_cgroup_memory_pressure_full_total + printf "\t%s\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d", + (have_peak_host_fd[phase] ? sprintf("%.0f", peak_host_fd[phase]) : ""), + host_fd_valid_samples[phase], + host_fd_unsupported_samples[phase], + host_fd_unreadable_samples[phase], + host_fd_raced_samples[phase], + smaps_valid_samples[phase], + smaps_unsupported_samples[phase], + smaps_disabled_samples[phase], + smaps_unreadable_samples[phase], + smaps_raced_samples[phase], + cgroup_valid_samples[phase], + cgroup_disabled_samples[phase], + cgroup_unavailable_samples[phase] + printf "\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s", + active_file_peak, + active_file_peak_mb, + inactive_file_peak, + inactive_file_peak_mb, + file_mapped_peak, + file_mapped_peak_mb, + phase_file_cache_status, + phase_file_cache_missing, + phase_events_source, + phase_file_cache_elapsed_ms + printf "\t%s\t%s\t%s\t%s", + counter_summary_value(phase, "workingset_refault_file", "start"), + counter_summary_value(phase, "workingset_refault_file", "end"), + counter_summary_value(phase, "workingset_refault_file", "delta"), + counter_summary_value(phase, "workingset_refault_file", "rate") + printf "\t%s\t%s\t%s\t%s", + counter_summary_value(phase, "workingset_activate_file", "start"), + counter_summary_value(phase, "workingset_activate_file", "end"), + counter_summary_value(phase, "workingset_activate_file", "delta"), + counter_summary_value(phase, "workingset_activate_file", "rate") + printf "\t%s\t%s\t%s\t%s", + counter_summary_value(phase, "workingset_restore_file", "start"), + counter_summary_value(phase, "workingset_restore_file", "end"), + counter_summary_value(phase, "workingset_restore_file", "delta"), + counter_summary_value(phase, "workingset_restore_file", "rate") + printf "\t%s\t%s\t%s\t%s", + counter_summary_value(phase, "pgscan", "start"), + counter_summary_value(phase, "pgscan", "end"), + counter_summary_value(phase, "pgscan", "delta"), + counter_summary_value(phase, "pgscan", "rate") + printf "\t%s\t%s\t%s\t%s\n", + counter_summary_value(phase, "pgsteal", "start"), + counter_summary_value(phase, "pgsteal", "end"), + counter_summary_value(phase, "pgsteal", "delta"), + counter_summary_value(phase, "pgsteal", "rate") + } + } + ' "$samples_tsv" | sort >>"$out_tsv" +} + +summarize_resource_usage_checked() { + local target="$1" + local samples_tsv="$2" + local out_tsv="$3" + + if ! summarize_resource_usage "$target" "$samples_tsv" "$out_tsv"; then + printf 'resource evidence summary validation failed for %s; see %s\n' \ + "$target" "$samples_tsv" >&2 + return 1 + fi +} + +sample_pg_wait_events() { + local target="$1" + local workload="$2" + local conn="$3" + local samples_tsv="$4" + local interval="$5" + local sampler_sql="${samples_tsv%.tsv}.sql" + local sampler_stderr="${samples_tsv%.tsv}.stderr.log" + + printf 'unix_ms\ttarget\tworkload\tpid\twait_event_type\twait_event\tstate\tbackend_type\tbackend_xid\tblocking_pids\textend_lock_modes\textend_lock_granted\textend_lock_waitstart\textend_lock_relations\n' >"$samples_tsv" + { + printf 'WITH sampled_at AS MATERIALIZED (\n' + printf ' SELECT floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint AS unix_ms\n' + printf ')\n' + printf 'SELECT\n' + printf ' sampled_at.unix_ms,\n' + printf " :'sample_target',\n" + printf " :'sample_workload',\n" + printf ' pid,\n' + printf " coalesce(wait_event_type, ''),\n" + printf " coalesce(wait_event, ''),\n" + printf " coalesce(state, ''),\n" + printf " coalesce(backend_type, ''),\n" + printf " coalesce(backend_xid::text, ''),\n" + printf " array_to_string(pg_blocking_pids(activity.pid), ','),\n" + printf " coalesce(extension_lock.modes, ''),\n" + printf " coalesce(extension_lock.granted, ''),\n" + printf " coalesce(extension_lock.waitstart, ''),\n" + printf " coalesce(extension_lock.relations, '')\n" + printf 'FROM pg_stat_activity AS activity\n' + printf 'CROSS JOIN sampled_at\n' + printf 'LEFT JOIN LATERAL (\n' + printf ' SELECT\n' + printf " string_agg(held_lock.mode, ',' ORDER BY held_lock.mode) AS modes,\n" + printf " string_agg(held_lock.granted::text, ',' ORDER BY held_lock.mode) AS granted,\n" + printf " min(held_lock.waitstart)::text AS waitstart,\n" + printf " string_agg(coalesce(held_lock.relation::regclass::text, held_lock.relation::text), ',' ORDER BY held_lock.relation) AS relations\n" + printf ' FROM pg_locks AS held_lock\n' + printf " WHERE held_lock.pid = activity.pid AND held_lock.locktype = 'extend'\n" + printf ') AS extension_lock ON true\n' + printf 'WHERE activity.pid <> pg_backend_pid()\n' + printf 'ORDER BY activity.backend_type, activity.pid;\n' + printf '\\watch %s\n' "$interval" + } >"$sampler_sql" + + exec "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -XAtq -F $'\t' \ + -v ON_ERROR_STOP=1 \ + -v "sample_target=$target" \ + -v "sample_workload=$workload" \ + -f "$sampler_sql" >>"$samples_tsv" 2>"$sampler_stderr" +} + +summarize_pg_wait_events() { + local samples_tsv="$1" + local out_tsv="$2" + + { + printf "wait_event_type\twait_event\tstate\tbackend_type\tsample_rows\tbackend_observations\tmax_count\n" + awk -F '\t' ' + NR == 1 { next } + { + key = $5 "\t" $6 "\t" $7 "\t" $8 + sample_key = $1 SUBSEP key + observations[key] += 1 + if (!seen_sample[sample_key]++) { + samples[key] += 1 + } + per_sample[sample_key] += 1 + if (per_sample[sample_key] > max_count[key]) { + max_count[key] = per_sample[sample_key] + } + } + END { + for (key in observations) { + printf "%s\t%d\t%d\t%d\n", key, samples[key], observations[key], max_count[key] + } + } + ' "$samples_tsv" | sort -t $'\t' -k6,6nr + } >"$out_tsv" +} + +capture_relation_footprint() { + local conn="$1" + local workload="$2" + local out_tsv="$3" + + fresh_run_process_group_timeout "$timeout_seconds" -- \ + "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -XAtq -F $'\t' -v ON_ERROR_STOP=1 -c " + SELECT + current_setting('data_directory') AS data_directory, + n.nspname, + c.relname, + c.relkind, + pg_relation_filepath(c.oid), + pg_relation_size(c.oid)::bigint, + pg_total_relation_size(c.oid)::bigint + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = 'public' + AND c.relname LIKE 'cqb_%' + ORDER BY c.relname, c.relkind + " >"$out_tsv.tmp" 2>"$out_tsv.stderr" || true + { + printf 'workload\tdata_directory\tschema\trelation\trelkind\tpath\trelation_bytes\ttotal_bytes\n' + awk -F '\t' -v workload="$workload" '{ print workload "\t" $0 }' "$out_tsv.tmp" + } >"$out_tsv" + rm -f "$out_tsv.tmp" +} + +capture_checkpoint_state() { + local conn="$1" + local out_tsv="$2" + local capture="$out_tsv.capture" + local row + + if ! fresh_run_process_group_timeout "$timeout_seconds" -- \ + "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -XAtq -F $'\t' -v ON_ERROR_STOP=1 -c " + SELECT + checkpointer.num_timed::text, + checkpointer.num_requested::text, + checkpointer.num_done::text, + wal.wal_bytes::text + FROM pg_stat_checkpointer AS checkpointer + CROSS JOIN pg_stat_wal AS wal + " >"$capture" 2>"$out_tsv.stderr"; then + rm -f "$capture" + return 1 + fi + row="$(<"$capture")" + rm -f "$capture" + case "$row" in + *$'\n'*) + printf 'checkpoint state returned more than one row\n' >&2 + return 1 + ;; + esac + if ! awk -F '\t' ' + NF == 4 && $1 ~ /^[0-9]+$/ && $2 ~ /^[0-9]+$/ && + $3 ~ /^[0-9]+$/ && $4 ~ /^[0-9]+$/ { valid = 1 } + END { exit !valid } + ' <<<"$row"; then + printf 'invalid checkpoint state row: %s\n' "$row" >&2 + return 1 + fi + printf '%s\n' "$row" >"$out_tsv" +} + +capture_pg_stat_io() { + local conn="$1" + local out_csv="$2" + + fresh_run_process_group_timeout "$timeout_seconds" -- \ + "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -X --csv -v ON_ERROR_STOP=1 -c " + SELECT + backend_type, + object, + context, + reads, + read_bytes, + read_time, + writes, + write_bytes, + write_time, + writebacks, + writeback_time, + extends, + extend_bytes, + extend_time, + hits, + evictions, + reuses, + fsyncs, + fsync_time, + stats_reset + FROM pg_stat_io + ORDER BY backend_type, object, context + " >"$out_csv" 2>"$out_csv.stderr" +} + +capture_checkpoint_settings() { + local conn="$1" + local out_tsv="$2" + local name names_sql="" separator="" seen_names=" " + + for name in \ + autovacuum_worker_slots \ + backend_flush_after \ + bgwriter_flush_after \ + checkpoint_flush_after \ + checkpoint_timeout \ + io_method \ + max_wal_size \ + min_wal_size \ + max_connections \ + max_wal_senders \ + max_worker_processes \ + shared_buffers \ + wal_segment_size \ + fsync \ + synchronous_commit \ + full_page_writes \ + "${explicit_postgres_guc_names[@]}"; do + case "$seen_names" in + *" $name "*) continue ;; + esac + seen_names+="$name " + names_sql+="$separator'$name'" + separator=, + done + + { + printf 'name\tsetting\tunit\tsource\n' + fresh_run_process_group_timeout "$timeout_seconds" -- \ + "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -XAtq -F $'\t' -v ON_ERROR_STOP=1 -c " + SELECT name, setting, coalesce(unit, ''), source + FROM pg_settings + WHERE name IN ($names_sql) + ORDER BY name + " + } >"$out_tsv" 2>"$out_tsv.stderr" +} + +validate_controlled_checkpoint_settings() { + local conn="$1" + local validation_log="$2" + local result + + if ! fresh_run_process_group_timeout "$timeout_seconds" -- \ + "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -XAtq -v ON_ERROR_STOP=1 -c " + SELECT + current_setting('checkpoint_timeout')::interval = interval '1 hour' + AND pg_size_bytes(current_setting('max_wal_size')) = 8589934592 + AND pg_size_bytes(current_setting('min_wal_size')) = 1073741824 + AND current_setting('fsync') = 'on' + AND current_setting('synchronous_commit') = 'on' + AND current_setting('full_page_writes') = 'on' + " >"$validation_log" 2>"$validation_log.stderr"; then + return 1 + fi + result="$(tr -d '[:space:]' <"$validation_log")" + [ "$result" = "t" ] || { + printf 'controlled checkpoint settings were not applied by PostgreSQL\n' >&2 + return 1 + } +} + +prepare_fanout_checkpoint_state() { + local conn="$1" + local workload_report_dir="$2" + local settings_tsv="$workload_report_dir/checkpoint-settings.tsv" + local checkpoint_log="$workload_report_dir/checkpoint-before.log" + local before_state="$workload_report_dir/checkpoint-before.tsv" + local before_io="$workload_report_dir/pg-stat-io-before.csv" + + capture_checkpoint_settings "$conn" "$settings_tsv" || return + if [ "$checkpoint_policy" = "controlled" ]; then + validate_controlled_checkpoint_settings \ + "$conn" "$workload_report_dir/checkpoint-settings-validation.log" || return + run_logged_timeout "$timeout_seconds" "$checkpoint_log" \ + "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -Xq -v ON_ERROR_STOP=1 -c CHECKPOINT || return + else + printf 'checkpoint policy is default; no pre-fanout CHECKPOINT was requested\n' \ + >"$checkpoint_log" + fi + capture_checkpoint_state "$conn" "$before_state" || return + capture_pg_stat_io "$conn" "$before_io" || return +} + +finish_fanout_checkpoint_state() { + local target="$1" + local workload="$2" + local conn="$3" + local workload_report_dir="$4" + local before_state="$workload_report_dir/checkpoint-before.tsv" + local after_state="$workload_report_dir/checkpoint-after.tsv" + local before_io="$workload_report_dir/pg-stat-io-before.csv" + local after_io="$workload_report_dir/pg-stat-io-after.csv" + local io_delta="$workload_report_dir/pg-stat-io-delta.tsv" + local io_delta_log="$workload_report_dir/pg-stat-io-delta.log" + local io_delta_status="passed" + local before_timed before_requested before_done before_wal + local after_timed after_requested after_done after_wal + local delta_timed delta_requested delta_done delta_wal checkpoint_status + + capture_checkpoint_state "$conn" "$after_state" || return + capture_pg_stat_io "$conn" "$after_io" || return + if ! python3 "$FRESH_ROOT/bin/delta-pg-stat-io.py" \ + "$before_io" "$after_io" "$io_delta" >"$io_delta_log" 2>&1; then + io_delta_status="failed" + fi + IFS=$'\t' read -r before_timed before_requested before_done before_wal <"$before_state" + IFS=$'\t' read -r after_timed after_requested after_done after_wal <"$after_state" + delta_timed=$((after_timed - before_timed)) + delta_requested=$((after_requested - before_requested)) + delta_done=$((after_done - before_done)) + delta_wal=$((after_wal - before_wal)) + checkpoint_status="observed" + if [ "$checkpoint_policy" = "controlled" ]; then + checkpoint_status="passed" + if [ "$delta_timed" -ne 0 ] || [ "$delta_requested" -ne 0 ] || + [ "$delta_done" -ne 0 ] || [ "$delta_wal" -lt 0 ] || + [ "$delta_wal" -gt "$checkpoint_wal_budget_bytes" ]; then + checkpoint_status="failed" + fi + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$workload" "$checkpoint_policy" "$checkpoint_status" \ + "$before_timed" "$after_timed" "$delta_timed" \ + "$before_requested" "$after_requested" "$delta_requested" \ + "$before_done" "$after_done" "$delta_done" \ + "$before_wal" "$after_wal" "$delta_wal" "$checkpoint_wal_budget_bytes" \ + "$before_state" "$after_state" "$before_io" "$after_io" \ + "$io_delta" "$io_delta_status" >>"$checkpoint_tsv" + + [ "$checkpoint_status" != "failed" ] && [ "$io_delta_status" = "passed" ] +} + +snapshot_memory_map() { + local target="$1" + local root_pid="$2" + local label="$3" + local out_dir="$4" + local snapshot_dir safe_label pids pid pid_csv + + [ "$memory_map_snapshots" -eq 1 ] || return 0 + if ! kill -0 "$root_pid" 2>/dev/null; then + return 0 + fi + + snapshot_dir="$out_dir/memory-maps" + mkdir -p "$snapshot_dir" + safe_label="$(printf '%s' "$label" | tr -c 'A-Za-z0-9._-' '_')" + pids="$(collect_process_tree "$root_pid" | sort -n | tr '\n' ' ')" + [ -n "$pids" ] || return 0 + + printf 'target=%s\nlabel=%s\nroot_pid=%s\npids=%s\n' \ + "$target" "$label" "$root_pid" "$pids" >"$snapshot_dir/$safe_label.summary.txt" + pid_csv="$(printf '%s\n' "$pids" | awk '{$1=$1; gsub(/ /, ","); print}')" + ps -o pid= -o ppid= -o rss= -o vsz= -o %cpu= -o command= -p "$pid_csv" \ + >>"$snapshot_dir/$safe_label.summary.txt" 2>&1 || true + + for pid in $pids; do + if [ -r "/proc/$pid/smaps" ]; then + cp "/proc/$pid/smaps" "$snapshot_dir/$safe_label.$pid.smaps.txt" 2>/dev/null || true + cp "/proc/$pid/smaps_rollup" "$snapshot_dir/$safe_label.$pid.smaps-rollup.txt" 2>/dev/null || true + cp "/proc/$pid/status" "$snapshot_dir/$safe_label.$pid.status.txt" 2>/dev/null || true + cp "/proc/$pid/numa_maps" "$snapshot_dir/$safe_label.$pid.numa-maps.txt" 2>/dev/null || true + if [ -s "$snapshot_dir/$safe_label.$pid.smaps.txt" ]; then + bash "$FRESH_ROOT/bin/summarize-linux-smaps.sh" \ + "$snapshot_dir/$safe_label.$pid.smaps.txt" \ + "$snapshot_dir/$safe_label.$pid.smaps-mappings.tsv" \ + "$snapshot_dir/$safe_label.$pid.smaps-categories.tsv" || true + fi + fi + if command -v vmmap >/dev/null 2>&1; then + vmmap -summary "$pid" >"$snapshot_dir/$safe_label.$pid.vmmap.txt" 2>&1 || true + elif command -v pmap >/dev/null 2>&1; then + pmap -x "$pid" >"$snapshot_dir/$safe_label.$pid.pmap.txt" 2>&1 || true + fi + done +} + +capture_measurement_tool_closure() { + local closure_id="${FRESH_MEASUREMENT_TOOL_CLOSURE_ID:-}" + local closure_manifest="${FRESH_MEASUREMENT_TOOL_CLOSURE_MANIFEST:-}" + local closure_manifest_sha256="${FRESH_MEASUREMENT_TOOL_CLOSURE_MANIFEST_SHA256:-}" + local verifier="$FRESH_ROOT/bin/run-frozen-measurement.py" + local verifier_sha256="none" + local copied_manifest_sha256="none" + + if [ -z "$closure_id$closure_manifest$closure_manifest_sha256" ]; then + [ "$require_frozen_measurement_tools" -eq 0 ] || { + echo 'frozen measurement-tool closure is required but absent' >&2 + return 2 + } + else + [ -n "$closure_id" ] && [ -n "$closure_manifest" ] && \ + [ -n "$closure_manifest_sha256" ] || { + echo 'partial frozen measurement-tool closure environment' >&2 + return 2 + } + fresh_is_sha256 "$closure_id" && \ + fresh_is_sha256 "$closure_manifest_sha256" || { + echo 'invalid frozen measurement-tool closure identity' >&2 + return 2 + } + [ -f "$verifier" ] && [ ! -L "$verifier" ] && [ -x "$verifier" ] || { + printf 'missing frozen measurement-tool verifier: %s\n' "$verifier" >&2 + return 2 + } + [ "$closure_manifest" = "$FRESH_ROOT/.measurement-tool-closure.tsv" ] || { + echo 'frozen measurement-tool manifest is outside FRESH_ROOT' >&2 + return 2 + } + "$verifier" verify \ + --root "$FRESH_ROOT" \ + --manifest "$closure_manifest" \ + --identity "$closure_id" \ + --manifest-sha256 "$closure_manifest_sha256" >/dev/null || return + cp -p -- "$closure_manifest" "$measurement_tool_closure_files_tsv" + chmod 0444 "$measurement_tool_closure_files_tsv" + copied_manifest_sha256="$( + fresh_wasmer_bin_hash "$measurement_tool_closure_files_tsv" + )" || return + [ "$copied_manifest_sha256" = "$closure_manifest_sha256" ] || { + echo 'captured measurement-tool manifest changed while copying' >&2 + return 125 + } + verifier_sha256="$(fresh_wasmer_bin_hash "$verifier")" || return + measurement_tool_closure_mode="content-addressed-read-only" + measurement_tool_closure_identity="$closure_id" + measurement_tool_closure_manifest_sha256="$closure_manifest_sha256" + fi + + printf 'schema_version\tmode\trequired\tclosure_identity\tmanifest_sha256\tcaptured_manifest_sha256\tverifier_sha256\n' \ + >"$measurement_tool_closure_tsv" + printf 'oliphaunt.wasix-postmaster.measurement-tool-evidence.v1\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$measurement_tool_closure_mode" \ + "$require_frozen_measurement_tools" \ + "$measurement_tool_closure_identity" \ + "$measurement_tool_closure_manifest_sha256" \ + "$copied_manifest_sha256" \ + "$verifier_sha256" >>"$measurement_tool_closure_tsv" + measurement_tool_closure_evidence_identity="$( + fresh_wasmer_bin_hash "$measurement_tool_closure_tsv" + )" || return + chmod 0444 "$measurement_tool_closure_tsv" +} + +assert_frozen_measurement_tool_closure() { + local actual_evidence_identity actual_manifest_sha256 + + actual_evidence_identity="$( + fresh_wasmer_bin_hash "$measurement_tool_closure_tsv" + )" || return + [ "$actual_evidence_identity" = \ + "$measurement_tool_closure_evidence_identity" ] || { + echo 'measurement-tool closure evidence changed during benchmark' >&2 + return 125 + } + [ "$measurement_tool_closure_mode" = content-addressed-read-only ] || \ + return 0 + actual_manifest_sha256="$( + fresh_wasmer_bin_hash "$measurement_tool_closure_files_tsv" + )" || return + [ "$actual_manifest_sha256" = \ + "$measurement_tool_closure_manifest_sha256" ] || { + echo 'captured measurement-tool manifest changed during benchmark' >&2 + return 125 + } + "$FRESH_ROOT/bin/run-frozen-measurement.py" verify \ + --root "$FRESH_ROOT" \ + --manifest "$FRESH_MEASUREMENT_TOOL_CLOSURE_MANIFEST" \ + --identity "$measurement_tool_closure_identity" \ + --manifest-sha256 "$measurement_tool_closure_manifest_sha256" >/dev/null +} + +suite_root="$RUN_DIR/concurrent-query-suite/$run_label" +report_dir="$REPORT_DIR/concurrent-query-suite/$run_label" +summary="$report_dir/summary.md" +summary_tsv="$report_dir/summary.tsv" +client_tsv="$report_dir/client-summary.tsv" +resource_tsv="$report_dir/resource-summary.tsv" +checkpoint_tsv="$report_dir/checkpoint-summary.tsv" +libpq_latency_tsv="$report_dir/libpq-latency-summary.tsv" +host_fd_checkpoints_tsv="$report_dir/host-fd-checkpoints.tsv" +host_fd_churn_tsv="$report_dir/host-fd-churn-summary.tsv" +server_limits_tsv="$report_dir/server-limits.tsv" +server_lifecycle_tsv="$report_dir/server-lifecycle.tsv" +memory_evidence_tsv="$report_dir/memory-evidence.tsv" +memory_budget_tsv="$report_dir/memory-budget.tsv" +lifecycle_plateau_tsv="$report_dir/wasix-runtime-plateau.tsv" +lifecycle_memory_checkpoints_tsv="$report_dir/wasix-lifecycle-memory-checkpoints.tsv" +lifecycle_memory_plateau_tsv="$report_dir/wasix-lifecycle-memory-plateau.tsv" +lifecycle_baseline_policy_tsv="$report_dir/wasix-lifecycle-baseline-policy.tsv" +lifecycle_baseline_binding_tsv="$report_dir/wasix-lifecycle-baseline-binding.tsv" +execution_identity_tsv="$report_dir/execution-identity.tsv" +sealed_executor_selection_tsv="$report_dir/sealed-executor-selection.tsv" +execution_identity_sha256="" +measurement_tool_closure_tsv="$report_dir/measurement-tool-closure.tsv" +measurement_tool_closure_files_tsv="$report_dir/measurement-tool-closure.files.tsv" +measurement_tool_closure_mode="unfrozen" +measurement_tool_closure_identity="none" +measurement_tool_closure_manifest_sha256="none" +measurement_tool_closure_evidence_identity="" +execution_bound_carrier_identity="" +execution_bound_manifest_identity="" +execution_bound_receipt_identity="" +execution_bound_payload_identity="" +execution_bound_headless_identity="" +execution_bound_wasmer_identity="" +execution_bound_postgres_module_identity="" +execution_bound_profile_identity="" +lifecycle_baseline_policy_identity="" +lifecycle_baseline_binding_identity="" +lifecycle_baseline_policy_id="" +lifecycle_baseline_policy_status="" +lifecycle_baseline_claim_scope="" +lifecycle_baseline_assumption="" +lifecycle_bound_wasmer_identity="" +lifecycle_bound_postgres_module_identity="" +lifecycle_bound_runtime_footprint_identity="" +lifecycle_bound_durability_identity="" +lifecycle_bound_profile_resolution_identity="" +lifecycle_bound_carrier_manifest_identity="" +lifecycle_bound_carrier_receipt_identity="" +lifecycle_bound_carrier_inventory_identity="" +instrumentation_policy_tsv="$report_dir/instrumentation-policy.tsv" +adaptive_cache_evidence_policy_tsv="$report_dir/adaptive-cache-evidence-policy.tsv" +adaptive_cache_evidence_policy_identity="" +adaptive_cache_validator_sha256="" +adaptive_cache_bound_manifest_sha256="none" +sealed_loader_policy_tsv="$report_dir/sealed-loader-policy.tsv" +sealed_loader_policy_identity="" +cold_ownership_mode_tsv="$report_dir/cold-ownership-mode.tsv" +postgres_profile_inputs_tsv="$report_dir/postgres-profile-inputs.tsv" +postgres_profile_resolution_tsv="$report_dir/postgres-profile-resolution.tsv" +checkpoint_wal_budget_bytes=4294967296 + +if [ "$wasix_runtime_mode" = "sealed-headless" ]; then + canonical_suite_root="$(cd "$RUN_DIR" && pwd -P)/concurrent-query-suite/$run_label" + canonical_report_dir="$(cd "$REPORT_DIR" && pwd -P)/concurrent-query-suite/$run_label" + require_sealed_output_disjoint run "$canonical_suite_root" + require_sealed_output_disjoint report "$canonical_report_dir" +fi + +if [ -e "$suite_root" ] || [ -L "$suite_root" ] || [ -e "$report_dir" ] || [ -L "$report_dir" ]; then + if [ "$replace_existing" -ne 1 ]; then + printf 'benchmark label already exists; choose a new --label or pass --replace-existing: %s\n' \ + "$run_label" >&2 + exit 2 + fi + old_shared_memory_evidence="$report_dir/wasix/shared-memory-provider.json" + if [ -L "$old_shared_memory_evidence" ] || + { [ -e "$old_shared_memory_evidence" ] && + [ ! -f "$old_shared_memory_evidence" ]; }; then + printf 'refusing to replace a report with unsafe shared-memory provider evidence: %s\n' \ + "$old_shared_memory_evidence" >&2 + exit 2 + fi + if [ -f "$old_shared_memory_evidence" ] && + [ ! -L "$old_shared_memory_evidence" ]; then + old_shared_memory_identity="$( + python3 "$FRESH_ROOT/lib/shared_memory_provider.py" identify \ + --evidence "$old_shared_memory_evidence" + )" || { + echo 'refusing to replace a report with invalid shared-memory provider evidence' >&2 + exit 2 + } + IFS=$'\t' read -r old_shared_memory_provider old_shared_memory_root \ + old_shared_memory_evidence_sha256 <<<"$old_shared_memory_identity" + if [ "$old_shared_memory_provider" = linux-tmpfs-v1 ] && + { [ -e "$old_shared_memory_root" ] || [ -L "$old_shared_memory_root" ]; }; then + printf 'refusing to replace a report while its external shared-memory provider root still exists: %s (receipt %s)\n' \ + "$old_shared_memory_root" "$old_shared_memory_evidence_sha256" >&2 + exit 2 + fi + fi + rm -rf -- "$suite_root" "$report_dir" +fi +mkdir -p "$suite_root/sql" "$report_dir" +if [ "$wasix_runtime_mode" = sealed-headless ]; then + { + printf 'schema_version\texecutor_role\texecutor_receipt_path\texecutor_receipt_sha256\texecutor_sha256\n' + printf 'oliphaunt.wasix-postmaster.sealed-executor-selection.v1\t%s\t%s\t%s\t%s\n' \ + "$sealed_executor_role" "$sealed_executor_receipt_relative" \ + "$sealed_executor_receipt_hash" "$sealed_executor_hash" + } >"$sealed_executor_selection_tsv" + chmod 0444 "$sealed_executor_selection_tsv" +fi +capture_measurement_tool_closure || { + echo 'failed to capture measurement-tool closure' >&2 + exit 2 +} +if [ "$cold_ownership" -eq 0 ]; then + cold_ownership_mode=disabled +elif [ "$cold_ownership_workloads" -eq 1 ]; then + cold_ownership_mode=whole-lifecycle +else + cold_ownership_mode=startup-only +fi +printf 'schema_version\tmode\tworkloads\tcgroup_scope\n' >"$cold_ownership_mode_tsv" +printf 'oliphaunt.wasix-postmaster.cold-ownership-mode.v1\t%s\t%s\t%s\n' \ + "$cold_ownership_mode" \ + "$([ "${#workloads[@]}" -gt 0 ] && printf '%s' "${workloads[*]}" || printf none)" \ + "$([ "$cold_ownership" -eq 1 ] && printf single-server-whole-lifecycle || printf none)" \ + >>"$cold_ownership_mode_tsv" +chmod 0444 "$cold_ownership_mode_tsv" +printf 'schema_version\truntime_mode\trequire_zero_write_aot\tverification_scope\tactivation_policy\truntime_environment\taudit_environment\tenvironment_inheritance\tallowed_snapshot_modes\tmax_source_bytes_written\tmax_snapshot_bytes_written\tmax_sync_calls\tvalidator\tvalidator_sha256\timmutable_receipt_path\timmutable_receipt_sha256\timmutable_receipt_dev\timmutable_receipt_ino\tcarrier_closure_identity\tcore_profile\tguest_build_recipe_sha256\n' \ + >"$sealed_loader_policy_tsv" +printf 'oliphaunt.wasix-postmaster.sealed-loader-policy.v2\t%s\t%s\t%s\t%s\t%s\t%s\tsanitized-then-explicit\t%s\t0\t0\t0\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$wasix_runtime_mode" "$require_zero_write_aot" \ + "$immutable_carrier_verification_scope" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf direct-immutable-only || printf compatibility)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf 'OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1' || printf unset)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf owned-per-target-jsonl || printf disabled)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf direct-immutable-inode || printf unrestricted)" \ + "$FRESH_ROOT/bin/validate-sealed-loader-audit.py" \ + "$(fresh_wasmer_bin_hash "$FRESH_ROOT/bin/validate-sealed-loader-audit.py")" \ + "${immutable_carrier_receipt:-none}" "$immutable_carrier_receipt_sha256" \ + "$immutable_carrier_receipt_dev" "$immutable_carrier_receipt_ino" \ + "$immutable_carrier_closure_identity" "$immutable_carrier_core_profile" \ + "$immutable_carrier_guest_build_recipe_sha256" >>"$sealed_loader_policy_tsv" +sealed_loader_policy_identity="$(fresh_wasmer_bin_hash "$sealed_loader_policy_tsv")" +chmod 0444 "$sealed_loader_policy_tsv" +adaptive_cache_validator="$FRESH_ROOT/bin/validate-adaptive-file-cache-telemetry.py" +adaptive_cache_validator_sha256="$(fresh_wasmer_bin_hash "$adaptive_cache_validator")" +adaptive_cache_bound_manifest_sha256="${sealed_manifest_hash:-none}" +case "$adaptive_cache_evidence_policy" in + portable-correctness-v1) + adaptive_cache_claim_scope=portable-correctness + adaptive_cache_required_host=any + adaptive_cache_required_runtime_mode=any + adaptive_cache_required_outcome=adaptive-active-or-observe-only-fallback + adaptive_cache_required_class=none + adaptive_cache_min_class_offers=0 + adaptive_cache_min_class_advice_calls=0 + adaptive_cache_min_class_advised_bytes=0 + adaptive_cache_max_sample_errors=unbounded + adaptive_cache_max_clock_errors=unbounded + adaptive_cache_max_advice_errors=unbounded + adaptive_cache_max_psi_breaker_trips=unbounded + adaptive_cache_max_refault_breaker_trips=unbounded + adaptive_cache_max_deferred_wal_pin_errors=unbounded + adaptive_cache_max_contended_wal_pin_failures=unbounded + adaptive_cache_terminal_receipt=active-finalized-or-admission-fallback + adaptive_cache_sample_scope_contract=not-required + adaptive_cache_required_cgroup_binding=none + adaptive_cache_required_limit_binding=none + adaptive_cache_required_monotonic_window=none + ;; + constrained-linux-wal-action-v1) + adaptive_cache_claim_scope=constrained-linux-performance + adaptive_cache_required_host=Linux + adaptive_cache_required_runtime_mode=sealed-headless + adaptive_cache_required_outcome=adaptive-active + adaptive_cache_required_class=6 + adaptive_cache_min_class_offers=1 + adaptive_cache_min_class_advice_calls=1 + adaptive_cache_min_class_advised_bytes=1 + adaptive_cache_max_sample_errors=0 + adaptive_cache_max_clock_errors=0 + adaptive_cache_max_advice_errors=0 + adaptive_cache_max_psi_breaker_trips=0 + adaptive_cache_max_refault_breaker_trips=0 + adaptive_cache_max_deferred_wal_pin_errors=0 + adaptive_cache_max_contended_wal_pin_failures=0 + adaptive_cache_terminal_receipt=active-finalized + adaptive_cache_sample_scope_contract=required + adaptive_cache_required_cgroup_binding=per-target-device-inode + adaptive_cache_required_limit_binding=requested-equals-leaf-and-effective-min + adaptive_cache_required_monotonic_window=launch-before-through-post-shutdown + ;; +esac +printf 'schema_version\tacceptance_policy\tclaim_scope\trequired_host\tselected_host\trequired_runtime_mode\tselected_runtime_mode\tselected_memory_max\tselected_memory_high\tselected_swap_max\trequired_outcome\trequired_class\tmin_class_offers\tmin_class_advice_calls\tmin_class_advised_bytes\tmax_sample_errors\tmax_clock_errors\tmax_advice_errors\tmax_psi_breaker_trips\tmax_refault_breaker_trips\tmax_deferred_wal_pin_errors\tmax_contended_wal_pin_failures\tterminal_receipt\tvalidator_sha256\tsealed_manifest_sha256\tsample_scope_contract\trequired_cgroup_binding\trequired_limit_binding\trequired_monotonic_window\n' \ + >"$adaptive_cache_evidence_policy_tsv" +printf 'oliphaunt.wasix-postmaster.adaptive-cache-evidence-policy.v3\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$adaptive_cache_evidence_policy" "$adaptive_cache_claim_scope" \ + "$adaptive_cache_required_host" "$(uname -s)" \ + "$adaptive_cache_required_runtime_mode" "$wasix_runtime_mode" \ + "${cgroup_memory_max:-none}" "${cgroup_memory_high:-none}" \ + "${cgroup_swap_max:-none}" "$adaptive_cache_required_outcome" \ + "$adaptive_cache_required_class" "$adaptive_cache_min_class_offers" \ + "$adaptive_cache_min_class_advice_calls" \ + "$adaptive_cache_min_class_advised_bytes" \ + "$adaptive_cache_max_sample_errors" "$adaptive_cache_max_clock_errors" \ + "$adaptive_cache_max_advice_errors" \ + "$adaptive_cache_max_psi_breaker_trips" \ + "$adaptive_cache_max_refault_breaker_trips" \ + "$adaptive_cache_max_deferred_wal_pin_errors" \ + "$adaptive_cache_max_contended_wal_pin_failures" \ + "$adaptive_cache_terminal_receipt" "$adaptive_cache_validator_sha256" \ + "$adaptive_cache_bound_manifest_sha256" \ + "$adaptive_cache_sample_scope_contract" \ + "$adaptive_cache_required_cgroup_binding" \ + "$adaptive_cache_required_limit_binding" \ + "$adaptive_cache_required_monotonic_window" \ + >>"$adaptive_cache_evidence_policy_tsv" +adaptive_cache_evidence_policy_identity="$( + fresh_wasmer_bin_hash "$adaptive_cache_evidence_policy_tsv" +)" +chmod 0444 "$adaptive_cache_evidence_policy_tsv" +instrumentation_lane=benchmark +wait_dump_policy=prohibited +wait_dump_fence_protocol=none +if [ "$wasix_lifecycle_plateau" -eq 1 ]; then + lifecycle_policy_root="$(cd "$FRESH_ROOT/profiles/lifecycle-baselines" && pwd -P)" + instrumentation_lane=untimed-lifecycle-diagnostic + wait_dump_policy=fenced-only + wait_dump_fence_protocol=wasix-runtime-fence-v1+wasix-runtime-fence-commit-v1 + if [ ! -f "$lifecycle_baseline_policy_source" ] || + [ -L "$lifecycle_baseline_policy_source" ]; then + printf 'lifecycle baseline policy must be a regular non-symlink file: %s\n' \ + "$lifecycle_baseline_policy_source" >&2 + exit 2 + fi + lifecycle_baseline_policy_source="$(realpath "$lifecycle_baseline_policy_source")" || exit + case "$lifecycle_baseline_policy_source" in + "$lifecycle_policy_root"/*) ;; + *) + printf 'lifecycle baseline policy must be checked in below %s: %s\n' \ + "$lifecycle_policy_root" "$lifecycle_baseline_policy_source" >&2 + exit 2 + ;; + esac + cp -- "$lifecycle_baseline_policy_source" "$lifecycle_baseline_policy_tsv" + [ "$(sed -n '1p' "$lifecycle_baseline_policy_tsv")" = \ + $'schema_version\tpolicy_id\tpolicy_status\tclaim_scope\tbaseline_assumption\tfield\trule\tminimum\tmaximum' ] || { + echo 'lifecycle baseline policy has an unexpected ordered schema' >&2 + exit 2 + } + IFS=$'\t' read -r _ lifecycle_baseline_policy_id \ + lifecycle_baseline_policy_status lifecycle_baseline_claim_scope \ + lifecycle_baseline_assumption _ < <(sed -n '2p' "$lifecycle_baseline_policy_tsv") + awk -F '\t' -v id="$lifecycle_baseline_policy_id" \ + -v status="$lifecycle_baseline_policy_status" \ + -v scope="$lifecycle_baseline_claim_scope" \ + -v assumption="$lifecycle_baseline_assumption" ' + NR == 1 { next } + NF != 9 || $2 != id || $3 != status || $4 != scope || $5 != assumption { exit 1 } + END { exit(NR > 1 ? 0 : 1) } + ' "$lifecycle_baseline_policy_tsv" || { + echo 'lifecycle baseline policy metadata is empty or inconsistent' >&2 + exit 2 + } + lifecycle_baseline_policy_identity="$( + fresh_wasmer_bin_hash "$lifecycle_baseline_policy_tsv" + )" +fi +printf 'schema_version\tlane\twasix_perf_stats\twait_dump_policy\twait_dump_interval_ms\twait_dump_max_per_wait\twait_dump_verbose\tfence_protocol\tsanitized_environment\n' \ + >"$instrumentation_policy_tsv" +printf 'oliphaunt.wasix-postmaster.instrumentation.v1\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$instrumentation_lane" "$wasix_perf_stats" "$wait_dump_policy" \ + "$wasix_wait_dump_interval_ms" "$wasix_wait_dump_max_per_wait" \ + "$wasix_wait_dump_verbose" "$wait_dump_fence_protocol" \ + "${wait_dump_environment_names[*]}" >>"$instrumentation_policy_tsv" +instrumentation_policy_identity="$(fresh_sha256_stream <"$instrumentation_policy_tsv")" +chmod 0444 "$instrumentation_policy_tsv" + +assert_frozen_adaptive_cache_evidence_policy() { + local actual + + actual="$(fresh_wasmer_bin_hash "$instrumentation_policy_tsv")" || return + [ "$actual" = "$instrumentation_policy_identity" ] || { + echo 'instrumentation policy receipt changed during benchmark' >&2 + return 125 + } + actual="$(fresh_wasmer_bin_hash "$adaptive_cache_evidence_policy_tsv")" || return + [ "$actual" = "$adaptive_cache_evidence_policy_identity" ] || { + echo 'adaptive cache evidence policy receipt changed during benchmark' >&2 + return 125 + } + [ "$(fresh_wasmer_bin_hash "$adaptive_cache_validator")" = \ + "$adaptive_cache_validator_sha256" ] || { + echo 'adaptive cache evidence validator changed during benchmark' >&2 + return 125 + } + if [ "$wasix_runtime_mode" = sealed-headless ]; then + [ "$(fresh_wasmer_bin_hash "$sealed_manifest")" = \ + "$adaptive_cache_bound_manifest_sha256" ] || { + echo 'sealed manifest changed after adaptive cache policy binding' >&2 + return 125 + } + fi +} + +assert_frozen_lifecycle_baseline() { + local actual + + [ "$wasix_lifecycle_plateau" -eq 1 ] || return 0 + actual="$(fresh_wasmer_bin_hash "$lifecycle_baseline_policy_tsv")" || return + [ "$actual" = "$lifecycle_baseline_policy_identity" ] || { + printf 'lifecycle baseline policy changed during benchmark: expected=%s actual=%s path=%s\n' \ + "$lifecycle_baseline_policy_identity" "$actual" \ + "$lifecycle_baseline_policy_tsv" >&2 + return 125 + } + if [ -n "$lifecycle_baseline_binding_identity" ]; then + actual="$(fresh_wasmer_bin_hash "$lifecycle_baseline_binding_tsv")" || return + [ "$actual" = "$lifecycle_baseline_binding_identity" ] || { + printf 'lifecycle baseline binding changed during benchmark: expected=%s actual=%s path=%s\n' \ + "$lifecycle_baseline_binding_identity" "$actual" \ + "$lifecycle_baseline_binding_tsv" >&2 + return 125 + } + [ "$(fresh_wasmer_bin_hash "$wasmer_bin")" = \ + "$lifecycle_bound_wasmer_identity" ] || { + echo 'Wasmer binary changed after lifecycle baseline binding' >&2 + return 125 + } + [ "$(fresh_wasmer_bin_hash "$wasix_postgres_module")" = \ + "$lifecycle_bound_postgres_module_identity" ] || { + echo 'PostgreSQL guest module changed after lifecycle baseline binding' >&2 + return 125 + } + [ "${FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256:-none}" = \ + "$lifecycle_bound_runtime_footprint_identity" ] && + [ "${FRESH_POSTGRES_DURABILITY_SHA256:-none}" = \ + "$lifecycle_bound_durability_identity" ] && + [ "${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-none}" = \ + "$lifecycle_bound_profile_resolution_identity" ] || { + echo 'PostgreSQL profile identity changed after lifecycle baseline binding' >&2 + return 125 + } + if [ "$wasix_runtime_mode" = sealed-headless ]; then + fresh_verify_sealed_headless_carrier "$sealed_carrier_root" || return 125 + [ "$(fresh_wasmer_bin_hash "$sealed_manifest")" = \ + "$lifecycle_bound_carrier_manifest_identity" ] && + [ "$(fresh_wasmer_bin_hash "$sealed_receipt")" = \ + "$lifecycle_bound_carrier_receipt_identity" ] && + [ "$(fresh_wasmer_bin_hash "$sealed_payload_inventory")" = \ + "$lifecycle_bound_carrier_inventory_identity" ] || { + echo 'sealed carrier changed after lifecycle baseline binding' >&2 + return 125 + } + fi + fi +} + +write_lifecycle_baseline_binding() { + local pending="$lifecycle_baseline_binding_tsv.pending.$$" + local postgres_module_sha256 runtime_footprint_id runtime_footprint_sha256 + local durability_id durability_sha256 profile_identity + local carrier_manifest carrier_receipt carrier_inventory + + [ "$wasix_lifecycle_plateau" -eq 1 ] || return 0 + assert_frozen_lifecycle_baseline || return + postgres_module_sha256="$(fresh_wasmer_bin_hash "$wasix_postgres_module")" || return + runtime_footprint_id="${runtime_footprint:-none}" + runtime_footprint_sha256="${FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256:-none}" + durability_id="${durability_profile:-none}" + durability_sha256="${FRESH_POSTGRES_DURABILITY_SHA256:-none}" + profile_identity="${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-none}" + carrier_manifest=none + carrier_receipt=none + carrier_inventory=none + if [ "$wasix_runtime_mode" = sealed-headless ]; then + carrier_manifest="$sealed_manifest_hash" + carrier_receipt="$sealed_receipt_hash" + carrier_inventory="$sealed_payload_inventory_hash" + fi + case "$runtime_footprint_id$runtime_footprint_sha256$durability_id$durability_sha256$profile_identity$wasix_runtime_mode$wasmer_bin_hash$postgres_module_sha256$carrier_manifest$carrier_receipt$carrier_inventory" in + *$'\t'*|*$'\n'*|*$'\r'*) + echo 'lifecycle baseline identity values may not contain control separators' >&2 + return 2 + ;; + esac + rm -f -- "$pending" + { + printf 'schema_version\tpolicy_id\tpolicy_sha256\tpolicy_status\tclaim_scope\tbaseline_assumption\tpostgres_major\truntime_footprint\truntime_footprint_sha256\tdurability_profile\tdurability_profile_sha256\tpostgres_profile_resolution_identity\truntime_mode\twasmer_bin_sha256\tpostgres_module_sha256\tcarrier_manifest_sha256\tcarrier_receipt_sha256\tcarrier_payload_inventory_sha256\n' + printf 'oliphaunt.wasix-postmaster.lifecycle-baseline-binding.v1\t%s\t%s\t%s\t%s\t%s\t18\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$lifecycle_baseline_policy_id" "$lifecycle_baseline_policy_identity" \ + "$lifecycle_baseline_policy_status" "$lifecycle_baseline_claim_scope" \ + "$lifecycle_baseline_assumption" \ + "$runtime_footprint_id" "$runtime_footprint_sha256" \ + "$durability_id" "$durability_sha256" "$profile_identity" \ + "$wasix_runtime_mode" "$wasmer_bin_hash" "$postgres_module_sha256" \ + "$carrier_manifest" "$carrier_receipt" "$carrier_inventory" + } >"$pending" || { + rm -f -- "$pending" + return 1 + } + mv -f -- "$pending" "$lifecycle_baseline_binding_tsv" || { + rm -f -- "$pending" + return 1 + } + lifecycle_baseline_binding_identity="$( + fresh_wasmer_bin_hash "$lifecycle_baseline_binding_tsv" + )" + lifecycle_bound_wasmer_identity="$wasmer_bin_hash" + lifecycle_bound_postgres_module_identity="$postgres_module_sha256" + lifecycle_bound_runtime_footprint_identity="$runtime_footprint_sha256" + lifecycle_bound_durability_identity="$durability_sha256" + lifecycle_bound_profile_resolution_identity="$profile_identity" + lifecycle_bound_carrier_manifest_identity="$carrier_manifest" + lifecycle_bound_carrier_receipt_identity="$carrier_receipt" + lifecycle_bound_carrier_inventory_identity="$carrier_inventory" + assert_frozen_lifecycle_baseline +} + +write_execution_identity() { + local pending="$execution_identity_tsv.pending.$$" + local carrier_identity=none manifest_identity=none receipt_identity=none + local payload_identity=none headless_identity=none + local postgres_module_identity profile_identity + local runtime_footprint_id runtime_footprint_sha durability_id durability_sha + + [ "$need_wasix" -eq 1 ] || return 0 + if [ "$require_zero_write_aot" -eq 1 ]; then + postgres_module_identity="$immutable_carrier_postgres_module_sha256" + else + postgres_module_identity="$(fresh_wasmer_bin_hash "$wasix_postgres_module")" || return + fi + profile_identity="${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-none}" + runtime_footprint_id="${runtime_footprint:-none}" + runtime_footprint_sha="${FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256:-none}" + durability_id="${durability_profile:-none}" + durability_sha="${FRESH_POSTGRES_DURABILITY_SHA256:-none}" + if [ "$wasix_runtime_mode" = sealed-headless ]; then + if [ "$require_zero_write_aot" -eq 1 ]; then + "$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier_root" \ + --receipt "$immutable_carrier_receipt" --fast || return + else + fresh_capture_qualification_carrier_identity "$sealed_carrier_root" || return + fi + carrier_identity="$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" + manifest_identity="$FRESH_QUALIFICATION_CARRIER_MANIFEST_SHA256" + receipt_identity="$FRESH_QUALIFICATION_CARRIER_RECEIPT_SHA256" + payload_identity="$FRESH_QUALIFICATION_CARRIER_PAYLOAD_SHA256" + headless_identity="$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" + [ "$headless_identity" = "$wasmer_bin_hash" ] || { + echo 'sealed carrier identity does not match selected Wasmer binary' >&2 + return 125 + } + fi + rm -f -- "$pending" + { + printf 'schema_version\tpostgres_major\truntime_mode\tcarrier_closure_identity\tcarrier_manifest_sha256\tcarrier_receipt_sha256\tcarrier_payload_inventory_sha256\tcarrier_headless_sha256\twasmer_bin_sha256\tpostgres_module_sha256\truntime_footprint\truntime_footprint_sha256\tdurability_profile\tdurability_profile_sha256\tpostgres_profile_resolution_identity\n' + printf 'oliphaunt.wasix-postmaster.execution-identity.v1\t18\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$wasix_runtime_mode" "$carrier_identity" "$manifest_identity" \ + "$receipt_identity" "$payload_identity" "$headless_identity" \ + "$wasmer_bin_hash" "$postgres_module_identity" \ + "$runtime_footprint_id" "$runtime_footprint_sha" \ + "$durability_id" "$durability_sha" "$profile_identity" + } >"$pending" || { + rm -f -- "$pending" + return 1 + } + mv -f -- "$pending" "$execution_identity_tsv" || { + rm -f -- "$pending" + return 1 + } + execution_identity_sha256="$(fresh_wasmer_bin_hash "$execution_identity_tsv")" + execution_bound_carrier_identity="$carrier_identity" + execution_bound_manifest_identity="$manifest_identity" + execution_bound_receipt_identity="$receipt_identity" + execution_bound_payload_identity="$payload_identity" + execution_bound_headless_identity="$headless_identity" + execution_bound_wasmer_identity="$wasmer_bin_hash" + execution_bound_postgres_module_identity="$postgres_module_identity" + execution_bound_profile_identity="$profile_identity" +} + +assert_frozen_execution_identity() { + local observed receipt_stat receipt_dev receipt_ino + + [ "$need_wasix" -eq 1 ] || return 0 + [ -n "$execution_identity_sha256" ] || { + echo 'WASIX execution identity was not captured before launch' >&2 + return 125 + } + observed="$(fresh_wasmer_bin_hash "$execution_identity_tsv")" || return + [ "$observed" = "$execution_identity_sha256" ] || { + echo 'WASIX execution identity receipt changed after capture' >&2 + return 125 + } + if [ "$require_zero_write_aot" -eq 1 ]; then + [ "$wasmer_bin_hash" = "$execution_bound_wasmer_identity" ] && + [ "$immutable_carrier_postgres_module_sha256" = \ + "$execution_bound_postgres_module_identity" ] && + [ "${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-none}" = \ + "$execution_bound_profile_identity" ] || { + echo 'runtime/module/profile identity changed after execution receipt capture' >&2 + return 125 + } + else + [ "$(fresh_wasmer_bin_hash "$wasmer_bin")" = \ + "$execution_bound_wasmer_identity" ] && + [ "$(fresh_wasmer_bin_hash "$wasix_postgres_module")" = \ + "$execution_bound_postgres_module_identity" ] && + [ "${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-none}" = \ + "$execution_bound_profile_identity" ] || { + echo 'runtime/module/profile identity changed after execution receipt capture' >&2 + return 125 + } + fi + if [ "$wasix_runtime_mode" = sealed-headless ]; then + fresh_sealed_executor_selection "$sealed_carrier_root" || return 125 + [ "$FRESH_SEALED_EXECUTOR_ROLE" = "$sealed_executor_role" ] && + [ "$FRESH_SEALED_EXECUTOR_RECEIPT_RELATIVE" = \ + "$sealed_executor_receipt_relative" ] && + [ "$FRESH_SEALED_EXECUTOR_RECEIPT_SHA256" = \ + "$sealed_executor_receipt_hash" ] && + [ "$FRESH_SEALED_EXECUTOR_SHA256" = "$sealed_executor_hash" ] || { + echo 'sealed executor role selection changed after execution receipt capture' >&2 + return 125 + } + if [ "$require_zero_write_aot" -eq 1 ]; then + "$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier_root" \ + --receipt "$immutable_carrier_receipt" --fast || return 125 + else + fresh_capture_qualification_carrier_identity "$sealed_carrier_root" || return 125 + fi + [ "$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" = \ + "$execution_bound_carrier_identity" ] && + [ "$FRESH_QUALIFICATION_CARRIER_MANIFEST_SHA256" = \ + "$execution_bound_manifest_identity" ] && + [ "$FRESH_QUALIFICATION_CARRIER_RECEIPT_SHA256" = \ + "$execution_bound_receipt_identity" ] && + [ "$FRESH_QUALIFICATION_CARRIER_PAYLOAD_SHA256" = \ + "$execution_bound_payload_identity" ] && + [ "$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" = \ + "$execution_bound_headless_identity" ] || { + echo 'sealed carrier changed after execution receipt capture' >&2 + return 125 + } + if [ "$require_zero_write_aot" -eq 1 ]; then + receipt_stat="$(stat -c '%d %i' -- "$immutable_carrier_receipt")" || return + read -r receipt_dev receipt_ino <<<"$receipt_stat" + [ "$receipt_dev" = "$immutable_carrier_receipt_dev" ] && + [ "$receipt_ino" = "$immutable_carrier_receipt_ino" ] || { + echo 'immutable carrier deployment receipt inode changed after preflight' >&2 + return 125 + } + [ "$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" = \ + "$immutable_carrier_closure_identity" ] || { + echo 'immutable carrier closure changed after deployment preflight' >&2 + return 125 + } + fi + fi +} +printf 'schema_version\tenabled\tmax_peak_pss_kib\tmax_peak_pss_anon_kib\tmax_peak_page_table_kib\tmax_cgroup_high_events_delta\tmax_psi_some_stall_fraction\tmax_psi_full_stall_fraction\n' \ + >"$memory_budget_tsv" +printf 'oliphaunt.wasix-postmaster.memory-budget.v1\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$memory_budget_requested" "$max_peak_pss_kib" "$max_peak_pss_anon_kib" \ + "$max_peak_page_table_kib" "$max_cgroup_high_events_delta" \ + "$max_psi_some_stall_fraction" "$max_psi_full_stall_fraction" \ + >>"$memory_budget_tsv" +memory_budget_identity="$(fresh_sha256_stream <"$memory_budget_tsv")" +assert_frozen_memory_budget() { + local actual + actual="$(fresh_sha256_stream <"$memory_budget_tsv")" || return + [ "$actual" = "$memory_budget_identity" ] || { + printf 'memory budget receipt changed during benchmark: expected=%s actual=%s path=%s\n' \ + "$memory_budget_identity" "$actual" "$memory_budget_tsv" >&2 + return 125 + } +} +if [ "$profile_resolution_active" -eq 1 ]; then + fresh_write_postgres_profile_evidence \ + "$postgres_profile_inputs_tsv" "$postgres_profile_resolution_tsv" +fi +if [ "$libpq_latency_samples" -gt 0 ]; then + build_libpq_latency_probe "$suite_root/libpq-latency-probe" +fi + +fresh_write_report_header "$summary" "WASIX Concurrent Query Suite" +{ + printf -- '- Targets: `%s`\n' "${targets[*]}" + printf -- '- Measurement-tool closure mode: `%s`\n' \ + "$measurement_tool_closure_mode" + printf -- '- Measurement-tool closure required: `%s`\n' \ + "$require_frozen_measurement_tools" + printf -- '- Measurement-tool closure identity: `%s`\n' \ + "$measurement_tool_closure_identity" + printf -- '- Measurement-tool manifest SHA-256: `%s`\n' \ + "$measurement_tool_closure_manifest_sha256" + printf -- '- Measurement-tool evidence: `%s` (`%s`)\n' \ + "$measurement_tool_closure_tsv" \ + "$measurement_tool_closure_evidence_identity" + if [ "${#workloads[@]}" -gt 0 ]; then + printf -- '- Workloads: `%s`\n' "${workloads[*]}" + else + printf -- '- Workloads: `(none; libpq latency only)`\n' + fi + printf -- '- Connections: `%s`\n' "$connections" + printf -- '- Bulk fanout population: `clients connected before timed start; backends retained through untimed drain`\n' + printf -- '- Iterations per connection: `%s`\n' "$iterations" + printf -- '- Requested seed rows: `%s`\n' "$row_count" + printf -- '- Actual setup rows: `%s`\n' "$setup_rows" + printf -- '- Timeout: `%s seconds`\n' "$timeout_seconds" + printf -- '- Timed-command supervision: `dedicated process group; SIGTERM then SIGKILL; direct child reaped; no live group accepted`\n' + printf -- '- Process SIGTERM/SIGKILL grace: `%s ms` / `%s ms`\n' \ + "$process_term_grace_ms" "$process_kill_grace_ms" + printf -- '- Resource sample interval: `%s seconds`\n' "$resource_sample_interval" + printf -- '- Resource sampler detail: `%s`\n' "$resource_detail" + if [ "$need_wasix" -eq 1 ]; then + printf -- '- WASIX shared-memory provider: `%s`\n' \ + "$shared_memory_provider" + printf -- '- Shared-memory provider activation: `%s`\n' \ + "$([ "$shared_memory_provider_explicit" -eq 1 ] && printf explicit-cli || printf default-portable)" + printf -- '- Shared-memory provider evidence: `%s/wasix/shared-memory-{provider,objects,release,cleanup}.json`\n' \ + "$report_dir" + printf -- '- Shared-memory provider claim: `diagnostic backing-substrate A/B; no cross-platform promotion claim`\n' + fi + printf -- '- Server cgroup MemoryMax: `%s`\n' "${cgroup_memory_max:-unset}" + printf -- '- Server cgroup MemoryHigh: `%s`\n' "${cgroup_memory_high:-unset}" + printf -- '- Server cgroup MemorySwapMax: `%s`\n' "${cgroup_swap_max:-unset}" + printf -- '- Cold ownership startup lane: `%s`\n' "$cold_ownership" + if [ "$cold_ownership" -eq 1 ]; then + printf -- '- Cold ownership lifecycle mode: `%s`\n' "$cold_ownership_mode" + printf -- '- Cold ownership mode evidence: `%s`\n' "$cold_ownership_mode_tsv" + printf -- '- Cold boundary: `content SHA-256 + per-file fdatasync + POSIX_FADV_DONTNEED + zero-page mincore proof`\n' + printf -- '- Cold accounting: `first carrier/PGDATA faults occur after launch in the fresh measured cgroup; no global drop_caches`\n' + fi + printf -- '- Memory performance budgets enabled: `%s`\n' "$memory_budget_requested" + printf -- '- Maximum fan-out peak PSS: `%s KiB`\n' "${max_peak_pss_kib:-unset}" + printf -- '- Maximum fan-out peak anonymous PSS: `%s KiB`\n' \ + "${max_peak_pss_anon_kib:-unset}" + printf -- '- Maximum fan-out peak page tables: `%s KiB`\n' \ + "${max_peak_page_table_kib:-unset}" + printf -- '- Maximum fan-out cgroup high-event delta: `%s`\n' \ + "${max_cgroup_high_events_delta:-unset}" + printf -- '- Maximum fan-out PSI some-stall fraction: `%s`\n' \ + "${max_psi_some_stall_fraction:-unset}" + printf -- '- Maximum fan-out PSI full-stall fraction: `%s`\n' \ + "${max_psi_full_stall_fraction:-unset}" + printf -- '- Memory budget receipt: `%s`\n' "$memory_budget_tsv" + printf -- '- Memory budget identity: `%s`\n' "$memory_budget_identity" + printf -- '- Instrumentation policy receipt: `%s`\n' "$instrumentation_policy_tsv" + printf -- '- Instrumentation policy identity: `%s`\n' "$instrumentation_policy_identity" + printf -- '- Adaptive cache evidence policy: `%s`\n' \ + "$adaptive_cache_evidence_policy" + printf -- '- Adaptive cache evidence policy receipt: `%s` (`%s`)\n' \ + "$adaptive_cache_evidence_policy_tsv" \ + "$adaptive_cache_evidence_policy_identity" + printf -- '- Post-readiness/workload quiescence: `%s seconds`\n' "$quiescence_seconds" + printf -- '- Checkpoint policy: `%s`\n' "$checkpoint_policy" + printf -- '- Runtime footprint: `%s`\n' "${runtime_footprint:-none}" + printf -- '- Runtime-footprint SHA-256: `%s`\n' \ + "${FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256:-}" + printf -- '- Durability profile: `%s`\n' "${durability_profile:-none}" + printf -- '- Durability-profile SHA-256: `%s`\n' \ + "${FRESH_POSTGRES_DURABILITY_SHA256:-}" + printf -- '- PostgreSQL profile resolution identity: `%s`\n' \ + "${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-}" + printf -- '- Native libpq true-latency measured samples per mode: `%s`\n' "$libpq_latency_samples" + printf -- '- Native libpq true-latency warmup samples per mode: `%s`\n' "$libpq_latency_warmup" + printf -- '- Native libpq true-latency only: `%s`\n' "$libpq_latency_only" + printf -- '- Native libpq true-latency clock: `CLOCK_MONOTONIC nanoseconds`\n' + printf -- '- Native libpq true-latency modes: `persistent times PQexec(SELECT 1); reconnect times PQconnectdb -> SELECT 1 -> PQfinish`\n' + printf -- '- Native libpq true-latency percentiles: `nearest rank over exact valid measured rows; warmups excluded`\n' + printf -- '- Native libpq true-latency isolation: `not psql bulk batch wall time and not bulk residual`\n' + printf -- '- Native libpq true-latency server soft RLIMIT_NOFILE: `%s` (hard limit preserved)\n' \ + "$libpq_latency_soft_nofile" + printf -- '- Native libpq reconnect quiescent host-FD growth allowance: `%s`\n' \ + "$libpq_latency_host_fd_allowance" + if [ "$libpq_latency_samples" -gt 0 ]; then + printf -- '- Native libpq latency probe: `%s`\n' "$libpq_latency_probe_bin" + printf -- '- Native libpq latency probe SHA-256: `%s`\n' "$libpq_latency_probe_sha256" + printf -- '- Native libpq latency probe source SHA-256: `%s`\n' "$libpq_latency_probe_source_sha256" + printf -- '- Native libpq latency probe compiler: `%s`\n' "$libpq_latency_compiler" + printf -- '- Native libpq latency shared library: `%s`\n' "$libpq_latency_libpq_path" + printf -- '- Native libpq latency shared-library SHA-256: `%s`\n' "$libpq_latency_libpq_sha256" + fi + printf -- '- Discard successful PGDATA: `%s`\n' "$discard_pgdata" + printf -- '- Linux memory diagnostics: `PSS` with overlapping anon/file/shmem, private, and mapped-shared breakdowns (never additive)\n' + printf -- '- Resource timestamps: `CLOCK_MONOTONIC milliseconds (not Unix epoch)`\n' + printf -- '- Cgroup scope accounting: `memory.current includes charged page cache and kernel memory; process PSS does not`\n' + printf -- '- Cgroup memory.stat accounting: `exact raw keys; overlapping gauges and aggregate/component counters are never summed`\n' + printf -- '- Cgroup phase metrics: `sampled current gauges and gauge peaks; cumulative file-cache counters retain phase start/end plus first-to-last delta/rate`\n' + printf -- '- Cgroup optional-key semantics: `missing kernel memory.stat keys are blank and named explicitly; blank never means zero`\n' + printf -- '- Cgroup whole-run metrics: `scope-lifetime peaks plus observed cumulative events/pressure totals; memory.events.local is used only for a leaf scope and the selected event file is evidence-bound`\n' + printf -- '- Cgroup memory.pressure totals: `absolute cumulative stall microseconds observed in the fresh transient scope (maximum observed), not interval deltas or percentages`\n' + printf -- '- Host open-FD occupancy: `Linux /proc entries summed over the sampled native process tree or singleton Wasmer process; host capacity occupancy, not a guest FD number or unique underlying open file descriptions; blank totals plus status distinguish unavailable observations from zero`\n' + printf -- '- PostgreSQL wait sample interval: `%s seconds`\n' "$pg_wait_sample_interval" + printf -- '- PostgreSQL wait sampler: `persistent-connection`\n' + printf -- '- WASIX perf stats: `%s`\n' "$wasix_perf_stats" + printf -- '- WASIX wait dump interval: `%s ms`\n' "$wasix_wait_dump_interval_ms" + printf -- '- WASIX wait dump max per wait: `%s`\n' "$wasix_wait_dump_max_per_wait" + printf -- '- WASIX wait dump verbose detail: `%s`\n' "$wasix_wait_dump_verbose" + printf -- '- WASIX lifecycle plateau lane: `%s`\n' "$wasix_lifecycle_plateau" + printf -- '- WASIX lifecycle reconnects: `%s`\n' "$wasix_lifecycle_reconnects" + printf -- '- WASIX lifecycle sampling window: `%s seconds`\n' \ + "$wasix_lifecycle_window_seconds" + printf -- '- WASIX lifecycle memory checkpoint interval: `%s reconnects`\n' \ + "$wasix_lifecycle_memory_checkpoint_every" + printf -- '- WASIX lifecycle memory checkpoint quiescence: `%s seconds`\n' \ + "$wasix_lifecycle_memory_quiescence_seconds" + printf -- '- WASIX lifecycle PSS / Pss_Anon / heap growth ceilings: `%s / %s / %s KiB`\n' \ + "${max_lifecycle_pss_growth_kib:-unset}" \ + "${max_lifecycle_pss_anon_growth_kib:-unset}" \ + "${max_lifecycle_heap_growth_kib:-unset}" + printf -- '- WASIX lifecycle late-tail PSS / Pss_Anon / heap Theil-Sen slope ceilings: `%s / %s / %s KiB per 1000 reconnects`\n' \ + "${max_late_lifecycle_pss_slope_kib_per_1000:-unset}" \ + "${max_late_lifecycle_pss_anon_slope_kib_per_1000:-unset}" \ + "${max_late_lifecycle_heap_slope_kib_per_1000:-unset}" + printf -- '- WASIX lifecycle memory sampling: `untimed reconnect boundaries only; no smaps read overlaps a client; baseline/final runtime-fenced`\n' + printf -- '- WASIX lifecycle stabilization: `pg_log_standby_snapshot -> walwriter pg_stat_io write/byte advance -> target LSN flushed`\n' + printf -- '- Memory map snapshots: `%s`\n' "$memory_map_snapshots" + printf -- '- WASIX sample seconds: `%s`\n' "$sample_seconds" + printf -- '- WASIX sample delay: `%s`\n' "$sample_delay" + printf -- '- Start port: `%s`\n' "$start_port" + printf -- '- WASIX core profile: `%s`\n' "$WASIX_CORE_PROFILE" + printf -- '- WASIX runtime mode: `%s`\n' "$wasix_runtime_mode" + if [ "$wasix_runtime_mode" = "sealed-headless" ]; then + printf -- '- Sealed carrier: `%s`\n' "$sealed_carrier_root" + printf -- '- Sealed manifest SHA-256: `%s`\n' "$sealed_manifest_hash" + printf -- '- Sealed receipt SHA-256: `%s`\n' "$sealed_receipt_hash" + printf -- '- Sealed payload inventory SHA-256: `%s`\n' "$sealed_payload_inventory_hash" + else + printf -- '- WASIX install dir: `%s`\n' "$WASIX_INSTALL_DIR" + fi + printf -- '- Pinned runtime: `%s`\n' "${FRESH_PINNED_RUNTIME_NAME:-}" + printf -- '- Effective PostgreSQL GUCs: `%s`\n' "${effective_postgres_gucs[*]:-}" + if [ "$profile_resolution_active" -eq 1 ]; then + printf -- '- PostgreSQL profile inputs: `%s`\n' "$postgres_profile_inputs_tsv" + printf -- '- PostgreSQL profile resolution: `%s`\n' "$postgres_profile_resolution_tsv" + fi + printf -- '- Extra Wasmer args: `%s`\n' "${wasmer_extra_args[*]:-}" + printf -- '- Summary TSV: `%s`\n' "$summary_tsv" + printf -- '- Client TSV: `%s`\n' "$client_tsv" + printf -- '- Resource TSV: `%s`\n' "$resource_tsv" + printf -- '- Checkpoint TSV: `%s`\n' "$checkpoint_tsv" + printf -- '- Native libpq latency TSV: `%s`\n\n' "$libpq_latency_tsv" + printf -- '- Host FD checkpoints TSV: `%s`\n' "$host_fd_checkpoints_tsv" + printf -- '- Server limits TSV: `%s`\n' "$server_limits_tsv" + printf -- '- Server lifecycle TSV: `%s`\n' "$server_lifecycle_tsv" + printf -- '- Memory evidence TSV: `%s`\n\n' "$memory_evidence_tsv" + printf -- '- Memory budget TSV: `%s`\n\n' "$memory_budget_tsv" + if [ "$cold_ownership" -eq 1 ]; then + printf -- '- Cold sample: `%s`\n\n' "$report_dir/wasix/cold-ownership-sample.tsv" + fi + if [ "$wasix_lifecycle_plateau" -eq 1 ]; then + printf -- '- WASIX lifecycle plateau TSV: `%s`\n\n' "$lifecycle_plateau_tsv" + printf -- '- WASIX lifecycle baseline policy: `%s` (`%s`)\n' \ + "$lifecycle_baseline_policy_tsv" "$lifecycle_baseline_policy_identity" + printf -- '- WASIX lifecycle baseline policy ID/status/scope: `%s` / `%s` / `%s`\n' \ + "$lifecycle_baseline_policy_id" "$lifecycle_baseline_policy_status" \ + "$lifecycle_baseline_claim_scope" + if [ "$wasix_lifecycle_memory_checkpoint_every" -gt 0 ]; then + printf -- '- WASIX lifecycle memory checkpoints: `%s`\n' \ + "$lifecycle_memory_checkpoints_tsv" + printf -- '- WASIX lifecycle memory plateau: `%s`\n\n' \ + "$lifecycle_memory_plateau_tsv" + fi + fi +} >>"$summary" + +printf 'target\tworkload\tstatus\tconnections\titerations\toperation_count\tverified_count\texpected_verify_count\tfanout_wall_ms\tthroughput_ops_per_sec\tok_clients\tfailed_clients\ttimed_out\tepoll_intr_count\tserver_log\treport_dir\n' >"$summary_tsv" +printf 'target\tworkload\tclient\tstatus\tbulk_batch_wall_ms\tbulk_batch_psql_time_sum_ms\tbulk_batch_psql_time_count\tlog\n' >"$client_tsv" +printf 'target\tphase\tpeak_rss_kb\tpeak_rss_mb\tpeak_vsz_kb\tpeak_cpu_percent\tpeak_process_count\tsample_count\tsamples_log\tpeak_pss_kb\tpeak_pss_mb\tpeak_pss_anon_kb\tpeak_pss_anon_mb\tpeak_pss_file_kb\tpeak_pss_file_mb\tpeak_pss_shmem_kb\tpeak_pss_shmem_mb\tpeak_private_kb\tpeak_private_mb\tpeak_shared_mapped_kb\tpeak_shared_mapped_mb\tpeak_anonymous_mapped_kb\tpeak_anonymous_mapped_mb\tpeak_swap_kb\tsmaps_sample_count\tpeak_host_thread_count\tpeak_page_table_kb\tpeak_page_table_mb\tpeak_private_clean_kb\tpeak_private_clean_mb\tpeak_private_dirty_kb\tpeak_private_dirty_mb\tcgroup_path\tphase_sampled_peak_cgroup_memory_current_bytes\tphase_sampled_peak_cgroup_memory_current_mb\twhole_run_observed_cgroup_memory_peak_bytes\twhole_run_observed_cgroup_memory_peak_mb\tphase_sampled_peak_cgroup_swap_current_bytes\tphase_sampled_peak_cgroup_swap_current_mb\twhole_run_observed_cgroup_swap_peak_bytes\twhole_run_observed_cgroup_swap_peak_mb\tphase_sampled_peak_cgroup_pids_current\twhole_run_observed_cgroup_event_high_total\twhole_run_observed_cgroup_event_max_total\twhole_run_observed_cgroup_event_oom_total\twhole_run_observed_cgroup_event_oom_kill_total\tcgroup_memory_max\tcgroup_memory_high\tcgroup_swap_max\tphase_sampled_peak_cgroup_memory_stat_anon_bytes\tphase_sampled_peak_cgroup_memory_stat_anon_mb\tphase_sampled_peak_cgroup_memory_stat_file_bytes\tphase_sampled_peak_cgroup_memory_stat_file_mb\tphase_sampled_peak_cgroup_memory_stat_shmem_bytes\tphase_sampled_peak_cgroup_memory_stat_shmem_mb\tphase_sampled_peak_cgroup_memory_stat_kernel_bytes\tphase_sampled_peak_cgroup_memory_stat_kernel_mb\tphase_sampled_peak_cgroup_memory_stat_pagetables_bytes\tphase_sampled_peak_cgroup_memory_stat_pagetables_mb\tphase_sampled_peak_cgroup_memory_stat_slab_bytes\tphase_sampled_peak_cgroup_memory_stat_slab_mb\tphase_sampled_peak_cgroup_memory_stat_file_dirty_bytes\tphase_sampled_peak_cgroup_memory_stat_file_dirty_mb\tphase_sampled_peak_cgroup_memory_stat_file_writeback_bytes\tphase_sampled_peak_cgroup_memory_stat_file_writeback_mb\twhole_run_observed_cgroup_memory_pressure_some_total_usec\twhole_run_observed_cgroup_memory_pressure_full_total_usec\tphase_sampled_peak_host_open_fd_count\tphase_host_open_fd_valid_sample_count\tphase_host_open_fd_unsupported_sample_count\tphase_host_open_fd_unreadable_sample_count\tphase_host_open_fd_raced_sample_count\tphase_smaps_valid_sample_count\tphase_smaps_unsupported_sample_count\tphase_smaps_disabled_sample_count\tphase_smaps_unreadable_sample_count\tphase_smaps_raced_sample_count\tphase_cgroup_valid_sample_count\tphase_cgroup_disabled_sample_count\tphase_cgroup_unavailable_sample_count\tphase_sampled_peak_cgroup_memory_stat_active_file_bytes\tphase_sampled_peak_cgroup_memory_stat_active_file_mb\tphase_sampled_peak_cgroup_memory_stat_inactive_file_bytes\tphase_sampled_peak_cgroup_memory_stat_inactive_file_mb\tphase_sampled_peak_cgroup_memory_stat_file_mapped_bytes\tphase_sampled_peak_cgroup_memory_stat_file_mapped_mb\tphase_cgroup_memory_stat_file_cache_status\tphase_cgroup_memory_stat_file_cache_missing_keys\twhole_run_observed_cgroup_memory_events_source\tphase_observed_cgroup_memory_stat_counter_elapsed_ms\tphase_observed_cgroup_memory_stat_workingset_refault_file_pages_start\tphase_observed_cgroup_memory_stat_workingset_refault_file_pages_end\tphase_observed_cgroup_memory_stat_workingset_refault_file_pages_delta\tphase_observed_cgroup_memory_stat_workingset_refault_file_pages_per_second\tphase_observed_cgroup_memory_stat_workingset_activate_file_pages_start\tphase_observed_cgroup_memory_stat_workingset_activate_file_pages_end\tphase_observed_cgroup_memory_stat_workingset_activate_file_pages_delta\tphase_observed_cgroup_memory_stat_workingset_activate_file_pages_per_second\tphase_observed_cgroup_memory_stat_workingset_restore_file_pages_start\tphase_observed_cgroup_memory_stat_workingset_restore_file_pages_end\tphase_observed_cgroup_memory_stat_workingset_restore_file_pages_delta\tphase_observed_cgroup_memory_stat_workingset_restore_file_pages_per_second\tphase_observed_cgroup_memory_stat_pgscan_pages_start\tphase_observed_cgroup_memory_stat_pgscan_pages_end\tphase_observed_cgroup_memory_stat_pgscan_pages_delta\tphase_observed_cgroup_memory_stat_pgscan_pages_per_second\tphase_observed_cgroup_memory_stat_pgsteal_pages_start\tphase_observed_cgroup_memory_stat_pgsteal_pages_end\tphase_observed_cgroup_memory_stat_pgsteal_pages_delta\tphase_observed_cgroup_memory_stat_pgsteal_pages_per_second\n' >"$resource_tsv" +printf 'target\tworkload\tpolicy\tstatus\tnum_timed_before\tnum_timed_after\tnum_timed_delta\tnum_requested_before\tnum_requested_after\tnum_requested_delta\tnum_done_before\tnum_done_after\tnum_done_delta\twal_bytes_before\twal_bytes_after\twal_bytes_delta\twal_budget_bytes\tstate_before\tstate_after\tio_before\tio_after\tio_delta\tio_delta_status\n' >"$checkpoint_tsv" +printf 'schema_version\ttarget\tmode\tstatus\tclock\twarmup_count\tsample_count\tp50_ns\tp95_ns\tp99_ns\tp50_ms\tp95_ms\tp99_ms\traw_tsv\tlibpq_path\tlibpq_sha256\tprobe_sha256\n' >"$libpq_latency_tsv" +printf 'target\tmode\tstage\tmonotonic_ms\ttotal_open_fds\tobserved_processes\texpected_processes\tstatus\n' >"$host_fd_checkpoints_tsv" +printf 'target\tmode\tbefore_open_fds\tafter_open_fds\tquiescent_open_fds\tquiescent_growth\tallowance\tstatus\n' >"$host_fd_churn_tsv" +printf 'target\trequested_soft_nofile\tpre_soft_nofile\tpre_hard_nofile\tactual_soft_nofile\tactual_hard_nofile\tstatus\tlaunch_record\n' >"$server_limits_tsv" +printf 'target\tserver_pid\tserver_pgid\tserver_birth_identity\tcgroup_path\tcgroup_identity\torderly_int\tforced\twait_status\tclean_shutdown_marker\tprocess_group_residue\tcgroup_residue\tport_residue\tstatus\treport\n' >"$server_lifecycle_tsv" +printf 'target\tstatus\tdetail\tsamples\n' >"$memory_evidence_tsv" + +write_workload_sql() { + local workload="$1" + local setup_sql="$2" + local client_sql="$3" + local verify_sql="$4" + + case "$workload" in + indexed-read) + cat >"$setup_sql" <<'SQL' +\set ON_ERROR_STOP 1 +DROP TABLE IF EXISTS cqb_indexed_read; +CREATE TABLE cqb_indexed_read ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL +); +INSERT INTO cqb_indexed_read +SELECT i, i % 1000, md5(i::text) +FROM generate_series(1, :row_count) AS i; +CREATE INDEX cqb_indexed_read_bucket_idx ON cqb_indexed_read (bucket); +CREATE INDEX cqb_indexed_read_payload_idx ON cqb_indexed_read (payload); +ANALYZE cqb_indexed_read; +CREATE OR REPLACE FUNCTION cqb_indexed_read_worker(client_id integer, iterations integer, row_count integer) +RETURNS bigint +LANGUAGE plpgsql +AS $$ +DECLARE + i integer; + key integer; + value text; + bucket_count bigint; + total bigint := 0; +BEGIN + FOR i IN 1..iterations LOOP + key := (((client_id::bigint * 104729) + (i::bigint * 7919)) % row_count + 1)::integer; + SELECT payload INTO value FROM cqb_indexed_read WHERE id = key; + total := total + length(value); + IF i % 10 = 0 THEN + SELECT count(*) INTO bucket_count FROM cqb_indexed_read WHERE bucket = key % 1000; + total := total + bucket_count; + END IF; + END LOOP; + RETURN total; +END; +$$; +SQL + cat >"$client_sql" <<'SQL' +\set ON_ERROR_STOP 1 +\pset tuples_only on +SELECT pg_backend_pid(); +\timing on +SELECT cqb_indexed_read_worker(:client_id, :iterations, :row_count); +\timing off +SQL + cat >"$verify_sql" <<'SQL' +SELECT count(*)::bigint FROM cqb_indexed_read; +SQL + ;; + mixed-write) + cat >"$setup_sql" <<'SQL' +\set ON_ERROR_STOP 1 +DROP TABLE IF EXISTS cqb_mixed_write; +CREATE TABLE cqb_mixed_write ( + client_id integer NOT NULL, + iteration integer NOT NULL, + bucket integer NOT NULL, + payload text NOT NULL, + updates integer NOT NULL DEFAULT 0, + PRIMARY KEY (client_id, iteration) +); +SQL + cat >"$client_sql" <<'SQL' +\set ON_ERROR_STOP 1 +\pset tuples_only on +SELECT pg_backend_pid(); +\timing on +BEGIN; +INSERT INTO cqb_mixed_write (client_id, iteration, bucket, payload) +SELECT :client_id, g, (:client_id * 100000 + g) % 1000, md5((:client_id::text || ':' || g::text)) +FROM generate_series(1, :iterations) AS g; +UPDATE cqb_mixed_write +SET bucket = bucket + 1, + payload = md5(payload || ':updated'), + updates = updates + 1 +WHERE client_id = :client_id; +COMMIT; +\timing off +SQL + cat >"$verify_sql" <<'SQL' +SELECT (count(*) + coalesce(sum(updates), 0))::bigint FROM cqb_mixed_write; +SQL + ;; + indexed-update) + cat >"$setup_sql" <<'SQL' +\set ON_ERROR_STOP 1 +DROP TABLE IF EXISTS cqb_indexed_update; +CREATE TABLE cqb_indexed_update ( + id integer PRIMARY KEY, + bucket integer NOT NULL, + payload text NOT NULL, + updates integer NOT NULL DEFAULT 0 +); +INSERT INTO cqb_indexed_update +SELECT i, i % 1000, md5(i::text), 0 +FROM generate_series(1, :setup_rows) AS i; +CREATE INDEX cqb_indexed_update_bucket_idx ON cqb_indexed_update (bucket); +CREATE INDEX cqb_indexed_update_payload_idx ON cqb_indexed_update (payload); +ANALYZE cqb_indexed_update; +SQL + cat >"$client_sql" <<'SQL' +\set ON_ERROR_STOP 1 +\pset tuples_only on +SELECT pg_backend_pid(); +\timing on +UPDATE cqb_indexed_update +SET bucket = bucket + 1, + payload = md5(payload || ':u'), + updates = updates + 1 +WHERE id BETWEEN ((:client_id - 1) * :iterations + 1) AND (:client_id * :iterations); +\timing off +SQL + cat >"$verify_sql" <<'SQL' +SELECT coalesce(sum(updates), 0)::bigint FROM cqb_indexed_update; +SQL + ;; + indexed-insert) + cat >"$setup_sql" <<'SQL' +\set ON_ERROR_STOP 1 +DROP TABLE IF EXISTS cqb_indexed_insert; +CREATE TABLE cqb_indexed_insert ( + id integer PRIMARY KEY, + client_id integer NOT NULL, + bucket integer NOT NULL, + payload text NOT NULL +); +CREATE INDEX cqb_indexed_insert_client_idx ON cqb_indexed_insert (client_id); +CREATE INDEX cqb_indexed_insert_bucket_idx ON cqb_indexed_insert (bucket); +CREATE INDEX cqb_indexed_insert_payload_idx ON cqb_indexed_insert (payload); +SQL + cat >"$client_sql" <<'SQL' +\set ON_ERROR_STOP 1 +\pset tuples_only on +SELECT pg_backend_pid(); +\timing on +INSERT INTO cqb_indexed_insert (id, client_id, bucket, payload) +SELECT ((:client_id - 1) * :iterations + g), :client_id, (:client_id * 100000 + g) % 1000, + md5((:client_id::text || ':' || g::text)) +FROM generate_series(1, :iterations) AS g; +\timing off +SQL + cat >"$verify_sql" <<'SQL' +SELECT count(*)::bigint FROM cqb_indexed_insert; +SQL + ;; + esac +} + +operation_count_for() { + local workload="$1" + case "$workload" in + indexed-read) echo $((connections * (iterations + iterations / 10))) ;; + mixed-write) echo $((connections * iterations * 2)) ;; + *) echo $((connections * iterations)) ;; + esac +} + +expected_verify_count_for() { + local workload="$1" + case "$workload" in + indexed-read) echo "$row_count" ;; + mixed-write) echo $((connections * iterations * 2)) ;; + indexed-update|indexed-insert) echo $((connections * iterations)) ;; + esac +} + +stop_pid() { + local pid="${1:-}" + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi +} + +active_server_pids=() +active_server_pgids=() +active_server_identities=() +active_server_ports=() +active_server_cgroup_dirs=() +active_server_cgroup_identities=() +active_background_pids=() +active_client_pids=() +active_client_pgids=() +active_shared_memory_records=() +pending_shared_memory_records=() + +register_server_pid() { + active_server_pids+=("$1") + active_server_pgids+=("$2") + active_server_identities+=("$3") + active_server_ports+=("$4") + active_server_cgroup_dirs+=("$5") + active_server_cgroup_identities+=("$6") +} + +unregister_server_pid() { + local remove_pid="$1" + local pid index + local remaining=() + local remaining_pgids=() remaining_identities=() remaining_ports=() + local remaining_cgroup_dirs=() remaining_cgroup_identities=() + for index in "${!active_server_pids[@]}"; do + pid="${active_server_pids[$index]}" + if [ "$pid" != "$remove_pid" ]; then + remaining+=("$pid") + remaining_pgids+=("${active_server_pgids[$index]}") + remaining_identities+=("${active_server_identities[$index]}") + remaining_ports+=("${active_server_ports[$index]}") + remaining_cgroup_dirs+=("${active_server_cgroup_dirs[$index]}") + remaining_cgroup_identities+=("${active_server_cgroup_identities[$index]}") + fi + done + active_server_pids=("${remaining[@]}") + active_server_pgids=("${remaining_pgids[@]}") + active_server_identities=("${remaining_identities[@]}") + active_server_ports=("${remaining_ports[@]}") + active_server_cgroup_dirs=("${remaining_cgroup_dirs[@]}") + active_server_cgroup_identities=("${remaining_cgroup_identities[@]}") +} + +register_external_shared_memory_provider() { + local field record existing provider root evidence evidence_sha256 + local cleanup_evidence exit_objects exit_release extra + [ "$#" -eq 7 ] || { + echo 'external shared-memory registration requires seven fields' >&2 + return 125 + } + for field in "$@"; do + case "$field" in + ''|*$'\t'*|*$'\n'*|*$'\r'*) + echo 'external shared-memory registration contains an unsafe field' >&2 + return 125 + ;; + esac + done + record="$1"$'\t'"$2"$'\t'"$3"$'\t'"$4"$'\t'"$5"$'\t'"$6"$'\t'"$7" + for existing in "${active_shared_memory_records[@]}"; do + IFS=$'\t' read -r provider root evidence evidence_sha256 cleanup_evidence \ + exit_objects exit_release extra <<<"$existing" + [ -z "$extra" ] || return 125 + if [ "$root" = "$2" ]; then + [ "$existing" = "$record" ] || { + printf 'conflicting external shared-memory registration for %s\n' "$2" >&2 + return 125 + } + return 0 + fi + done + active_shared_memory_records+=("$record") +} + +register_pending_external_shared_memory_provider() { + local field record existing provider evidence cleanup_evidence exit_objects + local exit_release extra + [ "$#" -eq 5 ] || { + echo 'pending shared-memory registration requires five fields' >&2 + return 125 + } + for field in "$@"; do + case "$field" in + ''|*$'\t'*|*$'\n'*|*$'\r'*) + echo 'pending shared-memory registration contains an unsafe field' >&2 + return 125 + ;; + esac + done + record="$1"$'\t'"$2"$'\t'"$3"$'\t'"$4"$'\t'"$5" + for existing in "${pending_shared_memory_records[@]}"; do + IFS=$'\t' read -r provider evidence cleanup_evidence exit_objects \ + exit_release extra <<<"$existing" + [ -z "$extra" ] || return 125 + if [ "$evidence" = "$2" ]; then + [ "$existing" = "$record" ] || { + printf 'conflicting pending shared-memory registration for %s\n' "$2" >&2 + return 125 + } + return 0 + fi + done + pending_shared_memory_records+=("$record") +} + +unregister_pending_external_shared_memory_provider() { + local remove_evidence="$1" + local record provider evidence cleanup_evidence exit_objects exit_release extra + local remaining=() + for record in "${pending_shared_memory_records[@]}"; do + IFS=$'\t' read -r provider evidence cleanup_evidence exit_objects \ + exit_release extra <<<"$record" + [ -z "$extra" ] || return 125 + if [ "$evidence" != "$remove_evidence" ]; then + remaining+=("$record") + fi + done + pending_shared_memory_records=("${remaining[@]}") +} + +recover_pending_external_shared_memory_providers() { + local record expected_provider evidence cleanup_evidence exit_objects exit_release + local identity provider root evidence_sha256 extra + local recovery_status=0 + local reconciled=() + + for record in "${pending_shared_memory_records[@]}"; do + IFS=$'\t' read -r expected_provider evidence cleanup_evidence exit_objects \ + exit_release extra <<<"$record" + if [ -n "$extra" ]; then + recovery_status=125 + continue + fi + if [ ! -e "$evidence" ] && [ ! -L "$evidence" ]; then + # The helper installs catchable-signal rollback before allocating. With + # no receipt there is no adopted root; SIGKILL/power-loss is the explicit + # unrecoverable window documented by the provider contract. + reconciled+=("$evidence") + continue + fi + if [ -L "$evidence" ] || [ ! -f "$evidence" ]; then + printf 'pending shared-memory provider evidence is unsafe: %s\n' \ + "$evidence" >&2 + recovery_status=125 + continue + fi + identity="$( + python3 "$FRESH_ROOT/lib/shared_memory_provider.py" identify \ + --evidence "$evidence" + )" || { + recovery_status=125 + continue + } + IFS=$'\t' read -r provider root evidence_sha256 extra <<<"$identity" + if [ "$provider" != "$expected_provider" ] || [ -z "$root" ] || + [ -n "$extra" ] || [[ ! "$evidence_sha256" =~ ^[0-9a-f]{64}$ ]]; then + printf 'pending shared-memory provider identity is invalid: %s\n' \ + "$evidence" >&2 + recovery_status=125 + continue + fi + if [ ! -e "$root" ] && [ ! -L "$root" ]; then + printf 'pending shared-memory provider root disappeared before adoption: %s\n' \ + "$root" >&2 + recovery_status=125 + continue + fi + register_external_shared_memory_provider \ + "$provider" "$root" "$evidence" "$evidence_sha256" \ + "$cleanup_evidence" "$exit_objects" "$exit_release" || { + recovery_status=125 + continue + } + reconciled+=("$evidence") + done + for evidence in "${reconciled[@]}"; do + unregister_pending_external_shared_memory_provider "$evidence" || + recovery_status=125 + done + [ "$recovery_status" -eq 0 ] +} + +unregister_external_shared_memory_provider() { + local remove_root="$1" + local record provider root evidence evidence_sha256 cleanup_evidence + local exit_objects exit_release extra + local remaining=() + for record in "${active_shared_memory_records[@]}"; do + IFS=$'\t' read -r provider root evidence evidence_sha256 cleanup_evidence \ + exit_objects exit_release extra <<<"$record" + [ -z "$extra" ] || return 125 + if [ "$root" != "$remove_root" ]; then + remaining+=("$record") + fi + done + active_shared_memory_records=("${remaining[@]}") +} + +release_external_shared_memory_providers() { + local reason="$1" + local record provider root evidence evidence_sha256 cleanup_evidence + local exit_objects exit_release extra + local release_status=0 + local released_roots=() + + recover_pending_external_shared_memory_providers || release_status=125 + + for record in "${active_shared_memory_records[@]}"; do + IFS=$'\t' read -r provider root evidence evidence_sha256 cleanup_evidence \ + exit_objects exit_release extra <<<"$record" + if [ -n "$extra" ]; then + release_status=125 + continue + fi + + if [ ! -e "$root" ] && [ ! -L "$root" ]; then + printf 'external shared-memory provider root disappeared before exact cleanup: %s\n' \ + "$root" >&2 + release_status=125 + continue + fi + if [ ! -e "$exit_objects" ] && [ ! -L "$exit_objects" ]; then + python3 "$FRESH_ROOT/lib/shared_memory_provider.py" capture-objects \ + --provider "$provider" --root "$root" --evidence "$evidence" \ + --evidence-sha256 "$evidence_sha256" --output "$exit_objects" \ + --require-main no --cgroup-identity post-process-drain || { + release_status=125 + continue + } + fi + # Never remove a directory that still contains an object. A surviving + # object may mean an owned process escaped teardown; retain it and its + # inventory rather than masking the lifecycle failure. + if ! python3 "$FRESH_ROOT/lib/shared_memory_provider.py" assert-empty \ + --provider "$provider" --root "$root" --evidence "$evidence" \ + --evidence-sha256 "$evidence_sha256" --output "$exit_release" \ + --release-kind post-process-drain-v1; then + release_status=125 + continue + fi + if ! python3 "$FRESH_ROOT/lib/shared_memory_provider.py" cleanup \ + --provider "$provider" --root "$root" --evidence "$evidence" \ + --evidence-sha256 "$evidence_sha256" \ + --cleanup-evidence "$cleanup_evidence" --reason "$reason"; then + release_status=125 + continue + fi + released_roots+=("$root") + done + for root in "${released_roots[@]}"; do + unregister_external_shared_memory_provider "$root" || release_status=125 + done + [ "$release_status" -eq 0 ] +} + +register_background_pid() { + active_background_pids+=("$1") +} + +unregister_background_pid() { + local remove_pid="$1" + local pid + local remaining=() + for pid in "${active_background_pids[@]}"; do + [ "$pid" = "$remove_pid" ] || remaining+=("$pid") + done + active_background_pids=("${remaining[@]}") +} + +register_client_process_group() { + active_client_pids+=("$1") + active_client_pgids+=("$2") +} + +unregister_client_process_group() { + local remove_pid="$1" + local index + local remaining_pids=() + local remaining_pgids=() + + for index in "${!active_client_pids[@]}"; do + if [ "${active_client_pids[$index]}" != "$remove_pid" ]; then + remaining_pids+=("${active_client_pids[$index]}") + remaining_pgids+=("${active_client_pgids[$index]}") + fi + done + active_client_pids=("${remaining_pids[@]}") + active_client_pgids=("${remaining_pgids[@]}") +} + +terminate_active_client_process_groups() { + local index pid pgid cleanup_status=0 + + for index in "${!active_client_pids[@]}"; do + pid="${active_client_pids[$index]}" + pgid="${active_client_pgids[$index]}" + if fresh_process_group_exists "$pgid" || fresh_supervision_pid_running "$pid"; then + if ! fresh_terminate_process_group "$pgid" "$pid"; then + cleanup_status=1 + fi + else + # The direct child may already be a waitable zombie even though no live + # member remains. Reap it before releasing the registry entry. + fresh_reap_process_group_leader "$pid" + fi + done + return "$cleanup_status" +} + +assert_no_client_process_residue() { + local context="$1" + local index pid pgid residue=0 cleanup_status=0 + local remaining_pids=() + local remaining_pgids=() + + for index in "${!active_client_pids[@]}"; do + pid="${active_client_pids[$index]}" + pgid="${active_client_pgids[$index]}" + if fresh_process_group_exists "$pgid" || fresh_supervision_pid_running "$pid"; then + residue=1 + printf 'client process residue before %s: pid=%s pgid=%s\n' \ + "$context" "$pid" "$pgid" >&2 + fi + done + if [ "${#active_client_pids[@]}" -gt 0 ]; then + terminate_active_client_process_groups || cleanup_status=1 + fi + for index in "${!active_client_pids[@]}"; do + pid="${active_client_pids[$index]}" + pgid="${active_client_pgids[$index]}" + if fresh_process_group_exists "$pgid" || fresh_supervision_pid_running "$pid"; then + remaining_pids+=("$pid") + remaining_pgids+=("$pgid") + fi + done + active_client_pids=("${remaining_pids[@]}") + active_client_pgids=("${remaining_pgids[@]}") + [ "$residue" -eq 0 ] && [ "$cleanup_status" -eq 0 ] && + [ "${#active_client_pids[@]}" -eq 0 ] +} + +wait_for_pid_exit() { + local pid="$1" + local tenths="$2" + local i + for ((i = 0; i < tenths; i++)); do + pid_is_running "$pid" || return 0 + sleep 0.1 + done + return 1 +} + +pid_is_running() { + local pid="$1" + local state + + kill -0 "$pid" 2>/dev/null || return 1 + state="$(ps -o stat= -p "$pid" 2>/dev/null | awk 'NR == 1 { print $1 }')" + case "$state" in + ""|Z*) return 1 ;; + *) return 0 ;; + esac +} + +captured_server_cgroup_dir="" +captured_server_cgroup_identity="" +captured_server_cgroup_membership_path="" +capture_server_cgroup_identity() { + local pid="$1" + local unit="$2" + local deadline relative="" directory identity="" + + captured_server_cgroup_dir="" + captured_server_cgroup_identity="" + captured_server_cgroup_membership_path="" + [ -n "$unit" ] || return 0 + deadline=$(( $(now_ms) + 5000 )) + while [ "$(now_ms)" -lt "$deadline" ]; do + if [ -r "/proc/$pid/cgroup" ]; then + relative="$(awk -F: '$1 == "0" { print $3; exit }' "/proc/$pid/cgroup")" + case "$relative" in + */"$unit.scope") break ;; + esac + fi + sleep 0.05 + done + case "$relative" in + /*/"$unit.scope") ;; + *) printf 'server did not enter expected cgroup scope: pid=%s unit=%s path=%s\n' \ + "$pid" "$unit" "$relative" >&2; return 125 ;; + esac + directory="/sys/fs/cgroup$relative" + [ -d "$directory" ] && [ -r "$directory/cgroup.procs" ] || { + printf 'server cgroup scope is unavailable: %s\n' "$directory" >&2 + return 125 + } + identity="$(fresh_path_identity "$directory")" || return 125 + captured_server_cgroup_dir="$directory" + captured_server_cgroup_identity="$identity" + captured_server_cgroup_membership_path="$relative" +} + +captured_server_cgroup_memory_max_bytes=none +captured_server_cgroup_memory_high_bytes=none +captured_server_cgroup_swap_max_bytes=none +capture_server_cgroup_limits() { + local directory="$1" + local expected_identity="$2" + local before_identity after_identity memory_max memory_high swap_max + + captured_server_cgroup_memory_max_bytes=none + captured_server_cgroup_memory_high_bytes=none + captured_server_cgroup_swap_max_bytes=none + [ -n "$directory" ] && [ -n "$expected_identity" ] || return 0 + before_identity="$(fresh_path_identity "$directory")" || return 125 + [ "$before_identity" = "$expected_identity" ] || { + printf 'server cgroup identity changed before limit capture: expected=%s actual=%s\n' \ + "$expected_identity" "$before_identity" >&2 + return 125 + } + for controller in memory.max memory.high memory.swap.max; do + [ -r "$directory/$controller" ] || { + printf 'required server cgroup controller file is unavailable: %s/%s\n' \ + "$directory" "$controller" >&2 + return 125 + } + done + memory_max="$(<"$directory/memory.max")" || return 125 + memory_high="$(<"$directory/memory.high")" || return 125 + swap_max="$(<"$directory/memory.swap.max")" || return 125 + case "$memory_max:$memory_high:$swap_max" in + *[!0-9:]*) + printf 'server cgroup limits are not exact finite byte counts: %s:%s:%s\n' \ + "$memory_max" "$memory_high" "$swap_max" >&2 + return 125 + ;; + esac + after_identity="$(fresh_path_identity "$directory")" || return 125 + [ "$after_identity" = "$expected_identity" ] || { + printf 'server cgroup identity changed during limit capture: expected=%s actual=%s\n' \ + "$expected_identity" "$after_identity" >&2 + return 125 + } + captured_server_cgroup_memory_max_bytes="$memory_max" + captured_server_cgroup_memory_high_bytes="$memory_high" + captured_server_cgroup_swap_max_bytes="$swap_max" +} + +record_server_limits() { + local target="$1" + local record="$2" + local pid="$3" + local pre_soft pre_hard actual_soft actual_hard launch_status status="passed" + local proc_soft="" proc_hard="" + + [ -s "$record" ] || { + printf '%s\t%s\t\t\t\t\tmissing\t%s\n' \ + "$target" "$libpq_latency_soft_nofile" "$record" >>"$server_limits_tsv" + return 1 + } + pre_soft="$(awk -F= '$1 == "pre_soft_nofile" { print $2 }' "$record")" + pre_hard="$(awk -F= '$1 == "pre_hard_nofile" { print $2 }' "$record")" + actual_soft="$(awk -F= '$1 == "actual_soft_nofile" { print $2 }' "$record")" + actual_hard="$(awk -F= '$1 == "actual_hard_nofile" { print $2 }' "$record")" + launch_status="$(awk -F= '$1 == "status" { print $2 }' "$record")" + if [ "$libpq_latency_samples" -gt 0 ]; then + if [ "$launch_status" != "applied" ] || + [ "$actual_soft" != "$libpq_latency_soft_nofile" ] || + [ "$actual_hard" != "$pre_hard" ]; then + status="failed" + fi + if [ -r "/proc/$pid/limits" ]; then + read -r proc_soft proc_hard < <( + awk '$1 == "Max" && $2 == "open" && $3 == "files" { print $4, $5; exit }' \ + "/proc/$pid/limits" + ) + if [ "$proc_soft" != "$libpq_latency_soft_nofile" ] || + [ "$proc_hard" != "$pre_hard" ]; then + status="failed" + fi + actual_soft="$proc_soft" + actual_hard="$proc_hard" + elif [ "$(uname -s 2>/dev/null || printf unknown)" = Linux ]; then + status="failed" + fi + elif [ "$launch_status" != "not-requested" ]; then + status="failed" + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$libpq_latency_soft_nofile" "$pre_soft" "$pre_hard" \ + "$actual_soft" "$actual_hard" "$status" "$record" >>"$server_limits_tsv" + [ "$status" = "passed" ] +} + +stop_server() { + local pid="$1" + local pgid="$2" + local birth_identity="$3" + local port="$4" + local cgroup_dir="$5" + local cgroup_identity="$6" + local server_log="$7" + local shutdown_report="$8" + local target="$9" + local cgroup_path="${10}" + local wait_status=0 + local forced=none + local clean_shutdown=0 + local orderly_int=0 process_group_residue=0 cgroup_residue=0 port_residue=0 + local lifecycle_status="passed" + + if pid_is_running "$pid"; then + # Wasmer's CLI signal bridge and PostgreSQL both treat SIGINT as a fast, + # orderly database shutdown. This exercises guest shutdown instead of + # merely terminating the host runtime. + if fresh_signal_owned_pid INT "$pid" "$birth_identity"; then + orderly_int=1 + else + lifecycle_status="failed" + fi + if ! wait_for_pid_exit "$pid" 300; then + forced=term + fresh_terminate_owned_process_group "$pgid" "$pid" "$birth_identity" \ + "$process_term_grace_ms" "$process_kill_grace_ms" || lifecycle_status="failed" + fi + fi + + if fresh_pid_matches_birth_identity "$pid" "$birth_identity"; then + set +e + wait "$pid" 2>/dev/null + wait_status=$? + set -e + fi + if fresh_process_group_exists "$pgid"; then + [ "$forced" != none ] || forced=term + fresh_terminate_owned_process_group "$pgid" "$pid" "$birth_identity" \ + "$process_term_grace_ms" "$process_kill_grace_ms" || lifecycle_status="failed" + fi + if fresh_process_group_exists "$pgid"; then + process_group_residue=1 + lifecycle_status="failed" + fi + if ! fresh_wait_cgroup_empty "$cgroup_dir" "$cgroup_identity" \ + "$process_kill_grace_ms"; then + cgroup_residue=1 + lifecycle_status="failed" + fi + if ! fresh_wait_tcp_port_closed 127.0.0.1 "$port" "$process_kill_grace_ms"; then + port_residue=1 + lifecycle_status="failed" + fi + unregister_server_pid "$pid" + + if [ -f "$server_log" ] && grep -q 'database system is shut down' "$server_log"; then + clean_shutdown=1 + fi + { + printf 'pid=%s\npgid=%s\nbirth_identity=%s\n' "$pid" "$pgid" "$birth_identity" + printf 'wait_status=%s\n' "$wait_status" + printf 'forced=%s\n' "$forced" + printf 'clean_shutdown_marker=%s\n' "$clean_shutdown" + printf 'process_group_residue=%s\n' "$process_group_residue" + printf 'cgroup_residue=%s\n' "$cgroup_residue" + printf 'port_residue=%s\n' "$port_residue" + printf 'status=%s\n' "$lifecycle_status" + } >"$shutdown_report" + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$pid" "$pgid" "$birth_identity" "$cgroup_path" \ + "$cgroup_identity" "$orderly_int" "$forced" "$wait_status" "$clean_shutdown" \ + "$process_group_residue" "$cgroup_residue" "$port_residue" \ + "$lifecycle_status" "$shutdown_report" >>"$server_lifecycle_tsv" + + [ "$forced" = none ] && [ "$wait_status" -eq 0 ] && + [ "$clean_shutdown" -eq 1 ] && [ "$lifecycle_status" = "passed" ] +} + +cleanup_active_servers() { + local status=$? + local pid index pgid identity port cgroup_dir cgroup_identity + trap - EXIT HUP INT TERM + if ! terminate_active_client_process_groups; then + [ "$status" -ne 0 ] || status=125 + fi + active_client_pids=() + active_client_pgids=() + for index in "${!active_server_pids[@]}"; do + pid="${active_server_pids[$index]}" + pgid="${active_server_pgids[$index]}" + identity="${active_server_identities[$index]}" + port="${active_server_ports[$index]}" + cgroup_dir="${active_server_cgroup_dirs[$index]}" + cgroup_identity="${active_server_cgroup_identities[$index]}" + if pid_is_running "$pid"; then + fresh_signal_owned_pid INT "$pid" "$identity" || [ "$status" -ne 0 ] || status=125 + wait_for_pid_exit "$pid" 50 || true + fi + if fresh_process_group_exists "$pgid"; then + fresh_terminate_owned_process_group "$pgid" "$pid" "$identity" || + { [ "$status" -ne 0 ] || status=125; } + elif fresh_pid_matches_birth_identity "$pid" "$identity"; then + wait "$pid" 2>/dev/null || true + fi + fresh_process_group_exists "$pgid" && { [ "$status" -ne 0 ] || status=125; } + fresh_wait_cgroup_empty "$cgroup_dir" "$cgroup_identity" "$process_kill_grace_ms" || + { [ "$status" -ne 0 ] || status=125; } + fresh_wait_tcp_port_closed 127.0.0.1 "$port" "$process_kill_grace_ms" || + { [ "$status" -ne 0 ] || status=125; } + done + for pid in "${active_background_pids[@]}"; do + if pid_is_running "$pid"; then + kill -TERM "$pid" 2>/dev/null || true + wait_for_pid_exit "$pid" 50 || kill -KILL "$pid" 2>/dev/null || true + fi + wait "$pid" 2>/dev/null || true + done + if ! release_external_shared_memory_providers exit-drain; then + [ "$status" -ne 0 ] || status=125 + fi + if [ "$status" -ne 0 ]; then + invalidate_derived_summary "$status" "" || true + invalidate_libpq_latency_summary "$status" "" || true + fi + exit "$status" +} + +invalidate_derived_summary() { + local invalid_status="$1" + local invalid_target="${2:-}" + local tmp + + [ -s "${summary_tsv:-}" ] || return 0 + tmp="$summary_tsv.invalid.$$" + awk -F '\t' -v OFS='\t' -v invalid_status="$invalid_status" \ + -v invalid_target="$invalid_target" ' + NR == 1 { print; next } + invalid_target == "" || $1 == invalid_target { + if ($3 == "0") { + $3 = invalid_status + } + $10 = "" + } + { print } + ' "$summary_tsv" >"$tmp" + mv "$tmp" "$summary_tsv" +} + +invalidate_libpq_latency_summary() { + local invalid_status="$1" + local invalid_target="${2:-}" + local tmp + + [ -s "${libpq_latency_tsv:-}" ] || return 0 + tmp="$libpq_latency_tsv.invalid.$$" + awk -F '\t' -v OFS='\t' -v invalid_status="$invalid_status" \ + -v invalid_target="$invalid_target" ' + NR == 1 { print; next } + (invalid_target == "" || $2 == invalid_target) && $4 == "ok" { + $4 = "invalidated_exit_" invalid_status + for (column = 8; column <= 13; column++) $column = "" + } + { print } + ' "$libpq_latency_tsv" >"$tmp" + mv "$tmp" "$libpq_latency_tsv" +} + +append_libpq_latency_failure() { + local target="$1" + local mode="$2" + local status="$3" + local raw="$4" + + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + 1 "$target" "$mode" "$status" CLOCK_MONOTONIC \ + "$libpq_latency_warmup" "$libpq_latency_samples" \ + "" "" "" "" "" "" "$raw" "$libpq_latency_libpq_path" \ + "$libpq_latency_libpq_sha256" "$libpq_latency_probe_sha256" >>"$libpq_latency_tsv" +} + +append_libpq_latency_target_failure() { + local target="$1" + local status="$2" + local target_report_dir="$3" + local mode + + [ "$libpq_latency_samples" -gt 0 ] || return 0 + for mode in persistent reconnect; do + append_libpq_latency_failure "$target" "$mode" "$status" \ + "$target_report_dir/libpq-latency/$mode.raw.tsv" + done +} + +host_fd_snapshot_total="" +host_fd_snapshot_observed=0 +host_fd_snapshot_expected=0 +host_fd_snapshot_status="raced" + +collect_exact_host_fd_snapshot() { + local root_pid="$1" + local kernel snapshot_before snapshot_after pids metrics + + kernel="$(uname -s 2>/dev/null || printf unknown)" + host_fd_snapshot_total="" + host_fd_snapshot_observed=0 + host_fd_snapshot_expected=0 + host_fd_snapshot_status="raced" + snapshot_before="$(collect_process_tree_snapshot "$root_pid" 2>/dev/null || true)" + pids="$(printf '%s\n' "$snapshot_before" | awk -F '\t' 'NF >= 2 { print $1 }' | tr '\n' ' ')" + host_fd_snapshot_expected="$(printf '%s\n' "$snapshot_before" | + awk -F '\t' 'NF >= 2 { count++ } END { print count + 0 }')" + [ "$host_fd_snapshot_expected" -gt 0 ] || return 1 + metrics="$(fresh_collect_host_fd_occupancy "$kernel" /proc "$pids")" || return + host_fd_snapshot_total="$(printf '%s\n' "$metrics" | awk -F '\t' '{ print $1 }')" + host_fd_snapshot_observed="$(printf '%s\n' "$metrics" | awk -F '\t' '{ print $2 }')" + host_fd_snapshot_expected="$(printf '%s\n' "$metrics" | awk -F '\t' '{ print $3 }')" + host_fd_snapshot_status="$(printf '%s\n' "$metrics" | awk -F '\t' '{ print $4 }')" + snapshot_after="$(collect_process_tree_snapshot "$root_pid" 2>/dev/null || true)" + if [ -z "$snapshot_after" ] || [ "$snapshot_before" != "$snapshot_after" ]; then + host_fd_snapshot_total="" + host_fd_snapshot_status="raced" + fi +} + +append_host_fd_checkpoint() { + local target="$1" + local mode="$2" + local stage="$3" + + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$mode" "$stage" "$(now_ms)" "$host_fd_snapshot_total" \ + "$host_fd_snapshot_observed" "$host_fd_snapshot_expected" \ + "$host_fd_snapshot_status" >>"$host_fd_checkpoints_tsv" +} + +capture_host_fd_checkpoint() { + local target="$1" + local mode="$2" + local stage="$3" + local root_pid="$4" + + collect_exact_host_fd_snapshot "$root_pid" || true + append_host_fd_checkpoint "$target" "$mode" "$stage" + [ "$host_fd_snapshot_status" = ok ] +} + +capture_quiescent_host_fd_checkpoint() { + local target="$1" + local mode="$2" + local root_pid="$3" + local deadline stable=0 previous="" + + deadline=$(( $(now_ms) + 5000 )) + while :; do + sleep 0.05 + collect_exact_host_fd_snapshot "$root_pid" || true + if [ "$host_fd_snapshot_status" = ok ]; then + if [ "$host_fd_snapshot_total" = "$previous" ]; then + stable=$((stable + 1)) + else + stable=1 + previous="$host_fd_snapshot_total" + fi + [ "$stable" -ge 3 ] && break + else + stable=0 + previous="" + fi + [ "$(now_ms)" -lt "$deadline" ] || break + done + append_host_fd_checkpoint "$target" "$mode" quiescent + [ "$host_fd_snapshot_status" = ok ] && [ "$stable" -ge 3 ] +} + +run_libpq_latency_mode() { + local target="$1" + local mode="$2" + local conn="$3" + local target_report_dir="$4" + local resource_phase_file="$5" + local server_pid="$6" + local latency_dir="$target_report_dir/libpq-latency" + local raw="$latency_dir/$mode.raw.tsv" + local mode_summary="$latency_dir/$mode.summary.tsv" + local probe_log="$latency_dir/$mode.probe.log" + local summary_log="$latency_dir/$mode.summary.log" + local probe_status summary_status fd_before_status fd_after_status + local fd_quiescent_status fd_validation_status fd_summary host_kernel + + mkdir -p "$latency_dir" + set_resource_phase "$resource_phase_file" "libpq-latency:$mode" + if capture_host_fd_checkpoint "$target" "$mode" before "$server_pid"; then + fd_before_status=0 + else + fd_before_status=$? + fi + set +e + run_logged_timeout "$timeout_seconds" "$probe_log" \ + env -u LD_LIBRARY_PATH -u LD_PRELOAD -u LD_AUDIT \ + -u DYLD_LIBRARY_PATH -u DYLD_FALLBACK_LIBRARY_PATH -u DYLD_INSERT_LIBRARIES \ + PGCONNECT_TIMEOUT=10 "$libpq_latency_probe_bin" \ + --conninfo "$conn" \ + --mode "$mode" \ + --warmup "$libpq_latency_warmup" \ + --samples "$libpq_latency_samples" \ + --output "$raw" + probe_status=$? + set -e + if capture_host_fd_checkpoint "$target" "$mode" after "$server_pid"; then + fd_after_status=0 + else + fd_after_status=$? + fi + if capture_quiescent_host_fd_checkpoint "$target" "$mode" "$server_pid"; then + fd_quiescent_status=0 + else + fd_quiescent_status=$? + fi + fd_summary="$latency_dir/$mode.host-fd-churn.tsv" + host_kernel="$(uname -s 2>/dev/null || printf unknown)" + if [ "$host_kernel" = Linux ]; then + set +e + "$FRESH_ROOT/bin/validate-host-fd-churn.sh" "$host_fd_checkpoints_tsv" \ + "$target" "$mode" "$libpq_latency_host_fd_allowance" "$fd_summary" + fd_validation_status=$? + set -e + if [ -s "$fd_summary" ]; then + sed -n '2p' "$fd_summary" >>"$host_fd_churn_tsv" + else + printf '%s\t%s\t\t\t\t\t%s\tfailed\n' "$target" "$mode" \ + "$libpq_latency_host_fd_allowance" >>"$host_fd_churn_tsv" + fi + else + fd_validation_status=0 + fd_before_status=0 + fd_after_status=0 + fd_quiescent_status=0 + printf '%s\t%s\t\t\t\t\t%s\tunsupported\n' "$target" "$mode" \ + "$libpq_latency_host_fd_allowance" >>"$host_fd_churn_tsv" + fi + if [ "$probe_status" -ne 0 ]; then + append_libpq_latency_failure "$target" "$mode" "probe_exit_$probe_status" "$raw" + return "$probe_status" + fi + if [ "$fd_before_status" -ne 0 ] || [ "$fd_after_status" -ne 0 ] || + [ "$fd_quiescent_status" -ne 0 ] || [ "$fd_validation_status" -ne 0 ]; then + append_libpq_latency_failure "$target" "$mode" "host_fd_churn_failed" "$raw" + return 1 + fi + + set +e + run_logged_timeout "$timeout_seconds" "$summary_log" \ + "$FRESH_ROOT/bin/summarize-libpq-latency.sh" \ + --raw "$raw" \ + --output "$mode_summary" \ + --target "$target" \ + --mode "$mode" \ + --warmup "$libpq_latency_warmup" \ + --samples "$libpq_latency_samples" \ + --libpq-path "$libpq_latency_libpq_path" \ + --libpq-sha256 "$libpq_latency_libpq_sha256" \ + --probe-sha256 "$libpq_latency_probe_sha256" + summary_status=$? + set -e + if [ "$summary_status" -ne 0 ]; then + append_libpq_latency_failure "$target" "$mode" "summary_exit_$summary_status" "$raw" + return "$summary_status" + fi + if [ "$(awk 'END { print NR }' "$mode_summary")" -ne 2 ]; then + append_libpq_latency_failure "$target" "$mode" "summary_row_count_invalid" "$raw" + return 1 + fi + sed -n '2p' "$mode_summary" >>"$libpq_latency_tsv" +} + +run_libpq_latency_suite() { + local target="$1" + local conn="$2" + local target_report_dir="$3" + local resource_phase_file="$4" + local server_pid="$5" + local mode mode_status + local status=0 + + for mode in persistent reconnect; do + if run_libpq_latency_mode "$target" "$mode" "$conn" "$target_report_dir" \ + "$resource_phase_file" "$server_pid" + then + mode_status=0 + else + mode_status=$? + fi + if [ "$mode_status" -ne 0 ]; then + status=1 + fi + done + set_resource_phase "$resource_phase_file" "idle" + return "$status" +} + +lifecycle_phase_sequence=0 +append_lifecycle_phase_marker() { + local log="$1" + local nonce="$2" + local phase="$3" + local observer_pid="$4" + local marker_mono_ns="${5:-$(now_ns)}" + + lifecycle_phase_sequence=$((lifecycle_phase_sequence + 1)) + printf 'wasix-runtime-phase-v1\tnonce=%s\tseq=%s\tmono_ns=%s\tphase=%s\tobserver_pid=%s\n' \ + "$nonce" "$lifecycle_phase_sequence" "$marker_mono_ns" "$phase" "$observer_pid" >>"$log" +} + +validate_walwriter_stabilization_state_file() { + local state_file="$1" + + [ -f "$state_file" ] && [ ! -L "$state_file" ] && awk -F '\t' ' + NF == 6 && + $1 ~ /^(0|[1-9][0-9]*)$/ && + $2 ~ /^(0|[1-9][0-9]*)$/ && + $3 ~ /^[1-9][0-9]*$/ && + $4 ~ /^[1-9][0-9]*$/ && + $5 ~ /^[0-9A-F]+\/[0-9A-F]+$/ && + ($6 == "t" || $6 == "f") { valid = 1 } + END { exit(NR == 1 && valid ? 0 : 1) } + ' "$state_file" +} + +capture_walwriter_stabilization_state() { + local conn="$1" + local target_lsn="$2" + local state_file="$3" + local diagnostics="$4" + local deadline_ms="$5" + local current_ms remaining_ms + + [[ "$target_lsn" =~ ^[0-9A-F]+/[0-9A-F]+$ ]] || return 2 + current_ms="$(now_ms)" + if [ "$current_ms" -ge "$deadline_ms" ]; then + return 124 + fi + remaining_ms=$((deadline_ms - current_ms)) + if ! fresh_run_process_group_timeout_ms "$remaining_ms" -- \ + "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -XAtq -F $'\t' \ + -v ON_ERROR_STOP=1 -c " + WITH current_flush AS MATERIALIZED ( + SELECT pg_current_wal_flush_lsn() AS lsn + ) + SELECT + io.writes::bigint, + io.write_bytes::bigint, + (extract(epoch FROM io.stats_reset) * 1000000)::bigint, + settings.setting::bigint, + current_flush.lsn, + current_flush.lsn >= '$target_lsn'::pg_lsn + FROM pg_stat_io AS io + CROSS JOIN current_flush + CROSS JOIN pg_settings AS settings + WHERE io.backend_type = 'walwriter' + AND io.object = 'wal' + AND io.context = 'normal' + AND settings.name = 'wal_writer_delay' + AND settings.unit = 'ms' + " >"$state_file" 2>>"$diagnostics" + then + return 1 + fi + if ! validate_walwriter_stabilization_state_file "$state_file"; then + printf 'expected one applicable walwriter/wal/normal pg_stat_io row and wal_writer_delay in ms\n' \ + >>"$diagnostics" + return 1 + fi +} + +emit_walwriter_stabilization_target() { + local conn="$1" + local target_file="$2" + local diagnostics="$3" + local deadline_ms="$4" + local current_ms remaining_ms target_lsn + + current_ms="$(now_ms)" + if [ "$current_ms" -ge "$deadline_ms" ]; then + return 124 + fi + remaining_ms=$((deadline_ms - current_ms)) + if ! fresh_run_process_group_timeout_ms "$remaining_ms" -- \ + "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -XAtq -v ON_ERROR_STOP=1 \ + -c 'SELECT pg_log_standby_snapshot()' >"$target_file" 2>>"$diagnostics" + then + return 1 + fi + if ! awk ' + /^[0-9A-F]+\/[0-9A-F]+$/ && $0 != "0/0" { valid = 1 } + END { exit(NR == 1 && valid ? 0 : 1) } + ' "$target_file"; then + printf 'pg_log_standby_snapshot did not return one applicable nonzero LSN\n' \ + >>"$diagnostics" + return 1 + fi + target_lsn="$(sed -n '1p' "$target_file")" + printf '%s\n' "$target_lsn" +} + +append_walwriter_stabilization_record() { + local log="$1" + local nonce="$2" + local before_writes="$3" + local after_writes="$4" + local before_write_bytes="$5" + local after_write_bytes="$6" + local before_stats_reset="$7" + local after_stats_reset="$8" + local target_lsn="$9" + local observed_flush_lsn="${10}" + local wal_writer_delay_ms="${11}" + local start_mono_ns="${12}" + local end_mono_ns="${13}" + local status="${14}" + local observer_pid="${15}" + + printf 'wasix-runtime-stabilization-v1\tnonce=%s\tmethod=pg_log_standby_snapshot\tbefore_writes=%s\tafter_writes=%s\tbefore_write_bytes=%s\tafter_write_bytes=%s\tbefore_stats_reset=%s\tafter_stats_reset=%s\ttarget_lsn=%s\tobserved_flush_lsn=%s\twal_writer_delay_ms=%s\tstart_mono_ns=%s\tend_mono_ns=%s\tstatus=%s\tobserver_pid=%s\n' \ + "$nonce" "$before_writes" "$after_writes" "$before_write_bytes" \ + "$after_write_bytes" "$before_stats_reset" "$after_stats_reset" \ + "$target_lsn" "$observed_flush_lsn" "$wal_writer_delay_ms" \ + "$start_mono_ns" "$end_mono_ns" "$status" "$observer_pid" >>"$log" +} + +run_walwriter_stabilization() { + local conn="$1" + local wait_dump_log="$2" + local nonce="$3" + local observer_pid="$4" + local target_report_dir="$5" + local diagnostics="$target_report_dir/lifecycle-walwriter-stabilization.log" + local before_state="$target_report_dir/lifecycle-walwriter-before.raw.tsv" + local after_state="$target_report_dir/lifecycle-walwriter-after.raw.tsv" + local target_file="$target_report_dir/lifecycle-walwriter-target-lsn.txt" + local report="$target_report_dir/lifecycle-walwriter-stabilization.tsv" + local deadline_ms start_mono_ns end_mono_ns target_lsn + local before_writes before_write_bytes before_stats_reset wal_writer_delay_ms + local before_flush_lsn before_flush_reached + local after_writes after_write_bytes after_stats_reset after_delay_ms + local observed_flush_lsn flush_reached current_ms + + : >"$diagnostics" + deadline_ms=$(( $(now_ms) + timeout_seconds * 1000 )) + start_mono_ns="$(now_ns)" + capture_walwriter_stabilization_state "$conn" "0/0" "$before_state" \ + "$diagnostics" "$deadline_ms" || return + IFS=$'\t' read -r before_writes before_write_bytes before_stats_reset \ + wal_writer_delay_ms before_flush_lsn before_flush_reached <"$before_state" + target_lsn="$(emit_walwriter_stabilization_target \ + "$conn" "$target_file" "$diagnostics" "$deadline_ms")" || return + + while :; do + capture_walwriter_stabilization_state "$conn" "$target_lsn" "$after_state" \ + "$diagnostics" "$deadline_ms" || return + IFS=$'\t' read -r after_writes after_write_bytes after_stats_reset \ + after_delay_ms observed_flush_lsn flush_reached <"$after_state" + if [ "$after_stats_reset" != "$before_stats_reset" ]; then + printf 'pg_stat_io stats_reset changed during WAL-writer stabilization\n' \ + >>"$diagnostics" + return 1 + fi + if [ "$after_delay_ms" != "$wal_writer_delay_ms" ]; then + printf 'wal_writer_delay changed during WAL-writer stabilization\n' \ + >>"$diagnostics" + return 1 + fi + if [ "$after_writes" -gt "$before_writes" ] && + [ "$after_write_bytes" -gt "$before_write_bytes" ] && + [ "$flush_reached" = t ]; then + break + fi + current_ms="$(now_ms)" + if [ "$current_ms" -ge "$deadline_ms" ]; then + printf 'WAL writer did not write and flush the standby-snapshot barrier within %s seconds\n' \ + "$timeout_seconds" >>"$diagnostics" + return 124 + fi + sleep 0.02 + done + end_mono_ns="$(now_ns)" + append_walwriter_stabilization_record "$wait_dump_log" "$nonce" \ + "$before_writes" "$after_writes" "$before_write_bytes" "$after_write_bytes" \ + "$before_stats_reset" "$after_stats_reset" "$target_lsn" \ + "$observed_flush_lsn" "$wal_writer_delay_ms" "$start_mono_ns" \ + "$end_mono_ns" passed "$observer_pid" + { + printf 'method\tbefore_writes\tafter_writes\tbefore_write_bytes\tafter_write_bytes\tstats_reset\ttarget_lsn\tobserved_flush_lsn\twal_writer_delay_ms\tstart_mono_ns\tend_mono_ns\tstatus\n' + printf 'pg_log_standby_snapshot\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\tpassed\n' \ + "$before_writes" "$after_writes" "$before_write_bytes" "$after_write_bytes" \ + "$before_stats_reset" "$target_lsn" "$observed_flush_lsn" \ + "$wal_writer_delay_ms" "$start_mono_ns" "$end_mono_ns" + } >"$report" +} + +write_lifecycle_fence_request() { + local request_file="$1" + local nonce="$2" + local request_sequence="$3" + local phase="$4" + local observer_pid="$5" + local pending="$request_file.pending.$$.$request_sequence" + + rm -f -- "$pending" + if ! printf 'wasix-runtime-fence-request-v1\tnonce=%s\trequest_seq=%s\tphase=%s\tobserver_pid=%s\n' \ + "$nonce" "$request_sequence" "$phase" "$observer_pid" >"$pending"; then + rm -f -- "$pending" + return 1 + fi + if ! mv -f -- "$pending" "$request_file"; then + rm -f -- "$pending" + return 1 + fi +} + +wait_for_lifecycle_fence_ack() { + local ack_file="$1" + local nonce="$2" + local request_sequence="$3" + local phase="$4" + local observer_pid="$5" + local server_pid="$6" + local deadline + + deadline=$(( $(now_ms) + timeout_seconds * 1000 )) + while [ "$(now_ms)" -lt "$deadline" ]; do + if [ -f "$ack_file" ] && [ ! -L "$ack_file" ] && awk -F '\t' \ + -v nonce="$nonce" \ + -v request_sequence="$request_sequence" \ + -v phase="$phase" \ + -v observer_pid="$observer_pid" ' + $1 == "wasix-runtime-fence-commit-v1" && NF == 9 && + $2 == "nonce=" nonce && + $3 ~ /^seq=[1-9][0-9]*$/ && + $4 ~ /^mono_ns=[1-9][0-9]*$/ && + $5 == "phase=" phase && + $6 == "observer_pid=" observer_pid && + $7 ~ /^observer_tid=[1-9][0-9]*$/ && + $8 == "request_seq=" request_sequence && + $9 ~ /^fence_end_offset=[1-9][0-9]*$/ { valid = 1 } + END { exit(NR == 1 && valid ? 0 : 1) } + ' "$ack_file" + then + return 0 + fi + if ! pid_is_running "$server_pid"; then + printf 'server exited before runtime writer acknowledged %s fence request %s\n' \ + "$phase" "$request_sequence" >&2 + return 1 + fi + sleep 0.02 + done + printf 'runtime writer did not acknowledge %s fence request %s within %s seconds\n' \ + "$phase" "$request_sequence" "$timeout_seconds" >&2 + return 124 +} + +request_lifecycle_fence() { + local request_file="$1" + local ack_file="$2" + local nonce="$3" + local request_sequence="$4" + local phase="$5" + local observer_pid="$6" + local server_pid="$7" + + # A committed ACK belongs to one request publication only. Remove the prior + # inode before the atomic request rename so a stale ACK cannot satisfy the + # next request even when all of its semantic fields happen to match. + rm -f -- "$ack_file" || return + write_lifecycle_fence_request "$request_file" "$nonce" "$request_sequence" \ + "$phase" "$observer_pid" || return + wait_for_lifecycle_fence_ack "$ack_file" "$nonce" "$request_sequence" "$phase" \ + "$observer_pid" "$server_pid" +} + +initialize_lifecycle_memory_checkpoints() { + local output="$1" + + printf 'schema_version\tnonce\tsequence\tstage\tcompleted_reconnects\trequested_reconnects\tcheckpoint_every\tquiescence_seconds\tquiescence_start_ns\tquiescence_end_ns\tmonotonic_before_ns\tmonotonic_after_ns\tcapture_elapsed_ns\tserver_pid\tserver_birth_identity\tpss_kib\tpss_anon_kib\tanonymous_kib\theap_pss_kib\theap_private_kib\theap_mappings\tstatus\n' \ + >"$output" +} + +capture_lifecycle_memory_checkpoint() { + local output="$1" + local nonce="$2" + local sequence="$3" + local stage="$4" + local completed="$5" + local server_pid="$6" + local expected_birth_identity="$7" + local quiescence_seconds="$8" + local quiescence_start_ns="$9" + local quiescence_end_ns="${10}" + local before_ns after_ns elapsed_ns before_identity after_identity + local rollup_metrics heap_metrics metric + local pss_kib=0 pss_anon_kib=0 anonymous_kib=0 + local heap_pss_kib=0 heap_private_kib=0 heap_mappings=0 status=passed + + before_ns="$(now_ns)" || return + before_identity="$(fresh_process_birth_identity "$server_pid" 2>/dev/null || true)" + if [ "$before_identity" != "$expected_birth_identity" ]; then + status=failed + fi + if [ "$status" = passed ]; then + rollup_metrics="$( + awk ' + /^Pss:[[:space:]]/ { pss = $2; saw_pss = 1 } + /^Pss_Anon:[[:space:]]/ { pss_anon = $2; saw_pss_anon = 1 } + /^Anonymous:[[:space:]]/ { anonymous = $2; saw_anonymous = 1 } + END { + if (!saw_pss || !saw_pss_anon || !saw_anonymous) exit 1 + printf "%.0f\t%.0f\t%.0f\n", pss, pss_anon, anonymous + } + ' "/proc/$server_pid/smaps_rollup" 2>/dev/null + )" || status=failed + fi + if [ "$status" = passed ]; then + IFS=$'\t' read -r pss_kib pss_anon_kib anonymous_kib <<<"$rollup_metrics" + for metric in "$pss_kib" "$pss_anon_kib" "$anonymous_kib"; do + case "$metric" in ''|*[!0-9]*) status=failed ;; esac + done + fi + if [ "$status" = passed ]; then + heap_metrics="$( + awk ' + /^[[:xdigit:]]+-[[:xdigit:]]+[[:space:]]/ { + in_heap = ($NF == "[heap]") + if (in_heap) mappings += 1 + next + } + in_heap && /^Pss:[[:space:]]/ { pss += $2 } + in_heap && /^Private_Clean:[[:space:]]/ { private += $2 } + in_heap && /^Private_Dirty:[[:space:]]/ { private += $2 } + in_heap && /^Private_Hugetlb:[[:space:]]/ { private += $2 } + END { printf "%.0f\t%.0f\t%.0f\n", pss, private, mappings } + ' "/proc/$server_pid/smaps" 2>/dev/null + )" || status=failed + fi + if [ "$status" = passed ]; then + IFS=$'\t' read -r heap_pss_kib heap_private_kib heap_mappings <<<"$heap_metrics" + for metric in "$heap_pss_kib" "$heap_private_kib" "$heap_mappings"; do + case "$metric" in ''|*[!0-9]*) status=failed ;; esac + done + fi + after_identity="$(fresh_process_birth_identity "$server_pid" 2>/dev/null || true)" + [ "$after_identity" = "$expected_birth_identity" ] || status=failed + after_ns="$(now_ns)" || return + elapsed_ns=$((after_ns - before_ns)) + [ "$elapsed_ns" -gt 0 ] || status=failed + + printf 'oliphaunt.wasix-postmaster.lifecycle-memory-checkpoint.v1\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$nonce" "$sequence" "$stage" "$completed" \ + "$wasix_lifecycle_reconnects" \ + "$wasix_lifecycle_memory_checkpoint_every" "$quiescence_seconds" \ + "$quiescence_start_ns" "$quiescence_end_ns" \ + "$before_ns" "$after_ns" "$elapsed_ns" "$server_pid" \ + "$expected_birth_identity" "$pss_kib" "$pss_anon_kib" \ + "$anonymous_kib" "$heap_pss_kib" "$heap_private_kib" \ + "$heap_mappings" "$status" >>"$output" + if [ "$status" != passed ]; then + printf 'failed to capture quiescent lifecycle memory checkpoint %s at reconnect %s\n' \ + "$sequence" "$completed" >&2 + return 1 + fi +} + +run_lifecycle_reconnect_churn() { + local conn="$1" + local output="$2" + local evidence_log="$3" + local nonce="$4" + local observer_pid="$5" + local resource_phase_file="$6" + local memory_checkpoints="$7" + local server_pid="$8" + local server_birth_identity="$9" + local deadline now remaining_ms attempt_timeout_ms reconnect status=0 completed=0 + local quiescence_start_ns quiescence_end_ns + local start_mono_ns end_mono_ns command_sha256 client_sha256 connection_sha256 + + command_sha256="$({ + printf '%s\0' \ + oliphaunt.wasix-postmaster.lifecycle-reconnect.v1 \ + PGCONNECT_TIMEOUT=5 psql -X -qAt -v ON_ERROR_STOP=1 -c 'select 1' + } | fresh_sha256_stream)" || return + client_sha256="$(fresh_wasmer_bin_hash "$NATIVE_INSTALL_DIR/bin/psql")" || return + connection_sha256="$(printf '%s\0' "$conn" | fresh_sha256_stream)" || return + for digest in "$command_sha256" "$client_sha256" "$connection_sha256"; do + fresh_is_sha256 "$digest" || return 1 + done + + : >"$output" + start_mono_ns="$(now_ns)" + deadline=$(( $(now_ms) + timeout_seconds * 1000 )) + for ((reconnect = 1; reconnect <= wasix_lifecycle_reconnects; reconnect++)); do + now="$(now_ms)" + if [ "$now" -ge "$deadline" ]; then + printf 'reconnect churn timed out after %s of %s attempts\n' \ + "$((reconnect - 1))" "$wasix_lifecycle_reconnects" >>"$output" + status=124 + break + fi + remaining_ms=$((deadline - now)) + attempt_timeout_ms=10000 + if [ "$remaining_ms" -lt "$attempt_timeout_ms" ]; then + attempt_timeout_ms="$remaining_ms" + fi + set +e + fresh_run_process_group_timeout_ms "$attempt_timeout_ms" -- \ + env PGCONNECT_TIMEOUT=5 "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" -X -qAt -v ON_ERROR_STOP=1 -c 'select 1' >>"$output" 2>&1 + status=$? + set -e + if [ "$status" -ne 0 ]; then + printf 'reconnect %s of %s failed with exit %s\n' \ + "$reconnect" "$wasix_lifecycle_reconnects" "$status" >>"$output" + break + fi + completed="$reconnect" + if [ "$wasix_lifecycle_memory_checkpoint_every" -gt 0 ] && + [ $((reconnect % wasix_lifecycle_memory_checkpoint_every)) -eq 0 ] && + [ "$reconnect" -lt "$wasix_lifecycle_reconnects" ]; then + set_resource_phase "$resource_phase_file" \ + "diagnostic:lifecycle-memory-quiescence-$reconnect" + quiescence_start_ns="$(now_ns)" + sleep "$wasix_lifecycle_memory_quiescence_seconds" + quiescence_end_ns="$(now_ns)" + if ! capture_lifecycle_memory_checkpoint "$memory_checkpoints" "$nonce" \ + "$((reconnect / wasix_lifecycle_memory_checkpoint_every))" \ + wave-quiescent "$reconnect" "$server_pid" "$server_birth_identity" \ + "$wasix_lifecycle_memory_quiescence_seconds" \ + "$quiescence_start_ns" "$quiescence_end_ns"; then + status=1 + break + fi + set_resource_phase "$resource_phase_file" \ + "diagnostic:lifecycle-reconnect-churn" + fi + done + end_mono_ns="$(now_ns)" + if [ "$status" -eq 0 ] && [ "$completed" -eq "$wasix_lifecycle_reconnects" ]; then + printf 'completed_reconnects=%s\n' "$completed" >>"$output" + printf 'wasix-runtime-reconnect-churn-v1\tnonce=%s\trequested=%s\tcompleted=%s\tcommand_sha256=%s\tclient_sha256=%s\tconnection_sha256=%s\tstart_mono_ns=%s\tend_mono_ns=%s\tstatus=passed\tobserver_pid=%s\n' \ + "$nonce" "$wasix_lifecycle_reconnects" "$completed" "$command_sha256" \ + "$client_sha256" "$connection_sha256" "$start_mono_ns" "$end_mono_ns" \ + "$observer_pid" >>"$evidence_log" + return 0 + fi + if [ "$status" -eq 0 ]; then + return 1 + fi + return "$status" +} + +run_wasix_lifecycle_plateau() { + local conn="$1" + local pgdata="$2" + local wait_dump_log="$3" + local resource_phase_file="$4" + local target_report_dir="$5" + local fence_request_file="$6" + local fence_ack_file="$7" + local server_pid="$8" + local postmaster_pid nonce churn_status=1 validator_status evidence_status=0 + local memory_checkpoint_status=0 memory_validator_status=0 + local server_birth_identity final_memory_sequence + local readiness_quiescence_start_ns readiness_quiescence_end_ns + local final_quiescence_start_ns final_quiescence_end_ns + local stabilization_status + local fence_status freeze_status complete_phase_sequence complete_phase_mono_ns + local -a freeze_args + local reconnect_log="$target_report_dir/lifecycle-reconnect-churn.log" + local frozen_log="$target_report_dir/wasix-runtime-evidence.log" + local freeze_receipt="$target_report_dir/wasix-runtime-evidence.freeze.tsv" + + postmaster_pid="$(sed -n '1p' "$pgdata/postmaster.pid" 2>/dev/null || true)" + case "$postmaster_pid" in + ''|*[!0-9]*|0) + printf 'could not read a positive guest postmaster PID from %s\n' \ + "$pgdata/postmaster.pid" >&2 + return 1 + ;; + esac + nonce="$(new_lifecycle_nonce)" || return + server_birth_identity="$(fresh_process_birth_identity "$server_pid" 2>/dev/null || true)" + if [ "$wasix_lifecycle_memory_checkpoint_every" -gt 0 ]; then + [[ "$server_birth_identity" =~ ^linux-starttime:[1-9][0-9]*$ ]] || { + echo 'could not capture the Linux Wasmer server birth identity for lifecycle memory checkpoints' >&2 + return 1 + } + initialize_lifecycle_memory_checkpoints "$lifecycle_memory_checkpoints_tsv" + fi + lifecycle_phase_sequence=0 + + set_resource_phase "$resource_phase_file" "diagnostic:lifecycle-cold-readiness" + append_lifecycle_phase_marker "$wait_dump_log" "$nonce" cold-readiness "$postmaster_pid" + sleep "$wasix_lifecycle_window_seconds" + set_resource_phase "$resource_phase_file" "diagnostic:lifecycle-maintenance-stabilization" + append_lifecycle_phase_marker "$wait_dump_log" "$nonce" maintenance-stabilization \ + "$postmaster_pid" + set +e + run_walwriter_stabilization "$conn" "$wait_dump_log" "$nonce" \ + "$postmaster_pid" "$target_report_dir" + stabilization_status=$? + set -e + if [ "$stabilization_status" -ne 0 ]; then + printf 'WAL-writer lifecycle stabilization failed; see %s\n' \ + "$target_report_dir/lifecycle-walwriter-stabilization.log" >&2 + set_resource_phase "$resource_phase_file" idle + return "$stabilization_status" + fi + set_resource_phase "$resource_phase_file" "diagnostic:lifecycle-readiness" + append_lifecycle_phase_marker "$wait_dump_log" "$nonce" readiness "$postmaster_pid" + readiness_quiescence_start_ns="$(now_ns)" + sleep "$wasix_lifecycle_window_seconds" + set +e + request_lifecycle_fence "$fence_request_file" "$fence_ack_file" "$nonce" 1 \ + readiness "$postmaster_pid" "$server_pid" + fence_status=$? + set -e + readiness_quiescence_end_ns="$(now_ns)" + if [ "$fence_status" -ne 0 ]; then + churn_status=1 + evidence_status=1 + else + if [ "$wasix_lifecycle_memory_checkpoint_every" -gt 0 ]; then + set +e + capture_lifecycle_memory_checkpoint "$lifecycle_memory_checkpoints_tsv" \ + "$nonce" 0 baseline-fenced 0 "$server_pid" "$server_birth_identity" \ + "$wasix_lifecycle_window_seconds" "$readiness_quiescence_start_ns" \ + "$readiness_quiescence_end_ns" + memory_checkpoint_status=$? + set -e + fi + if [ "$memory_checkpoint_status" -ne 0 ]; then + churn_status=1 + evidence_status=1 + else + set_resource_phase "$resource_phase_file" "diagnostic:lifecycle-reconnect-churn" + append_lifecycle_phase_marker "$wait_dump_log" "$nonce" reconnect-churn "$postmaster_pid" + set +e + run_lifecycle_reconnect_churn "$conn" "$reconnect_log" "$wait_dump_log" \ + "$nonce" "$postmaster_pid" "$resource_phase_file" \ + "$lifecycle_memory_checkpoints_tsv" "$server_pid" \ + "$server_birth_identity" + churn_status=$? + set -e + if [ "$churn_status" -ne 0 ]; then + evidence_status=1 + else + set_resource_phase "$resource_phase_file" "diagnostic:lifecycle-post-quiescence" + append_lifecycle_phase_marker "$wait_dump_log" "$nonce" post-quiescence "$postmaster_pid" + final_quiescence_start_ns="$(now_ns)" + sleep "$wasix_lifecycle_window_seconds" + set +e + request_lifecycle_fence "$fence_request_file" "$fence_ack_file" "$nonce" 2 \ + post-quiescence "$postmaster_pid" "$server_pid" + fence_status=$? + set -e + final_quiescence_end_ns="$(now_ns)" + if [ "$fence_status" -ne 0 ]; then + evidence_status=1 + elif [ "$wasix_lifecycle_memory_checkpoint_every" -gt 0 ]; then + final_memory_sequence=$(( + (wasix_lifecycle_reconnects - 1) / + wasix_lifecycle_memory_checkpoint_every + 1 + )) + set +e + capture_lifecycle_memory_checkpoint "$lifecycle_memory_checkpoints_tsv" \ + "$nonce" "$final_memory_sequence" final-fenced \ + "$wasix_lifecycle_reconnects" "$server_pid" \ + "$server_birth_identity" "$wasix_lifecycle_window_seconds" \ + "$final_quiescence_start_ns" "$final_quiescence_end_ns" + memory_checkpoint_status=$? + set -e + if [ "$memory_checkpoint_status" -ne 0 ]; then + evidence_status=1 + fi + fi + fi + fi + fi + + # Freeze an immutable prefix through the runtime-owned final fence. The + # canonical complete marker is created inside that frozen artifact; the raw + # writer log remains available separately and may continue to grow. + complete_phase_sequence=$((lifecycle_phase_sequence + 1)) + complete_phase_mono_ns="$(now_ns)" + freeze_args=( + --raw-log "$wait_dump_log" + --commit-ack "$fence_ack_file" + --output "$frozen_log" + --receipt "$freeze_receipt" + --nonce "$nonce" + --observer-pid "$postmaster_pid" + --complete-phase-sequence "$complete_phase_sequence" + --complete-phase-mono-ns "$complete_phase_mono_ns" + ) + set +e + python3 "$FRESH_ROOT/bin/freeze-wasix-lifecycle-evidence.py" "${freeze_args[@]}" + freeze_status=$? + set -e + if [ "$freeze_status" -ne 0 ]; then + evidence_status=1 + fi + append_lifecycle_phase_marker "$wait_dump_log" "$nonce" complete "$postmaster_pid" \ + "$complete_phase_mono_ns" + + set +e + python3 "$FRESH_ROOT/bin/validate-wasix-lifecycle-plateau.py" \ + --log "$frozen_log" \ + --freeze-receipt "$freeze_receipt" \ + --baseline-policy "$lifecycle_baseline_policy_tsv" \ + --baseline-binding "$lifecycle_baseline_binding_tsv" \ + --output "$lifecycle_plateau_tsv" \ + --target wasix \ + --nonce "$nonce" \ + --observer-pid "$postmaster_pid" \ + --min-samples 3 \ + --min-span-ms 1000 \ + --expected-interval-ms "$wasix_wait_dump_interval_ms" + validator_status=$? + set -e + if [ "$wasix_lifecycle_memory_checkpoint_every" -gt 0 ]; then + set +e + python3 "$FRESH_ROOT/bin/validate-wasix-lifecycle-memory-plateau.py" \ + --input "$lifecycle_memory_checkpoints_tsv" \ + --runtime-plateau "$lifecycle_plateau_tsv" \ + --output "$lifecycle_memory_plateau_tsv" \ + --target wasix \ + --nonce "$nonce" \ + --server-pid "$server_pid" \ + --requested-reconnects "$wasix_lifecycle_reconnects" \ + --checkpoint-every "$wasix_lifecycle_memory_checkpoint_every" \ + --min-quiescence-seconds \ + "$wasix_lifecycle_memory_quiescence_seconds" \ + --max-pss-growth-kib "$max_lifecycle_pss_growth_kib" \ + --max-pss-anon-growth-kib "$max_lifecycle_pss_anon_growth_kib" \ + --max-heap-growth-kib "$max_lifecycle_heap_growth_kib" \ + --max-late-pss-slope-kib-per-1000-reconnects \ + "$max_late_lifecycle_pss_slope_kib_per_1000" \ + --max-late-pss-anon-slope-kib-per-1000-reconnects \ + "$max_late_lifecycle_pss_anon_slope_kib_per_1000" \ + --max-late-heap-slope-kib-per-1000-reconnects \ + "$max_late_lifecycle_heap_slope_kib_per_1000" + memory_validator_status=$? + set -e + fi + set_resource_phase "$resource_phase_file" idle + [ "$churn_status" -eq 0 ] && [ "$evidence_status" -eq 0 ] && + [ "$validator_status" -eq 0 ] && [ "$memory_validator_status" -eq 0 ] +} + +started_server_pid="" +started_server_pgid="" +started_server_birth_identity="" +server_command_prefix=() +server_cgroup_unit="" + +configure_server_command_prefix() { + local target="$1" + local port="$2" + local unit + + server_command_prefix=() + server_cgroup_unit="" + [ -n "$cgroup_memory_max$cgroup_memory_high$cgroup_swap_max" ] || return 0 + unit="oliphaunt-wasix-postmaster-$target-$$-$port" + server_cgroup_unit="$unit" + server_command_prefix=( + systemd-run + --user + --scope + --quiet + --collect + "--unit=$unit" + --property=MemoryAccounting=yes + ) + if [ "$cold_ownership" -eq 1 ]; then + server_command_prefix+=(--property=IOAccounting=yes) + fi + if [ -n "$cgroup_memory_max" ]; then + server_command_prefix+=("--property=MemoryMax=$cgroup_memory_max") + fi + if [ -n "$cgroup_memory_high" ]; then + server_command_prefix+=("--property=MemoryHigh=$cgroup_memory_high") + fi + if [ -n "$cgroup_swap_max" ]; then + server_command_prefix+=("--property=MemorySwapMax=$cgroup_swap_max") + fi +} + +launch_measured_server() { + local limits_record="$1" + shift + local pre_soft pre_hard actual_soft actual_hard status="not-requested" + + pre_soft="$(ulimit -S -n)" + pre_hard="$(ulimit -H -n)" + if [ "$libpq_latency_samples" -gt 0 ]; then + status="applied" + case "$pre_hard" in + unlimited) ;; + ""|*[!0-9]*) status="invalid-hard-limit" ;; + *) [ "$pre_hard" -ge "$libpq_latency_soft_nofile" ] || status="hard-limit-too-low" ;; + esac + if [ "$status" = "applied" ] && + ! ulimit -S -n "$libpq_latency_soft_nofile"; then + status="set-failed" + fi + fi + actual_soft="$(ulimit -S -n)" + actual_hard="$(ulimit -H -n)" + { + printf 'pre_soft_nofile=%s\n' "$pre_soft" + printf 'pre_hard_nofile=%s\n' "$pre_hard" + printf 'actual_soft_nofile=%s\n' "$actual_soft" + printf 'actual_hard_nofile=%s\n' "$actual_hard" + printf 'status=%s\n' "$status" + } >"$limits_record" + if [ "$libpq_latency_samples" -gt 0 ] && + { [ "$status" != "applied" ] || + [ "$actual_soft" != "$libpq_latency_soft_nofile" ] || + [ "$actual_hard" != "$pre_hard" ]; }; then + return 125 + fi + exec "$@" +} + +cold_first_query_monotonic_ns="" +cold_readiness_attempts=0 +wait_for_ready() { + local conn="$1" + local server_pid="$2" + local wait_log="$3" + local attempt_log="$wait_log.attempt" + local deadline now remaining_ms attempt_timeout_ms readiness_status + + : >"$wait_log" + cold_first_query_monotonic_ns="" + cold_readiness_attempts=0 + deadline=$(( $(now_ms) + timeout_seconds * 1000 )) + while :; do + now="$(now_ms)" + if [ "$now" -ge "$deadline" ]; then + printf 'readiness timed out after %s seconds\n' "$timeout_seconds" >>"$wait_log" + rm -f "$attempt_log" + return 124 + fi + remaining_ms=$((deadline - now)) + attempt_timeout_ms=1000 + if [ "$remaining_ms" -lt "$attempt_timeout_ms" ]; then + attempt_timeout_ms="$remaining_ms" + fi + set +e + cold_readiness_attempts=$((cold_readiness_attempts + 1)) + fresh_run_process_group_timeout_ms "$attempt_timeout_ms" -- \ + env PGCONNECT_TIMEOUT=1 "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" -X -q -c 'select 1' >"$attempt_log" 2>&1 + readiness_status=$? + set -e + if [ -s "$attempt_log" ]; then + cat "$attempt_log" >>"$wait_log" + fi + rm -f "$attempt_log" + if [ "$readiness_status" -eq 0 ]; then + cold_first_query_monotonic_ns="$(now_ns)" + return 0 + fi + if [ "$readiness_status" -eq 125 ]; then + echo "readiness process supervision failed" >>"$wait_log" + return 125 + fi + if ! pid_is_running "$server_pid"; then + echo "server exited before readiness" >>"$wait_log" + return 1 + fi + sleep 0.1 + done +} + +start_native_server() { + local pgdata="$1" + local port="$2" + local initdb_log="$3" + local server_log="$4" + local limits_record="$5" + local postgres_args initdb_status + + if run_logged_timeout "$timeout_seconds" "$initdb_log" \ + "$NATIVE_INSTALL_DIR/bin/initdb" -D "$pgdata" -A trust --no-locale \ + --encoding=UTF8 --no-instructions --wal-segsize=16; then + initdb_status=0 + else + initdb_status=$? + return "$initdb_status" + fi + postgres_args=( + -D "$pgdata" + -h 127.0.0.1 + -p "$port" + -c unix_socket_directories= + -c "max_connections=$((connections + 32))" + ) + if [ "${#effective_postgres_gucs[@]}" -gt 0 ]; then + for guc in "${effective_postgres_gucs[@]}"; do + postgres_args+=(-c "$guc") + done + fi + fresh_spawn_process_group -- launch_measured_server "$limits_record" \ + "${server_command_prefix[@]}" \ + "$NATIVE_INSTALL_DIR/bin/postgres" "${postgres_args[@]}" \ + >"$server_log" 2>&1 || return + started_server_pid="$FRESH_PROCESS_GROUP_PID" + started_server_pgid="$FRESH_PROCESS_GROUP_PGID" + started_server_birth_identity="$FRESH_PROCESS_GROUP_IDENTITY" +} + +wasmer_bin="" +wasmer_bin_hash="" +wasmer_cache_dir="" +wasmer_compiler="" +wasmer_llvm_opt_level="" +wasmer_stack_size="" +wasmer_compiler_threads="" +wasmer_version="" +wasix_initdb_module="" +wasix_postgres_module="" +wasix_runtime_lib_dir="" +wasmer_args=() +wasmer_env=() +wasmer_env_command=() +cold_spawn_monotonic_ns="" + +configure_wasmer_env_command() { + wasmer_env_command=(env "$@") + for wait_dump_name in "${wait_dump_environment_names[@]}"; do + wasmer_env_command+=(-u "$wait_dump_name") + done + for sealed_loader_name in "${sealed_loader_environment_names[@]}"; do + wasmer_env_command+=(-u "$sealed_loader_name") + done +} + +prepare_wasix_runtime() { + wasmer_stack_size="${WASMER_STACK_SIZE:-33554432}" + if [ "$wasix_runtime_mode" = "sealed-headless" ]; then + wasmer_bin="$sealed_carrier_root/bin/wasmer-headless" + if [ "$require_zero_write_aot" -eq 1 ]; then + wasmer_bin_hash="$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" + else + wasmer_bin_hash="$(fresh_wasmer_bin_hash "$wasmer_bin")" + fi + [ "$wasmer_bin_hash" = "$sealed_executor_hash" ] || { + echo 'selected sealed executor identity differs from carrier role receipt' >&2 + return 2 + } + wasmer_cache_dir="" + wasmer_compiler="" + wasmer_llvm_opt_level="" + wasmer_compiler_threads="" + wasix_initdb_module="$sealed_initdb_module" + wasix_postgres_module="$sealed_postgres_module" + wasix_runtime_lib_dir="$sealed_lib_dir" + configure_wasmer_env_command -u WASMER_DIR -u WASMER_CACHE_DIR + if [ "$require_zero_write_aot" -ne 1 ]; then + case "$sealed_executor_role" in + postmaster-product) + fresh_require_patched_postmaster_executor \ + "$wasmer_bin" \ + "$sealed_carrier_root/$sealed_executor_receipt_relative" \ + "$sealed_receipt" + ;; + full-headless) + WASMER_BUILD_RECEIPT="$sealed_receipt" \ + fresh_require_patched_wasmer_headless "$wasmer_bin" + ;; + *) + echo 'unsupported sealed executor role after carrier validation' >&2 + return 2 + ;; + esac + fi + wasmer_version="$(env -u WASMER_DIR -u WASMER_CACHE_DIR "$wasmer_bin" --version 2>/dev/null || true)" + cp -p "$sealed_manifest" "$report_dir/wasix-sealed-manifest.json" + cp -p "$sealed_receipt" "$report_dir/wasix-sealed-wasmer-build.receipt" + cp -p "$sealed_payload_inventory" "$report_dir/wasix-sealed-payload.files" + else + configure_wasmer_env_command + wasmer_bin="$(fresh_wasmer_bin)" + wasmer_bin_hash="$(fresh_wasmer_bin_hash "$wasmer_bin")" + wasmer_cache_dir="$(fresh_wasmer_cache_dir "$wasmer_bin")" + wasmer_compiler="$(fresh_wasmer_compiler)" + wasmer_llvm_opt_level="${WASMER_LLVM_OPT_LEVEL:-aggressive}" + wasmer_compiler_threads="${WASMER_COMPILER_THREADS:-$(fresh_jobs)}" + wasix_initdb_module="$WASIX_INSTALL_DIR/bin/initdb" + wasix_postgres_module="$WASIX_INSTALL_DIR/bin/postgres" + wasix_runtime_lib_dir="$WASIX_INSTALL_DIR/lib" + fresh_require_wasmer_compiler_cli "$wasmer_bin" "$wasmer_compiler" run + if [ "$skip_precompile" != "1" ]; then + "$FRESH_ROOT/bin/precompile-wasix-core.sh" >/dev/null + fi + fi +} + +build_wasmer_args() { + local dev_shm="$1" + if [ "$wasix_runtime_mode" = "sealed-headless" ]; then + wasmer_env=() + else + wasmer_env=( + "WASMER_DIR=$FRESH_WORK_ROOT/tools/wasmer-home" + "WASMER_CACHE_DIR=$wasmer_cache_dir" + ) + fi + wasmer_args=(run --quiet) + if [ "$wasix_runtime_mode" = "sealed-headless" ]; then + wasmer_args+=(--disable-cache --sealed-module-manifest "$sealed_manifest") + else + while IFS= read -r arg; do + wasmer_args+=("$arg") + done < <(fresh_wasmer_compiler_args_for "$wasmer_bin" run "$wasmer_compiler" "$wasmer_llvm_opt_level" "$wasmer_compiler_threads") + fi + wasmer_args+=( + --stack-size "$wasmer_stack_size" + --enable-exceptions + --enable-threads + --net + --volume "$REPO_ROOT:$REPO_ROOT" + --volume "$wasix_runtime_lib_dir:/lib" + --volume "$dev_shm:/dev/shm" + ) + case "$FRESH_WORK_ROOT/" in + "$REPO_ROOT/"*) ;; + *) wasmer_args+=(--volume "$FRESH_WORK_ROOT:$FRESH_WORK_ROOT") ;; + esac + if [ "$wasix_runtime_mode" = "sealed-headless" ]; then + case "$sealed_carrier_root/" in + "$REPO_ROOT/"*|"$FRESH_WORK_ROOT/"*) ;; + *) wasmer_args+=(--volume "$sealed_carrier_root:$sealed_carrier_root") ;; + esac + fi + if [ "${#wasmer_extra_args[@]}" -gt 0 ]; then + wasmer_args+=("${wasmer_extra_args[@]}") + fi +} + +start_wasix_server() { + local pgdata="$1" + local dev_shm="$2" + local port="$3" + local initdb_log="$4" + local server_log="$5" + local perf_initdb_log="$6" + local perf_server_log="$7" + local limits_record="$8" + local fence_request_file="$9" + local fence_ack_file="${10}" + local cold_residency_receipt="${11}" + local sealed_loader_audit="${12}" + local cache_offer_initdb_telemetry="${13}" + local cache_offer_postgres_telemetry="${14}" + local postgres_args initdb_status + local initdb_env=() + local server_env=() + + build_wasmer_args "$dev_shm" + initdb_env=("${wasmer_env[@]}") + server_env=("${wasmer_env[@]}") + if [ "$wasix_runtime_mode" = "sealed-headless" ]; then + initdb_env+=( + "OLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE=$cache_offer_initdb_telemetry" + ) + server_env+=( + "OLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE=$cache_offer_postgres_telemetry" + ) + fi + if [ "$require_zero_write_aot" -eq 1 ]; then + initdb_env+=( + "OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1" + "OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE=$sealed_loader_audit" + ) + server_env+=( + "OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1" + "OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE=$sealed_loader_audit" + ) + fi + if [ "$wasix_perf_stats" = "1" ]; then + initdb_env+=("WASIX_PERF_STATS=1" "WASIX_PERF_STATS_FILE=$perf_initdb_log") + server_env+=("WASIX_PERF_STATS=1" "WASIX_PERF_STATS_FILE=$perf_server_log") + fi + if [ "$wasix_wait_dump_interval_ms" -gt 0 ]; then + initdb_env+=( + "WASIX_WAIT_DUMP_INTERVAL_MS=$wasix_wait_dump_interval_ms" + "WASIX_WAIT_DUMP_FILE=$perf_initdb_log" + "WASIX_WAIT_DUMP_MAX_PER_WAIT=$wasix_wait_dump_max_per_wait" + "WASIX_WAIT_DUMP_VERBOSE=$wasix_wait_dump_verbose" + ) + server_env+=( + "WASIX_WAIT_DUMP_INTERVAL_MS=$wasix_wait_dump_interval_ms" + "WASIX_WAIT_DUMP_FILE=$perf_server_log" + "WASIX_WAIT_DUMP_MAX_PER_WAIT=$wasix_wait_dump_max_per_wait" + "WASIX_WAIT_DUMP_VERBOSE=$wasix_wait_dump_verbose" + ) + if [ "$wasix_lifecycle_plateau" -eq 1 ]; then + server_env+=( + "WASIX_WAIT_DUMP_FENCE_REQUEST_FILE=$fence_request_file" + "WASIX_WAIT_DUMP_FENCE_ACK_FILE=$fence_ack_file" + ) + fi + fi + + if run_logged_timeout "$timeout_seconds" "$initdb_log" \ + "${wasmer_env_command[@]}" "${initdb_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" "$wasix_initdb_module" -- \ + -D "$pgdata" -A trust --no-locale --encoding=UTF8 --no-instructions \ + --wal-segsize=16; then + initdb_status=0 + else + initdb_status=$? + return "$initdb_status" + fi + + postgres_args=( + -D "$pgdata" + -h 127.0.0.1 + -p "$port" + -c unix_socket_directories= + -c "max_connections=$((connections + 32))" + ) + if [ "${#effective_postgres_gucs[@]}" -gt 0 ]; then + for guc in "${effective_postgres_gucs[@]}"; do + postgres_args+=(-c "$guc") + done + fi + if [ "$cold_ownership" -eq 1 ]; then + python3 "$FRESH_ROOT/bin/prove-linux-cold-residency.py" \ + --root "carrier=$sealed_carrier_root" \ + --root "pgdata=$pgdata" \ + --read-only-root carrier \ + --binding "execution_identity_sha256=$execution_identity_sha256" \ + --binding "carrier_manifest_sha256=$sealed_manifest_hash" \ + --binding "carrier_receipt_sha256=$sealed_receipt_hash" \ + --binding "carrier_payload_inventory_sha256=$sealed_payload_inventory_hash" \ + --output "$cold_residency_receipt" || return + # FINAL COLD BOUNDARY: no carrier/PGDATA verifier, hash, stat, or content + # read is permitted between the completed mincore proof above and exec. + cold_spawn_monotonic_ns="$(now_ns)" + fi + fresh_spawn_process_group -- launch_measured_server "$limits_record" \ + "${server_command_prefix[@]}" "${wasmer_env_command[@]}" "${server_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" "$wasix_postgres_module" -- \ + "${postgres_args[@]}" >"$server_log" 2>&1 || return + started_server_pid="$FRESH_PROCESS_GROUP_PID" + started_server_pgid="$FRESH_PROCESS_GROUP_PGID" + started_server_birth_identity="$FRESH_PROCESS_GROUP_IDENTITY" +} + +write_connected_client_script() { + local source_sql="$1" + local connected_sql="$2" + local ready_file="$3" + local fanout_gate="$4" + local end_file="$5" + local drain_gate="$6" + + python3 - "$source_sql" "$connected_sql" "$ready_file" "$fanout_gate" \ + "$end_file" "$drain_gate" <<'PY' +import os +import shlex +import stat +import sys +from pathlib import Path + +source, output, ready, start, completed, drain = map(Path, sys.argv[1:]) +source_stat = os.lstat(source) +if not stat.S_ISREG(source_stat.st_mode) or stat.S_ISLNK(source_stat.st_mode): + raise SystemExit(f"client SQL source must be a regular non-symlink file: {source}") +for label, path in ( + ("connected SQL", output), + ("ready marker", ready), + ("start gate", start), + ("completion marker", completed), + ("drain gate", drain), +): + if "\n" in str(path) or "\r" in str(path) or "\0" in str(path): + raise SystemExit(f"{label} path contains a control character") +if os.path.lexists(output): + raise SystemExit(f"connected client SQL already exists: {output}") + +ready_command = f": > {shlex.quote(str(ready))}" +start_command = ( + f"while [ ! -f {shlex.quote(str(start))} ]; do sleep 0.001; done" +) +clock_script = ( + 'my $tmp = "$ARGV[0].tmp.$$"; ' + 'sysopen(my $fh, $tmp, O_WRONLY | O_CREAT | O_EXCL, 0600) ' + 'or die "open completion marker: $!"; ' + 'printf {$fh} "%.0f\\n", clock_gettime(CLOCK_MONOTONIC) * 1000; ' + 'close($fh) or die "close completion marker: $!"; ' + 'rename($tmp, $ARGV[0]) or die "publish completion marker: $!"' +) +complete_command = shlex.join( + [ + "perl", + "-MTime::HiRes=clock_gettime,CLOCK_MONOTONIC", + "-MFcntl=:DEFAULT", + "-e", + clock_script, + str(completed), + ] +) +drain_command = ( + f"while [ ! -f {shlex.quote(str(drain))} ]; do sleep 0.001; done" +) +payload = source.read_bytes() +if b"\r" in payload: + raise SystemExit(f"client SQL source contains a carriage return: {source}") +with output.open("xb") as sink: + sink.write(f"\\! {ready_command}\n".encode()) + sink.write(f"\\! {start_command}\n".encode()) + sink.write(payload) + if payload and not payload.endswith(b"\n"): + sink.write(b"\n") + sink.write(f"\\! {complete_command}\n".encode()) + sink.write(f"\\! {drain_command}\n".encode()) +os.chmod(output, 0o600) +PY +} + +run_client_process() { + local conn="$1" + local client="$2" + local client_connections="$3" + local client_iterations="$4" + local client_row_count="$5" + local client_setup_rows="$6" + local client_sql="$7" + local client_log="$8" + local client_status_file="$9" + local client_end_file="${10}" + local client_status + + set +e + trap 'client_status=124; printf "%s\n" "$client_status" >"$client_status_file"; if [ ! -s "$client_end_file" ]; then now_ms >"$client_end_file"; fi; exit "$client_status"' TERM INT + PGCONNECT_TIMEOUT=10 "$NATIVE_INSTALL_DIR/bin/psql" "$conn" \ + -X -q \ + -v ON_ERROR_STOP=1 \ + -v "client_id=$client" \ + -v "connections=$client_connections" \ + -v "iterations=$client_iterations" \ + -v "row_count=$client_row_count" \ + -v "setup_rows=$client_setup_rows" \ + -f "$client_sql" >"$client_log" 2>&1 + client_status=$? + printf '%s\n' "$client_status" >"$client_status_file" + if [ ! -s "$client_end_file" ]; then + now_ms >"$client_end_file" + fi + exit "$client_status" +} + +run_workload() { + local target="$1" + local workload="$2" + local conn="$3" + local target_report_dir="$4" + local server_log="$5" + local resource_phase_file="$6" + local server_pid="$7" + local workload_report_dir="$target_report_dir/$workload" + local setup_sql="$suite_root/sql/$workload.setup.sql" + local client_sql="$suite_root/sql/$workload.client.sql" + local verify_sql="$suite_root/sql/$workload.verify.sql" + local setup_log="$workload_report_dir/setup.log" + local verify_log="$workload_report_dir/verify.log" + local operation_count expected_verify_count verified_count throughput fanout_start fanout_end fanout_wall + local timed_out=0 ok_clients=0 failed_clients=0 status=0 pids=() pgids=() client_logs=() client_start=() + local client_status_files=() client_end_files=() client_ready_files=() + local client client_log client_status_file client_end_file client_ready_file deadline running pid pgid index + local barrier_ready=1 + local client_status client_end client_wall psql_time psql_count wait_status + local setup_status setup_timed_out verify_status epoll_intr_count + local sample_log sample_stderr sample_status sample_pid perfmap perfmap_copy symbol_prefix + local pg_wait_samples pg_wait_summary pg_wait_pid relation_footprint fanout_gate drain_gate + local connected_client_sql early_client_exit + + mkdir -p "$workload_report_dir" + if ! assert_no_client_process_residue "$target/$workload"; then + printf 'refusing to start %s/%s after client process residue\n' \ + "$target" "$workload" >&2 + printf '%s\t%s\t125\t%s\t%s\t%s\t\t%s\t0\t\t0\t%s\t0\t0\t%s\t%s\n' \ + "$target" "$workload" "$connections" "$iterations" \ + "$(operation_count_for "$workload")" \ + "$(expected_verify_count_for "$workload")" "$connections" \ + "$server_log" "$workload_report_dir" >>"$summary_tsv" + return 125 + fi + write_workload_sql "$workload" "$setup_sql" "$client_sql" "$verify_sql" + operation_count="$(operation_count_for "$workload")" + expected_verify_count="$(expected_verify_count_for "$workload")" + throughput="" + + set_resource_phase "$resource_phase_file" "setup:$workload" + set +e + run_logged_timeout "$timeout_seconds" "$setup_log" \ + "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -X -q -v ON_ERROR_STOP=1 \ + -v "row_count=$row_count" -v "setup_rows=$setup_rows" -f "$setup_sql" + setup_status=$? + set -e + if [ "$setup_status" -ne 0 ]; then + setup_timed_out=0 + [ "$setup_status" -eq 124 ] && setup_timed_out=1 + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t0\t\t0\t%s\t%s\t0\t%s\t%s\n' \ + "$target" "$workload" "$setup_status" "$connections" "$iterations" "$operation_count" \ + "" "$expected_verify_count" "$connections" "$setup_timed_out" "$server_log" \ + "$workload_report_dir" >>"$summary_tsv" + set_resource_phase "$resource_phase_file" "idle" + return "$setup_status" + fi + + set_resource_phase "$resource_phase_file" "checkpoint:$workload" + if ! prepare_fanout_checkpoint_state "$conn" "$workload_report_dir"; then + printf '%s\t%s\t%s\tpreparation-failed\t\t\t\t\t\t\t\t\t\t\t\t\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$workload" "$checkpoint_policy" "$checkpoint_wal_budget_bytes" \ + "$workload_report_dir/checkpoint-before.tsv" "" \ + "$workload_report_dir/pg-stat-io-before.csv" "" \ + "$workload_report_dir/pg-stat-io-delta.tsv" "not-run" >>"$checkpoint_tsv" + printf '%s\t%s\t1\t%s\t%s\t%s\t\t%s\t0\t\t0\t%s\t0\t0\t%s\t%s\n' \ + "$target" "$workload" "$connections" "$iterations" "$operation_count" \ + "$expected_verify_count" "$connections" "$server_log" "$workload_report_dir" \ + >>"$summary_tsv" + set_resource_phase "$resource_phase_file" "idle" + return 1 + fi + + set_resource_phase "$resource_phase_file" "fanout-preparing:$workload" + snapshot_memory_map "$target" "$server_pid" "fanout-$workload-before" "$workload_report_dir" + fanout_gate="$workload_report_dir/fanout.start" + drain_gate="$workload_report_dir/fanout.drain" + rm -f "$fanout_gate" "$drain_gate" + sample_log="$workload_report_dir/sample.txt" + sample_stderr="$workload_report_dir/sample.stderr.log" + sample_status=0 + sample_pid="" + pg_wait_samples="$workload_report_dir/pg-wait-samples.tsv" + pg_wait_summary="$workload_report_dir/pg-wait-summary.tsv" + pg_wait_pid="" + for client in $(seq 1 "$connections"); do + client_log="$workload_report_dir/client-$client.log" + client_status_file="$workload_report_dir/client-$client.status" + client_end_file="$workload_report_dir/client-$client.end_ms" + client_ready_file="$workload_report_dir/client-$client.ready" + connected_client_sql="$workload_report_dir/client-$client.connected.sql" + rm -f "$client_status_file" "$client_end_file" "$client_ready_file" + write_connected_client_script "$client_sql" "$connected_client_sql" \ + "$client_ready_file" "$fanout_gate" "$client_end_file" "$drain_gate" + client_logs+=("$client_log") + client_status_files+=("$client_status_file") + client_end_files+=("$client_end_file") + client_ready_files+=("$client_ready_file") + client_start+=(0) + if ! fresh_spawn_process_group -- run_client_process \ + "$conn" "$client" "$connections" "$iterations" "$row_count" \ + "$setup_rows" "$connected_client_sql" "$client_log" \ + "$client_status_file" "$client_end_file"; then + terminate_active_client_process_groups || true + assert_no_client_process_residue "$target/$workload spawn failure" || true + printf 'could not isolate client %s for %s/%s\n' \ + "$client" "$target" "$workload" >&2 + return 125 + fi + pid="$FRESH_PROCESS_GROUP_PID" + pgid="$FRESH_PROCESS_GROUP_PGID" + pids+=("$pid") + pgids+=("$pgid") + register_background_pid "$pid" + register_client_process_group "$pid" "$pgid" + done + + deadline=$(( $(now_ms) + timeout_seconds * 1000 )) + while :; do + running=0 + early_client_exit=0 + for index in "${!client_ready_files[@]}"; do + client_ready_file="${client_ready_files[$index]}" + client_status_file="${client_status_files[$index]}" + if [ ! -f "$client_ready_file" ]; then + if [ -f "$client_status_file" ]; then + early_client_exit=1 + status=1 + barrier_ready=0 + break + fi + running=1 + fi + done + [ "$early_client_exit" -eq 0 ] || break + [ "$running" -eq 0 ] && break + if [ "$(now_ms)" -ge "$deadline" ]; then + timed_out=1 + status=1 + barrier_ready=0 + break + fi + sleep 0.001 + done + + if [ "$barrier_ready" -eq 1 ] && float_gt_zero "$pg_wait_sample_interval"; then + sample_pg_wait_events "$target" "$workload" "$conn" \ + "$pg_wait_samples" "$pg_wait_sample_interval" & + pg_wait_pid="$!" + register_background_pid "$pg_wait_pid" + fi + if [ "$barrier_ready" -eq 1 ] && [ "$target" = "wasix" ] && float_gt_zero "$sample_seconds"; then + ( + sleep "$sample_delay" + if command -v sample >/dev/null 2>&1 && kill -0 "$server_pid" 2>/dev/null; then + sample "$server_pid" "$sample_seconds" -file "$sample_log" >"$sample_stderr" 2>&1 + else + printf 'sample command unavailable or server exited before sampling\n' >"$sample_stderr" + exit 127 + fi + ) & + sample_pid="$!" + register_background_pid "$sample_pid" + fi + set_resource_phase "$resource_phase_file" "fanout:$workload" + fanout_start="$(now_ms)" + for index in "${!client_start[@]}"; do + client_start[$index]="$fanout_start" + done + if [ "$barrier_ready" -ne 1 ]; then + terminate_active_client_process_groups || status=1 + fi + : >"$fanout_gate" + deadline=$((fanout_start + timeout_seconds * 1000)) + while :; do + running=0 + early_client_exit=0 + for index in "${!client_end_files[@]}"; do + client_end_file="${client_end_files[$index]}" + client_status_file="${client_status_files[$index]}" + if [ -s "$client_end_file" ]; then + continue + fi + if [ -f "$client_status_file" ]; then + early_client_exit=1 + status=1 + break + else + running=1 + fi + done + [ "$early_client_exit" -eq 0 ] || break + [ "$running" -eq 0 ] && break + if [ "$(now_ms)" -ge "$deadline" ]; then + timed_out=1 + terminate_active_client_process_groups || status=1 + break + fi + sleep 0.05 + done + + if [ "$early_client_exit" -eq 1 ]; then + # Keep failed and still-running SQL inside the fanout phase. A peer must + # never continue untimed after one client has failed the measured batch. + terminate_active_client_process_groups || status=1 + fi + + fanout_end="$fanout_start" + for client_end_file in "${client_end_files[@]}"; do + if [ -s "$client_end_file" ]; then + client_end="$(tr -d '[:space:]' <"$client_end_file")" + case "$client_end" in ''|*[!0-9]*) continue ;; esac + if [ "$client_end" -gt "$fanout_end" ]; then + fanout_end="$client_end" + fi + fi + done + set_resource_phase "$resource_phase_file" "fanout-draining:$workload" + : >"$drain_gate" + if [ "$timed_out" -eq 1 ]; then + terminate_active_client_process_groups || status=1 + fi + + for index in "${!pids[@]}"; do + pid="${pids[$index]}" + pgid="${pgids[$index]}" + client=$((index + 1)) + client_log="${client_logs[$index]}" + client_status_file="${client_status_files[$index]}" + client_end_file="${client_end_files[$index]}" + if wait "$pid" 2>/dev/null; then + wait_status=0 + else + wait_status=$? + fi + unregister_background_pid "$pid" + if [ -f "$client_status_file" ]; then + client_status="$(tr -d '[:space:]' <"$client_status_file")" + else + client_status=124 + fi + case "$client_status" in ''|*[!0-9]*) client_status="$wait_status" ;; esac + if [ "$timed_out" -eq 1 ] && [ "$client_status" -eq 143 ]; then + client_status=124 + fi + if [ -s "$client_end_file" ]; then + client_end="$(tr -d '[:space:]' <"$client_end_file")" + else + client_end="$(now_ms)" + fi + case "$client_end" in ''|*[!0-9]*) client_end="$(now_ms)" ;; esac + if fresh_process_group_exists "$pgid" || fresh_supervision_pid_running "$pid"; then + printf 'client process residue after %s/%s client %s: pid=%s pgid=%s\n' \ + "$target" "$workload" "$client" "$pid" "$pgid" >&2 + if fresh_terminate_process_group "$pgid" "$pid"; then + unregister_client_process_group "$pid" + fi + client_status=125 + status=1 + else + unregister_client_process_group "$pid" + fi + if [ "$client_end" -gt "$fanout_end" ]; then + fanout_end="$client_end" + fi + client_wall=$((client_end - client_start[$index])) + psql_time="$(extract_psql_time_sum_ms "$client_log")" + psql_count="$(extract_psql_time_count "$client_log")" + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$workload" "$client" "$client_status" "$client_wall" \ + "$psql_time" "$psql_count" "$client_log" >>"$client_tsv" + if [ "$client_status" -eq 0 ]; then + ok_clients=$((ok_clients + 1)) + else + failed_clients=$((failed_clients + 1)) + status=1 + fi + done + + if ! assert_no_client_process_residue "$target/$workload post-fanout"; then + status=1 + fi + + fanout_wall=$((fanout_end - fanout_start)) + rm -f "$fanout_gate" "$drain_gate" + if [ -n "$pg_wait_pid" ]; then + stop_pid "$pg_wait_pid" + unregister_background_pid "$pg_wait_pid" + summarize_pg_wait_events "$pg_wait_samples" "$pg_wait_summary" + fi + if ! finish_fanout_checkpoint_state \ + "$target" "$workload" "$conn" "$workload_report_dir"; then + status=1 + if [ ! -s "$workload_report_dir/checkpoint-after.tsv" ]; then + printf '%s\t%s\t%s\tcapture-failed\t\t\t\t\t\t\t\t\t\t\t\t\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$workload" "$checkpoint_policy" "$checkpoint_wal_budget_bytes" \ + "$workload_report_dir/checkpoint-before.tsv" \ + "$workload_report_dir/checkpoint-after.tsv" \ + "$workload_report_dir/pg-stat-io-before.csv" \ + "$workload_report_dir/pg-stat-io-after.csv" \ + "$workload_report_dir/pg-stat-io-delta.tsv" "failed" >>"$checkpoint_tsv" + fi + fi + snapshot_memory_map "$target" "$server_pid" "fanout-$workload-after" "$workload_report_dir" + + if [ -n "$sample_pid" ]; then + set +e + wait "$sample_pid" + sample_status=$? + set -e + unregister_background_pid "$sample_pid" + perfmap="/tmp/perf-$server_pid.map" + perfmap_copy="$workload_report_dir/perf.map" + symbol_prefix="$workload_report_dir/symbolized-sample" + if [ -s "$sample_log" ] && [ -s "$perfmap" ]; then + cp "$perfmap" "$perfmap_copy" + "$FRESH_ROOT/bin/symbolize-wasmer-sample.sh" \ + "$sample_log" \ + "$perfmap_copy" \ + "$symbol_prefix" \ + >"$workload_report_dir/symbolize.log" 2>&1 || true + fi + printf 'sample_status=%s\nsample_log=%s\nsample_stderr=%s\n' \ + "$sample_status" "$sample_log" "$sample_stderr" \ + >"$workload_report_dir/sample-summary.txt" + fi + + set_resource_phase "$resource_phase_file" "verify:$workload" + set +e + run_logged_timeout "$timeout_seconds" "$verify_log" \ + "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -X -q -A -t -v ON_ERROR_STOP=1 -f "$verify_sql" + verify_status=$? + set -e + verified_count="" + if [ "$verify_status" -eq 0 ]; then + verified_count="$(tail -n 1 "$verify_log" | tr -d '[:space:]')" + else + status=1 + fi + if [ "$verified_count" != "$expected_verify_count" ]; then + status=1 + fi + relation_footprint="$workload_report_dir/relation-footprint.tsv" + capture_relation_footprint "$conn" "$workload" "$relation_footprint" + snapshot_memory_map "$target" "$server_pid" "verify-$workload-after" "$workload_report_dir" + + if float_gt_zero "$quiescence_seconds"; then + set_resource_phase "$resource_phase_file" "quiescence:$workload" + snapshot_memory_map "$target" "$server_pid" "quiescence-$workload-before" "$workload_report_dir" + sleep "$quiescence_seconds" + snapshot_memory_map "$target" "$server_pid" "quiescence-$workload-after" "$workload_report_dir" + fi + + epoll_intr_count=0 + if [ "$target" = "wasix" ] && [ -f "$server_log" ]; then + epoll_intr_count="$(grep -c 'failed to epoll during deep sleep - intr' "$server_log" || true)" + if [ "$epoll_intr_count" != "0" ]; then + status=1 + fi + fi + + if [ "$status" -eq 0 ]; then + throughput="$(calc_rate "$operation_count" "$fanout_wall")" + fi + + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$workload" "$status" "$connections" "$iterations" "$operation_count" \ + "$verified_count" "$expected_verify_count" "$fanout_wall" "$throughput" \ + "$ok_clients" "$failed_clients" "$timed_out" "$epoll_intr_count" "$server_log" \ + "$workload_report_dir" >>"$summary_tsv" + set_resource_phase "$resource_phase_file" "idle" + return "$status" +} + +validate_target_memory_evidence() { + local target="$1" + local target_report_dir="$2" + local resource_samples_tsv="$3" + local output="$target_report_dir/memory-evidence.tsv" + local receipt="$target_report_dir/memory-validation-receipt.tsv" + local require_cgroup=no + local status sample_count workload required_phase_list="" + local -a args + + if [ "$resource_detail" != full ]; then + sample_count=0 + [ -s "$resource_samples_tsv" ] && + sample_count="$(awk 'END { print (NR > 0 ? NR - 1 : 0) }' "$resource_samples_tsv")" + printf '%s\tnot-applicable\tresource-detail-%s\t%s\n' \ + "$target" "$resource_detail" "$sample_count" >>"$memory_evidence_tsv" + return 0 + fi + if [ "${#workloads[@]}" -eq 0 ]; then + printf '%s\tfailed\tno-required-workload-phase\t0\n' "$target" \ + >>"$memory_evidence_tsv" + return 1 + fi + [ -n "$cgroup_memory_max$cgroup_memory_high$cgroup_swap_max" ] && require_cgroup=yes + args=( + --samples "$resource_samples_tsv" + --target "$target" + --interval-seconds "$resource_sample_interval" + --require-cgroup "$require_cgroup" + --memory-max "$cgroup_memory_max" + --memory-high "$cgroup_memory_high" + --swap-max "$cgroup_swap_max" + --output "$output" + ) + for workload in "${workloads[@]}"; do + args+=(--require-phase "fanout:$workload") + [ -z "$required_phase_list" ] || required_phase_list+=, + required_phase_list+="fanout:$workload" + done + [ -z "$max_peak_pss_kib" ] || + args+=(--max-peak-pss-kib "$max_peak_pss_kib") + [ -z "$max_peak_pss_anon_kib" ] || + args+=(--max-peak-pss-anon-kib "$max_peak_pss_anon_kib") + [ -z "$max_peak_page_table_kib" ] || + args+=(--max-peak-page-table-kib "$max_peak_page_table_kib") + [ -z "$max_cgroup_high_events_delta" ] || + args+=(--max-cgroup-high-events-delta "$max_cgroup_high_events_delta") + [ -z "$max_psi_some_stall_fraction" ] || + args+=(--max-psi-some-stall-fraction "$max_psi_some_stall_fraction") + [ -z "$max_psi_full_stall_fraction" ] || + args+=(--max-psi-full-stall-fraction "$max_psi_full_stall_fraction") + set +e + "$FRESH_ROOT/bin/validate-memory-evidence.sh" "${args[@]}" + status=$? + set -e + if [ -s "$output" ]; then + sed -n '2p' "$output" >>"$memory_evidence_tsv" + else + printf '%s\tfailed\tvalidator-produced-no-output\t0\n' "$target" \ + >>"$memory_evidence_tsv" + status=1 + fi + if [ -s "$resource_samples_tsv" ] && [ -s "$output" ]; then + { + printf 'schema_version\ttarget\tstatus\trequired_phases\tresource_samples_sha256\tmemory_budget_sha256\texecution_identity_sha256\tvalidator_sha256\tmemory_evidence_sha256\n' + printf 'oliphaunt.wasix-postmaster.memory-validation.v1\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$([ "$status" -eq 0 ] && printf passed || printf failed)" \ + "$required_phase_list" \ + "$(fresh_wasmer_bin_hash "$resource_samples_tsv")" \ + "$memory_budget_identity" "$execution_identity_sha256" \ + "$(fresh_wasmer_bin_hash "$FRESH_ROOT/bin/validate-memory-evidence.sh")" \ + "$(fresh_wasmer_bin_hash "$output")" + } >"$receipt" + fi + return "$status" +} + +run_target() { + local target="$1" + local port="$2" + local target_run_dir="$suite_root/$target" + local target_report_dir="$report_dir/$target" + local pgdata="$target_run_dir/pgdata" + local dev_shm="$target_run_dir/dev-shm" + local shared_memory_provider_evidence="$target_report_dir/shared-memory-provider.json" + local shared_memory_provider_objects="$target_report_dir/shared-memory-objects.json" + local shared_memory_provider_release="$target_report_dir/shared-memory-release.json" + local shared_memory_provider_post_shutdown_objects="$target_report_dir/shared-memory-post-shutdown-objects.json" + local shared_memory_provider_cleanup="$target_report_dir/shared-memory-cleanup.json" + local shared_memory_provider_exit_objects="$target_report_dir/shared-memory-exit-objects.json" + local shared_memory_provider_exit_release="$target_report_dir/shared-memory-exit-release.json" + local shared_memory_provider_sha256=none + local initdb_log="$target_report_dir/initdb.log" + local server_log="$target_report_dir/server.log" + local wasix_perf_initdb_log="$target_report_dir/wasix-perf-initdb.log" + local wasix_perf_server_log="$target_report_dir/wasix-perf-server.log" + local wait_dump_fence_request="$target_report_dir/wasix-runtime-fence.request" + local wait_dump_fence_ack="$target_report_dir/wasix-runtime-fence.ack" + local wait_log="$target_report_dir/wait.log" + local shutdown_report="$target_report_dir/shutdown.txt" + local limits_record="$target_report_dir/server-limits.launch" + local cold_residency_receipt="$target_report_dir/cold-residency-receipt.json" + local cold_first_query_snapshot="$target_report_dir/cold-first-query-cgroup.json" + local cold_final_snapshot="$target_report_dir/cold-final-cgroup.json" + local cold_sample="$target_report_dir/cold-ownership-sample.tsv" + local sealed_loader_audit="$target_report_dir/sealed-loader-audit.jsonl" + local sealed_loader_validation="$target_report_dir/sealed-loader-audit-validation.tsv" + local cache_offer_initdb_telemetry="$target_report_dir/cache-offers-initdb.json" + local cache_offer_postgres_telemetry="$target_report_dir/cache-offers-postgres.json" + # The sealed executor derives this sibling with + # Path::with_extension("adaptive.json"). The sole environment value above + # remains an output destination and can never select or activate a policy. + local cache_offer_postgres_adaptive_telemetry="${cache_offer_postgres_telemetry%.json}.adaptive.json" + local cache_offer_initdb_validation="$target_report_dir/cache-offers-initdb-validation.tsv" + local cache_offer_postgres_validation="$target_report_dir/cache-offers-postgres-validation.tsv" + local cache_offer_postgres_adaptive_validation="$target_report_dir/cache-offers-postgres-adaptive-validation.tsv" + local adaptive_cache_sample_contract="$target_report_dir/adaptive-cache-sample-contract.tsv" + local resource_phase_file="$target_report_dir/resource-phase" + local resource_stop_file="$target_report_dir/resource-stop" + local resource_samples_tsv="$target_report_dir/resource-samples.tsv" + local resource_sampler_log="$target_report_dir/resource-sampler.log" + local db_user="wasix" + local conn + local server_pid="" + local server_pgid="" + local server_birth_identity="" + local server_cgroup_dir="" + local server_cgroup_identity="" + local server_cgroup_membership_path="" + local server_cgroup_memory_max_bytes=none + local server_cgroup_memory_high_bytes=none + local server_cgroup_swap_max_bytes=none + local final_server_cgroup_memory_max_bytes=none + local final_server_cgroup_memory_high_bytes=none + local final_server_cgroup_swap_max_bytes=none + local adaptive_sample_contract_status=passed + local adaptive_sample_window_start_ns="" + local adaptive_sample_window_end_ns="" + local resource_monitor_pid="" + local resource_monitor_status=0 + local cold_evidence_status=0 + local target_status=0 + local start_status workload workload_status stop_status=0 + local shutdown_report_sha256="" shared_memory_release_status=0 + local shared_memory_prepare_args=() shared_memory_prepare_identity + local shared_memory_prepare_extra + local provider_retention retention_status + + if [ "$target" = "native" ]; then + db_user="$(id -un)" + fi + conn="postgresql://$db_user@127.0.0.1:$port/postgres" + + rm -rf "$target_run_dir" "$target_report_dir" + mkdir -p "$pgdata" "$target_report_dir" + if [ "$target" = wasix ]; then + shared_memory_prepare_args=( + prepare + --provider "$shared_memory_provider" + --evidence "$shared_memory_provider_evidence" + --measurement-id "$run_label" + --target wasix + --output-format path-sha256-tsv + ) + if [ "$shared_memory_provider" = portable-file-v1 ]; then + shared_memory_prepare_args+=(--portable-root "$dev_shm") + else + # Trap-visible before the helper allocates. If output parsing, a + # catchable signal, or verification interrupts adoption, exit cleanup + # recovers the exact root from the immutable provider receipt. + register_pending_external_shared_memory_provider \ + "$shared_memory_provider" "$shared_memory_provider_evidence" \ + "$shared_memory_provider_cleanup" "$shared_memory_provider_exit_objects" \ + "$shared_memory_provider_exit_release" || return 125 + fi + shared_memory_prepare_identity="$( + python3 "$FRESH_ROOT/lib/shared_memory_provider.py" \ + "${shared_memory_prepare_args[@]}" + )" || return 125 + IFS=$'\t' read -r dev_shm shared_memory_provider_sha256 \ + shared_memory_prepare_extra <<<"$shared_memory_prepare_identity" + if [ -z "$dev_shm" ] || [ -n "$shared_memory_prepare_extra" ] || + [[ ! "$shared_memory_provider_sha256" =~ ^[0-9a-f]{64}$ ]]; then + echo 'shared-memory provider returned an invalid allocation identity' >&2 + return 125 + fi + # The external root becomes trap-owned immediately after the atomic helper + # result is parsed. No fallible hash/verification command can strand it. + if [ "$shared_memory_provider" = linux-tmpfs-v1 ]; then + register_external_shared_memory_provider \ + "$shared_memory_provider" "$dev_shm" \ + "$shared_memory_provider_evidence" "$shared_memory_provider_sha256" \ + "$shared_memory_provider_cleanup" "$shared_memory_provider_exit_objects" \ + "$shared_memory_provider_exit_release" || return 125 + unregister_pending_external_shared_memory_provider \ + "$shared_memory_provider_evidence" || return 125 + fi + python3 "$FRESH_ROOT/lib/shared_memory_provider.py" verify \ + --provider "$shared_memory_provider" --root "$dev_shm" \ + --evidence "$shared_memory_provider_evidence" \ + --evidence-sha256 "$shared_memory_provider_sha256" || return 125 + else + mkdir -p "$dev_shm" + fi + if fresh_tcp_port_open 127.0.0.1 "$port"; then + printf 'refusing to start measured server on occupied port: 127.0.0.1:%s\n' \ + "$port" >&2 + printf '%s\tstartup\t125\t%s\t%s\t0\t\t\t0\t\t0\t0\t0\t0\t%s\t%s\n' \ + "$target" "$connections" "$iterations" "$server_log" "$target_report_dir" \ + >>"$summary_tsv" + return 125 + fi + rm -f "$resource_phase_file" "$resource_stop_file" + set_resource_phase "$resource_phase_file" "startup" + configure_server_command_prefix "$target" "$port" + + adaptive_sample_window_start_ns="$(now_ns)" || return 125 + set +e + started_server_pid="" + started_server_pgid="" + started_server_birth_identity="" + if [ "$target" = "native" ]; then + start_native_server "$pgdata" "$port" "$initdb_log" "$server_log" "$limits_record" + start_status=$? + else + start_wasix_server "$pgdata" "$dev_shm" "$port" "$initdb_log" "$server_log" \ + "$wasix_perf_initdb_log" "$wasix_perf_server_log" "$limits_record" \ + "$wait_dump_fence_request" "$wait_dump_fence_ack" \ + "$cold_residency_receipt" "$sealed_loader_audit" \ + "$cache_offer_initdb_telemetry" "$cache_offer_postgres_telemetry" + start_status=$? + fi + server_pid="$started_server_pid" + server_pgid="$started_server_pgid" + server_birth_identity="$started_server_birth_identity" + set -e + + if [ "$start_status" -ne 0 ] || [ -z "$server_pid" ]; then + printf '%s\tstartup\t%s\t%s\t%s\t0\t\t\t0\t\t0\t0\t0\t0\t%s\t%s\n' \ + "$target" "$start_status" "$connections" "$iterations" "$server_log" "$target_report_dir" \ + >>"$summary_tsv" + if [ "$start_status" -eq 0 ]; then + append_libpq_latency_target_failure "$target" "server_start_missing_pid" \ + "$target_report_dir" + else + append_libpq_latency_target_failure "$target" "server_start_exit_$start_status" \ + "$target_report_dir" + fi + return 1 + fi + if [ -z "$server_birth_identity" ] || [ "$server_pgid" != "$server_pid" ]; then + printf 'could not capture exact server process-group identity: pid=%s pgid=%s\n' \ + "$server_pid" "$server_pgid" >&2 + fresh_terminate_owned_process_group "$server_pgid" "$server_pid" \ + "$server_birth_identity" 2>/dev/null || true + return 125 + fi + if ! capture_server_cgroup_identity "$server_pid" "$server_cgroup_unit"; then + fresh_terminate_owned_process_group "$server_pgid" "$server_pid" \ + "$server_birth_identity" || true + return 125 + fi + server_cgroup_dir="$captured_server_cgroup_dir" + server_cgroup_identity="$captured_server_cgroup_identity" + server_cgroup_membership_path="$captured_server_cgroup_membership_path" + if ! capture_server_cgroup_limits \ + "$server_cgroup_dir" "$server_cgroup_identity"; then + fresh_terminate_owned_process_group "$server_pgid" "$server_pid" \ + "$server_birth_identity" || true + return 125 + fi + server_cgroup_memory_max_bytes="$captured_server_cgroup_memory_max_bytes" + server_cgroup_memory_high_bytes="$captured_server_cgroup_memory_high_bytes" + server_cgroup_swap_max_bytes="$captured_server_cgroup_swap_max_bytes" + if [ "$adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ] && + { [ -z "$server_cgroup_identity" ] || + [ "$server_cgroup_memory_max_bytes" != "$cgroup_memory_max_bytes" ] || + [ "$server_cgroup_memory_high_bytes" != "$cgroup_memory_high_bytes" ] || + [ "$server_cgroup_swap_max_bytes" != "$cgroup_swap_max_bytes" ]; }; then + printf 'measured server cgroup contract differs from requested exact limits: identity=%s requested=%s/%s/%s observed=%s/%s/%s\n' \ + "${server_cgroup_identity:-none}" "$cgroup_memory_max_bytes" \ + "$cgroup_memory_high_bytes" "$cgroup_swap_max_bytes" \ + "$server_cgroup_memory_max_bytes" "$server_cgroup_memory_high_bytes" \ + "$server_cgroup_swap_max_bytes" >&2 + fresh_terminate_owned_process_group "$server_pgid" "$server_pid" \ + "$server_birth_identity" || true + return 125 + fi + register_server_pid "$server_pid" "$server_pgid" "$server_birth_identity" \ + "$port" "$server_cgroup_dir" "$server_cgroup_identity" + for _ in 1 2 3 4 5 6 7 8 9 10; do + [ -s "$limits_record" ] && break + sleep 0.01 + done + if ! record_server_limits "$target" "$limits_record" "$server_pid"; then + stop_server "$server_pid" "$server_pgid" "$server_birth_identity" "$port" \ + "$server_cgroup_dir" "$server_cgroup_identity" "$server_log" \ + "$shutdown_report" "$target" "$server_cgroup_membership_path" || true + return 125 + fi + + if [ "$resource_detail" != "off" ]; then + monitor_resource_usage "$target" "$server_pid" "$resource_phase_file" "$resource_stop_file" \ + "$resource_samples_tsv" "$resource_sample_interval" "$resource_detail" \ + "$server_cgroup_dir" \ + 2>"$resource_sampler_log" & + resource_monitor_pid="$!" + register_background_pid "$resource_monitor_pid" + fi + + set_resource_phase "$resource_phase_file" "readiness" + if ! wait_for_ready "$conn" "$server_pid" "$wait_log"; then + printf '%s\treadiness\t1\t%s\t%s\t0\t\t\t0\t\t0\t0\t0\t0\t%s\t%s\n' \ + "$target" "$connections" "$iterations" "$server_log" "$target_report_dir" >>"$summary_tsv" + append_libpq_latency_target_failure "$target" "readiness_failed" "$target_report_dir" + set_resource_phase "$resource_phase_file" "stopping" + stop_server "$server_pid" "$server_pgid" "$server_birth_identity" "$port" \ + "$server_cgroup_dir" "$server_cgroup_identity" "$server_log" \ + "$shutdown_report" "$target" "$server_cgroup_membership_path" || true + if [ -n "$resource_monitor_pid" ]; then + touch "$resource_stop_file" + set +e + wait "$resource_monitor_pid" + resource_monitor_status=$? + set -e + unregister_background_pid "$resource_monitor_pid" + summarize_resource_usage_checked "$target" "$resource_samples_tsv" \ + "$resource_tsv" || true + validate_target_memory_evidence "$target" "$target_report_dir" \ + "$resource_samples_tsv" || true + if [ "$resource_monitor_status" -ne 0 ]; then + printf 'resource sampler failed for %s; see %s\n' "$target" "$resource_sampler_log" >&2 + fi + fi + return 1 + fi + + if [ "$cold_ownership" -eq 1 ]; then + if [ -z "$cold_spawn_monotonic_ns" ] || + [ -z "$cold_first_query_monotonic_ns" ] || + [ "$cold_readiness_attempts" -lt 1 ] || + ! python3 "$FRESH_ROOT/bin/capture-linux-cgroup-v2.py" \ + --cgroup-dir "$server_cgroup_dir" \ + --cgroup-identity "$server_cgroup_identity" \ + --output "$cold_first_query_snapshot" + then + echo "failed to capture exact cold first-query cgroup evidence" >&2 + target_status=1 + cold_evidence_status=1 + fi + fi + + if [ "$target" = wasix ]; then + if ! python3 "$FRESH_ROOT/lib/shared_memory_provider.py" capture-objects \ + --provider "$shared_memory_provider" --root "$dev_shm" \ + --evidence "$shared_memory_provider_evidence" \ + --evidence-sha256 "$shared_memory_provider_sha256" \ + --output "$shared_memory_provider_objects" --require-main yes \ + --cgroup-identity "${server_cgroup_identity:-none}"; then + printf 'live PostgreSQL shared-memory backing validation failed for %s; see %s\n' \ + "$target" "$target_report_dir" >&2 + target_status=1 + fi + fi + + snapshot_memory_map "$target" "$server_pid" "readiness" "$target_report_dir" + if capture_checkpoint_settings \ + "$conn" "$target_report_dir/effective-postgres-settings.tsv"; then + if [ "$profile_resolution_active" -eq 1 ] && + ! fresh_validate_postgres_profile_settings \ + "$target_report_dir/effective-postgres-settings.tsv" \ + "$target_report_dir/effective-postgres-profile-validation.tsv"; then + printf 'effective PostgreSQL profile validation failed for %s\n' \ + "$target" >&2 + target_status=1 + fi + else + printf 'could not capture target-level effective PostgreSQL settings for %s\n' \ + "$target" >&2 + target_status=1 + fi + if [ "$wasix_lifecycle_plateau" -eq 1 ]; then + if ! run_wasix_lifecycle_plateau "$conn" "$pgdata" "$wasix_perf_server_log" \ + "$resource_phase_file" "$target_report_dir" "$wait_dump_fence_request" \ + "$wait_dump_fence_ack" "$server_pid"; then + printf 'WASIX lifecycle plateau validation failed; see %s\n' \ + "$lifecycle_plateau_tsv" >&2 + target_status=1 + fi + fi + if float_gt_zero "$quiescence_seconds"; then + set_resource_phase "$resource_phase_file" "quiescence:readiness" + sleep "$quiescence_seconds" + snapshot_memory_map "$target" "$server_pid" "readiness-quiescent" "$target_report_dir" + fi + set_resource_phase "$resource_phase_file" "idle" + if [ "$libpq_latency_samples" -gt 0 ]; then + if ! run_libpq_latency_suite "$target" "$conn" "$target_report_dir" \ + "$resource_phase_file" "$server_pid" + then + target_status=1 + fi + fi + for workload in "${workloads[@]}"; do + if run_workload "$target" "$workload" "$conn" "$target_report_dir" "$server_log" "$resource_phase_file" "$server_pid"; then + workload_status=0 + else + workload_status=$? + fi + if [ "$workload_status" -ne 0 ]; then + target_status=1 + fi + done + + if [ "$cold_ownership" -eq 1 ]; then + set_resource_phase "$resource_phase_file" "cold:post-first-query" + if ! python3 "$FRESH_ROOT/bin/capture-linux-cgroup-v2.py" \ + --cgroup-dir "$server_cgroup_dir" \ + --cgroup-identity "$server_cgroup_identity" \ + --output "$cold_final_snapshot" + then + echo "failed to capture exact cold final cgroup evidence" >&2 + target_status=1 + cold_evidence_status=1 + fi + fi + if [ "$adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ]; then + if ! capture_server_cgroup_limits \ + "$server_cgroup_dir" "$server_cgroup_identity"; then + echo 'could not recapture final exact server cgroup limits' >&2 + adaptive_sample_contract_status=failed + target_status=1 + else + final_server_cgroup_memory_max_bytes="$captured_server_cgroup_memory_max_bytes" + final_server_cgroup_memory_high_bytes="$captured_server_cgroup_memory_high_bytes" + final_server_cgroup_swap_max_bytes="$captured_server_cgroup_swap_max_bytes" + if [ "$final_server_cgroup_memory_max_bytes" != "$server_cgroup_memory_max_bytes" ] || + [ "$final_server_cgroup_memory_high_bytes" != "$server_cgroup_memory_high_bytes" ] || + [ "$final_server_cgroup_swap_max_bytes" != "$server_cgroup_swap_max_bytes" ]; then + echo 'server cgroup limits changed during the measured target lifetime' >&2 + adaptive_sample_contract_status=failed + target_status=1 + fi + fi + fi + set_resource_phase "$resource_phase_file" "stopping" + if stop_server "$server_pid" "$server_pgid" "$server_birth_identity" "$port" \ + "$server_cgroup_dir" "$server_cgroup_identity" "$server_log" \ + "$shutdown_report" "$target" "$server_cgroup_membership_path"; then + stop_status=0 + else + stop_status=$? + target_status=1 + fi + if [ "$target" = wasix ]; then + if [ "$stop_status" -eq 0 ]; then + shutdown_report_sha256="$( + fresh_wasmer_bin_hash "$shutdown_report" + )" || shared_memory_release_status=1 + if [ "$shared_memory_release_status" -eq 0 ] && + ! python3 "$FRESH_ROOT/lib/shared_memory_provider.py" assert-empty \ + --provider "$shared_memory_provider" --root "$dev_shm" \ + --evidence "$shared_memory_provider_evidence" \ + --evidence-sha256 "$shared_memory_provider_sha256" \ + --output "$shared_memory_provider_release" \ + --release-kind clean-postgresql-shutdown-v1 \ + --lifecycle-evidence "$shutdown_report" \ + --lifecycle-evidence-sha256 "$shutdown_report_sha256"; then + shared_memory_release_status=1 + fi + else + shared_memory_release_status=1 + fi + if [ "$shared_memory_release_status" -ne 0 ]; then + printf 'clean WASIX shared-memory release validation failed for %s; see %s\n' \ + "$target" "$target_report_dir" >&2 + target_status=1 + python3 "$FRESH_ROOT/lib/shared_memory_provider.py" capture-objects \ + --provider "$shared_memory_provider" --root "$dev_shm" \ + --evidence "$shared_memory_provider_evidence" \ + --evidence-sha256 "$shared_memory_provider_sha256" \ + --output "$shared_memory_provider_post_shutdown_objects" \ + --require-main no --cgroup-identity post-shutdown || true + fi + fi + adaptive_sample_window_end_ns="$(now_ns)" || { + echo 'could not capture adaptive sample window end' >&2 + target_status=1 + } + if [ "$target" = wasix ] && [ "$wasix_runtime_mode" = sealed-headless ]; then + sample_contract_mode=portable-not-required + contract_cgroup_path=none + contract_cgroup_identity=none + contract_memory_max_bytes=none + contract_memory_high_bytes=none + contract_swap_max_bytes=none + contract_final_memory_max_bytes=none + contract_final_memory_high_bytes=none + contract_final_swap_max_bytes=none + contract_window_start_ns=none + contract_window_end_ns=none + if [ "$adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ]; then + sample_contract_mode=constrained-exact-cgroup-time + # cgroup_path is the unified-hierarchy membership path from + # /proc/PID/cgroup. The controller filesystem path is deliberately + # kept separate in server_cgroup_dir: the two live in different path + # namespaces and must never be compared lexically. + contract_cgroup_path="$server_cgroup_membership_path" + contract_cgroup_identity="$server_cgroup_identity" + contract_memory_max_bytes="$server_cgroup_memory_max_bytes" + contract_memory_high_bytes="$server_cgroup_memory_high_bytes" + contract_swap_max_bytes="$server_cgroup_swap_max_bytes" + contract_final_memory_max_bytes="$final_server_cgroup_memory_max_bytes" + contract_final_memory_high_bytes="$final_server_cgroup_memory_high_bytes" + contract_final_swap_max_bytes="$final_server_cgroup_swap_max_bytes" + contract_window_start_ns="$adaptive_sample_window_start_ns" + contract_window_end_ns="$adaptive_sample_window_end_ns" + fi + printf 'schema_version\tmeasurement_id\ttarget\tacceptance_policy\tcontract_mode\tbase_policy_sha256\tvalidator_sha256\tmanifest_sha256\tcgroup_path\tcgroup_identity\tserver_pid\tserver_birth_identity\tcgroup_unit\trequested_memory_max\trequested_memory_high\trequested_swap_max\trequested_memory_max_bytes\trequested_memory_high_bytes\trequested_swap_max_bytes\tobserved_initial_memory_max_bytes\tobserved_initial_memory_high_bytes\tobserved_initial_swap_max_bytes\tobserved_final_memory_max_bytes\tobserved_final_memory_high_bytes\tobserved_final_swap_max_bytes\tsample_window_start_monotonic_ns\tsample_window_end_monotonic_ns\tstatus\n' \ + >"$adaptive_cache_sample_contract" + { + printf '%s' oliphaunt.wasix-postmaster.adaptive-cache-sample-contract.v1 + printf '\t%s' "$run_label" "$target" "$adaptive_cache_evidence_policy" \ + "$sample_contract_mode" "$adaptive_cache_evidence_policy_identity" \ + "$adaptive_cache_validator_sha256" \ + "$adaptive_cache_bound_manifest_sha256" "$contract_cgroup_path" \ + "$contract_cgroup_identity" "$server_pid" "$server_birth_identity" \ + "${server_cgroup_unit:-none}" "${cgroup_memory_max:-none}" \ + "${cgroup_memory_high:-none}" "${cgroup_swap_max:-none}" \ + "$([ "$sample_contract_mode" = constrained-exact-cgroup-time ] && printf '%s' "$cgroup_memory_max_bytes" || printf none)" \ + "$([ "$sample_contract_mode" = constrained-exact-cgroup-time ] && printf '%s' "$cgroup_memory_high_bytes" || printf none)" \ + "$([ "$sample_contract_mode" = constrained-exact-cgroup-time ] && printf '%s' "$cgroup_swap_max_bytes" || printf none)" \ + "$contract_memory_max_bytes" "$contract_memory_high_bytes" \ + "$contract_swap_max_bytes" "$contract_final_memory_max_bytes" \ + "$contract_final_memory_high_bytes" "$contract_final_swap_max_bytes" \ + "$contract_window_start_ns" "$contract_window_end_ns" \ + "$adaptive_sample_contract_status" + printf '\n' + } >>"$adaptive_cache_sample_contract" + chmod 0444 "$adaptive_cache_sample_contract" + if ! python3 "$FRESH_ROOT/bin/validate-file-cache-telemetry.py" \ + --telemetry "$cache_offer_initdb_telemetry" \ + --manifest "$sealed_manifest" \ + --output "$cache_offer_initdb_validation" \ + --expected-workload runtime:initdb + then + printf 'initdb cache-offer telemetry validation failed for %s; see %s\n' \ + "$target" "$target_report_dir" >&2 + target_status=1 + else + chmod 0444 "$cache_offer_initdb_telemetry" + fi + if ! python3 "$FRESH_ROOT/bin/validate-file-cache-telemetry.py" \ + --telemetry "$cache_offer_postgres_telemetry" \ + --manifest "$sealed_manifest" \ + --output "$cache_offer_postgres_validation" \ + --expected-workload runtime:postgres + then + printf 'postmaster cache-offer telemetry validation failed for %s; see %s\n' \ + "$target" "$target_report_dir" >&2 + target_status=1 + else + chmod 0444 "$cache_offer_postgres_telemetry" + fi + if ! assert_frozen_adaptive_cache_evidence_policy; then + printf 'adaptive cache evidence policy changed before validation for %s\n' \ + "$target" >&2 + target_status=1 + else + adaptive_validator_args=( + --telemetry "$cache_offer_postgres_adaptive_telemetry" + --manifest "$sealed_manifest" + --output "$cache_offer_postgres_adaptive_validation" + --acceptance-policy "$adaptive_cache_evidence_policy" + --measurement-id "$run_label" + --target "$target" + ) + if [ "$adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ]; then + adaptive_validator_args+=( + --cgroup-identity "$contract_cgroup_identity" + --cgroup-memory-max-bytes "$contract_memory_max_bytes" + --cgroup-memory-high-bytes "$contract_memory_high_bytes" + --cgroup-swap-max-bytes "$contract_swap_max_bytes" + --sample-window-start-monotonic-ns "$contract_window_start_ns" + --sample-window-end-monotonic-ns "$contract_window_end_ns" + ) + fi + if ! python3 "$adaptive_cache_validator" "${adaptive_validator_args[@]}" + then + printf 'postmaster adaptive cache telemetry validation failed for %s; see %s\n' \ + "$target" "$target_report_dir" >&2 + target_status=1 + else + chmod 0444 "$cache_offer_postgres_adaptive_telemetry" + fi + fi + fi + if [ "$target" = wasix ] && [ "$require_zero_write_aot" -eq 1 ]; then + if ! python3 "$FRESH_ROOT/bin/validate-sealed-loader-audit.py" \ + --audit "$sealed_loader_audit" \ + --manifest "$sealed_manifest" \ + --output "$sealed_loader_validation" \ + --required-snapshot-mode direct-immutable-inode + then + printf 'direct immutable sealed-loader audit validation failed for %s; see %s\n' \ + "$target" "$target_report_dir" >&2 + target_status=1 + else + chmod 0444 "$sealed_loader_audit" + fi + fi + if [ -n "$resource_monitor_pid" ]; then + touch "$resource_stop_file" + set +e + wait "$resource_monitor_pid" + resource_monitor_status=$? + set -e + unregister_background_pid "$resource_monitor_pid" + if ! summarize_resource_usage_checked "$target" "$resource_samples_tsv" \ + "$resource_tsv"; then + target_status=1 + fi + if ! validate_target_memory_evidence "$target" "$target_report_dir" \ + "$resource_samples_tsv"; then + printf 'memory evidence validation failed for %s; see %s\n' "$target" \ + "$target_report_dir/memory-evidence.tsv" >&2 + target_status=1 + fi + if [ "$resource_monitor_status" -ne 0 ]; then + printf 'resource sampler failed for %s; see %s\n' "$target" "$resource_sampler_log" >&2 + target_status=1 + fi + else + validate_target_memory_evidence "$target" "$target_report_dir" \ + "$resource_samples_tsv" || target_status=1 + fi + if [ "$cold_ownership" -eq 1 ]; then + if [ "$cold_evidence_status" -ne 0 ] || + ! python3 "$FRESH_ROOT/bin/validate-wasix-cold-ownership.py" \ + --residency-receipt "$cold_residency_receipt" \ + --first-query-snapshot "$cold_first_query_snapshot" \ + --final-snapshot "$cold_final_snapshot" \ + --resource-samples "$resource_samples_tsv" \ + --execution-identity "$execution_identity_tsv" \ + --carrier-root "$sealed_carrier_root" \ + --pgdata-root "$pgdata" \ + --spawn-monotonic-ns "$cold_spawn_monotonic_ns" \ + --first-query-monotonic-ns "$cold_first_query_monotonic_ns" \ + --readiness-attempts "$cold_readiness_attempts" \ + --memory-max "$cgroup_memory_max" \ + --memory-high "$cgroup_memory_high" \ + --swap-max "$cgroup_swap_max" \ + --output "$cold_sample" + then + printf 'cold ownership validation failed for %s; see %s\n' \ + "$target" "$target_report_dir" >&2 + target_status=1 + fi + fi + if [ "$target_status" -ne 0 ]; then + invalidate_derived_summary "$target_status" "$target" + invalidate_libpq_latency_summary "$target_status" "$target" + fi + if [ "$target" = "wasix" ] && [ "$wasix_perf_stats" = "1" ] && [ -s "$wasix_perf_server_log" ]; then + "$FRESH_ROOT/bin/summarize-wasix-perf-stats.sh" \ + "$wasix_perf_server_log" \ + "$target_report_dir/wasix-perf-server" \ + >"$target_report_dir/wasix-perf-summary.log" 2>&1 || true + fi + provider_retention=not-applicable + if [ "$target" = wasix ]; then + if [ "$shared_memory_provider" = linux-tmpfs-v1 ]; then + if release_external_shared_memory_providers target-complete; then + provider_retention=removed-after-target + else + provider_retention=retained-cleanup-refused + [ "$target_status" -ne 0 ] || target_status=125 + invalidate_derived_summary "$target_status" "$target" + invalidate_libpq_latency_summary "$target_status" "$target" + fi + else + provider_retention=retained + fi + fi + if [ "$discard_pgdata" -eq 1 ] && [ "$target_status" -eq 0 ]; then + if [ "$target" = wasix ] && + [ "$shared_memory_provider" = portable-file-v1 ]; then + if python3 "$FRESH_ROOT/lib/shared_memory_provider.py" cleanup \ + --provider "$shared_memory_provider" --root "$dev_shm" \ + --evidence "$shared_memory_provider_evidence" \ + --evidence-sha256 "$shared_memory_provider_sha256" \ + --cleanup-evidence "$shared_memory_provider_cleanup" \ + --reason successful-discard; then + provider_retention=removed-after-success + else + target_status=125 + fi + fi + if [ "$target_status" -eq 0 ]; then + if [ "$target" = native ]; then + rm -rf -- "$pgdata" "$dev_shm" + else + rm -rf -- "$pgdata" + fi + retention_status=discarded-after-success + else + retention_status=retained-cleanup-failed + invalidate_derived_summary "$target_status" "$target" + invalidate_libpq_latency_summary "$target_status" "$target" + fi + else + retention_status=retained + fi + { + printf 'status=%s\npgdata=%s\ndev_shm=%s\n' \ + "$retention_status" "$pgdata" "$dev_shm" + printf 'shared_memory_provider=%s\n' \ + "$([ "$target" = wasix ] && printf '%s' "$shared_memory_provider" || printf not-applicable)" + printf 'shared_memory_provider_evidence=%s\n' \ + "$([ "$target" = wasix ] && printf '%s' "$shared_memory_provider_evidence" || printf none)" + printf 'shared_memory_provider_evidence_sha256=%s\n' \ + "$shared_memory_provider_sha256" + printf 'shared_memory_provider_retention=%s\n' "$provider_retention" + } >"$target_report_dir/run-retention.txt" + return "$target_status" +} + +if [ "$need_wasix" -eq 1 ]; then + prepare_wasix_runtime + write_execution_identity + if [ "$wasix_lifecycle_plateau" -eq 1 ]; then + write_lifecycle_baseline_binding + fi + { + printf -- '- WASIX runtime mode: `%s`\n' "$wasix_runtime_mode" + printf -- '- Wasmer binary: `%s`\n' "$wasmer_bin" + printf -- '- Wasmer binary hash: `%s`\n' "$wasmer_bin_hash" + printf -- '- Execution identity receipt: `%s` (`%s`)\n' \ + "$execution_identity_tsv" "$execution_identity_sha256" + if [ "$wasix_runtime_mode" = "sealed-headless" ]; then + printf -- '- Wasmer version: `%s`\n' "$wasmer_version" + printf -- '- Wasmer cache: `disabled (--disable-cache; sealed in-memory modules only)`\n' + printf -- '- Wasmer compiler: `unavailable (headless runtime)`\n' + printf -- '- Sealed carrier: `%s`\n' "$sealed_carrier_root" + printf -- '- Sealed manifest: `%s`\n' "$sealed_manifest" + printf -- '- Sealed manifest SHA-256: `%s`\n' "$sealed_manifest_hash" + printf -- '- Sealed receipt: `%s`\n' "$sealed_receipt" + printf -- '- Sealed receipt SHA-256: `%s`\n' "$sealed_receipt_hash" + printf -- '- Sealed payload inventory: `%s`\n' "$sealed_payload_inventory" + printf -- '- Sealed payload inventory SHA-256: `%s`\n' "$sealed_payload_inventory_hash" + printf -- '- Sealed initdb module: `%s`\n' "$wasix_initdb_module" + printf -- '- Sealed postgres module: `%s`\n' "$wasix_postgres_module" + printf -- '- Captured sealed manifest: `%s`\n' "$report_dir/wasix-sealed-manifest.json" + printf -- '- Captured sealed receipt: `%s`\n' "$report_dir/wasix-sealed-wasmer-build.receipt" + printf -- '- Captured sealed payload inventory: `%s`\n' "$report_dir/wasix-sealed-payload.files" + else + printf -- '- Wasmer version: `%s`\n' "$(fresh_wasmer_version "$wasmer_bin" 2>/dev/null || true)" + printf -- '- Wasmer cache dir: `%s`\n' "$wasmer_cache_dir" + printf -- '- Wasmer compiler: `%s`\n' "$wasmer_compiler" + printf -- '- Wasmer LLVM opt level: `%s`\n' "$wasmer_llvm_opt_level" + printf -- '- WASMER_LLVM_NATIVE_CPU: `%s`\n' "${WASMER_LLVM_NATIVE_CPU:-0}" + printf -- '- WASMER_LLVM_FULL_O3_PIPELINE: `%s`\n' "${WASMER_LLVM_FULL_O3_PIPELINE:-0}" + printf -- '- WASMER_LLVM_INDIRECT_CALL_CACHE: `%s`\n' "${WASMER_LLVM_INDIRECT_CALL_CACHE:-0}" + printf -- '- WASMER_LLVM_VOLATILE_MEMOPS: `%s`\n' "${WASMER_LLVM_VOLATILE_MEMOPS:-0}" + printf -- '- Wasmer compiler threads: `%s`\n' "$wasmer_compiler_threads" + printf -- '- Skip precompile: `%s`\n' "$skip_precompile" + fi + printf -- '- Wasmer stack size: `%s`\n' "$wasmer_stack_size" + printf -- '- Sealed loader policy receipt: `%s` (`%s`)\n' \ + "$sealed_loader_policy_tsv" "$sealed_loader_policy_identity" + printf -- '- Require zero-write AOT/images: `%s`\n' "$require_zero_write_aot" + if [ "$require_zero_write_aot" -eq 1 ]; then + printf -- '- Immutable deployment receipt: `%s` (`%s`, dev `%s`, ino `%s`)\n' \ + "$immutable_carrier_receipt" "$immutable_carrier_receipt_sha256" \ + "$immutable_carrier_receipt_dev" "$immutable_carrier_receipt_ino" + printf -- '- Immutable carrier closure identity: `%s`\n' \ + "$immutable_carrier_closure_identity" + fi + if [ "$wasix_lifecycle_plateau" -eq 1 ]; then + printf -- '- Lifecycle baseline binding: `%s`\n' \ + "$lifecycle_baseline_binding_tsv" + printf -- '- Lifecycle baseline binding SHA-256: `%s`\n' \ + "$lifecycle_baseline_binding_identity" + fi + printf '\n' + } >>"$summary" +fi + +trap cleanup_active_servers EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +overall_status=0 +port="$start_port" +for target in "${targets[@]}"; do + if ! assert_frozen_measurement_tool_closure; then + overall_status=1 + break + fi + if ! assert_frozen_adaptive_cache_evidence_policy; then + overall_status=1 + break + fi + if ! assert_frozen_memory_budget; then + overall_status=1 + break + fi + if ! assert_frozen_lifecycle_baseline; then + overall_status=1 + break + fi + if ! assert_frozen_execution_identity; then + overall_status=1 + break + fi + if [ "$profile_resolution_active" -eq 1 ] && + ! fresh_assert_postgres_profile_inputs; then + overall_status=1 + break + fi + if run_target "$target" "$port"; then + target_status=0 + else + target_status=$? + fi + if [ "$target_status" -ne 0 ]; then + overall_status=1 + fi + if ! release_external_shared_memory_providers target-return; then + invalidate_derived_summary 125 "$target" + invalidate_libpq_latency_summary 125 "$target" + overall_status=1 + fi + if [ "$profile_resolution_active" -eq 1 ] && + ! fresh_assert_postgres_profile_inputs; then + invalidate_derived_summary 125 "$target" + invalidate_libpq_latency_summary 125 "$target" + overall_status=1 + break + fi + if ! assert_frozen_lifecycle_baseline; then + invalidate_derived_summary 125 "$target" + invalidate_libpq_latency_summary 125 "$target" + overall_status=1 + break + fi + if ! assert_frozen_execution_identity; then + invalidate_derived_summary 125 "$target" + invalidate_libpq_latency_summary 125 "$target" + overall_status=1 + break + fi + if ! assert_frozen_measurement_tool_closure; then + invalidate_derived_summary 125 "$target" + invalidate_libpq_latency_summary 125 "$target" + overall_status=1 + break + fi + if ! assert_frozen_adaptive_cache_evidence_policy; then + invalidate_derived_summary 125 "$target" + invalidate_libpq_latency_summary 125 "$target" + overall_status=1 + break + fi + port=$((port + 1)) +done +if ! assert_frozen_measurement_tool_closure; then + overall_status=1 +fi +if ! assert_frozen_adaptive_cache_evidence_policy; then + overall_status=1 +fi +if ! assert_frozen_memory_budget; then + overall_status=1 +fi +if ! assert_frozen_lifecycle_baseline; then + overall_status=1 +fi +if ! assert_frozen_execution_identity; then + overall_status=1 +fi + +{ + printf '\n## Results\n\n' + printf -- '- Exit code: `%s`\n' "$overall_status" + printf -- '- Summary TSV: `%s`\n' "$summary_tsv" + printf -- '- Client TSV: `%s`\n' "$client_tsv" + printf -- '- Resource TSV: `%s`\n' "$resource_tsv" + printf -- '- Checkpoint TSV: `%s`\n' "$checkpoint_tsv" + printf -- '- Memory budget TSV: `%s` (`%s`)\n' \ + "$memory_budget_tsv" "$memory_budget_identity" + printf -- '- Adaptive cache evidence policy TSV: `%s` (`%s`)\n' \ + "$adaptive_cache_evidence_policy_tsv" \ + "$adaptive_cache_evidence_policy_identity" + printf -- '- Measurement-tool closure TSV: `%s` (`%s`)\n' \ + "$measurement_tool_closure_tsv" \ + "$measurement_tool_closure_evidence_identity" + if [ "$need_wasix" -eq 1 ]; then + printf -- '- WASIX shared-memory provider: `%s`\n' \ + "$shared_memory_provider" + printf -- '- WASIX shared-memory provider receipt: `%s/wasix/shared-memory-provider.json`\n' \ + "$report_dir" + printf -- '- WASIX live shared-object receipt: `%s/wasix/shared-memory-objects.json`\n' \ + "$report_dir" + printf -- '- WASIX shared-memory release/cleanup receipts: `%s/wasix/shared-memory-{release,cleanup}.json`\n' \ + "$report_dir" + fi + if [ "$measurement_tool_closure_mode" = content-addressed-read-only ]; then + printf -- '- Captured measurement-tool file manifest: `%s`\n' \ + "$measurement_tool_closure_files_tsv" + fi + if [ "$libpq_latency_samples" -gt 0 ]; then + printf -- '- Native libpq true-latency summary TSV: `%s`\n' "$libpq_latency_tsv" + printf -- '- Native libpq raw latency TSVs: `%s//libpq-latency/{persistent,reconnect}.raw.tsv`\n' "$report_dir" + printf -- '- Native libpq latency status/logs: `%s//libpq-latency/`\n' "$report_dir" + fi + if [ "$wasix_perf_stats" = "1" ]; then + printf -- '- WASIX perf stats TSV: `%s/wasix/wasix-perf-server.tsv`\n' "$report_dir" + printf -- '- WASIX perf stats top time TSV: `%s/wasix/wasix-perf-server.top-time.tsv`\n' "$report_dir" + fi + if [ "$require_zero_write_aot" -eq 1 ]; then + printf -- '- Direct immutable loader audit: `%s/wasix/sealed-loader-audit.jsonl`\n' "$report_dir" + printf -- '- Direct immutable loader validation: `%s/wasix/sealed-loader-audit-validation.tsv`\n' "$report_dir" + fi + if [ "$wasix_wait_dump_interval_ms" -gt 0 ]; then + printf -- '- WASIX wait dump log: `%s/wasix/wasix-perf-server.log`\n' "$report_dir" + fi + if [ "$wasix_lifecycle_plateau" -eq 1 ]; then + printf -- '- WASIX lifecycle plateau TSV: `%s`\n' "$lifecycle_plateau_tsv" + printf -- '- WASIX frozen lifecycle evidence: `%s/wasix/wasix-runtime-evidence.log`\n' \ + "$report_dir" + printf -- '- WASIX lifecycle freeze receipt: `%s/wasix/wasix-runtime-evidence.freeze.tsv`\n' \ + "$report_dir" + printf -- '- WASIX lifecycle committed ACK: `%s/wasix/wasix-runtime-fence.ack`\n' \ + "$report_dir" + printf -- '- WASIX lifecycle baseline policy/binding: `%s` (`%s`) / `%s` (`%s`)\n' \ + "$lifecycle_baseline_policy_tsv" "$lifecycle_baseline_policy_identity" \ + "$lifecycle_baseline_binding_tsv" "$lifecycle_baseline_binding_identity" + printf -- '- WASIX lifecycle reconnect log: `%s/wasix/lifecycle-reconnect-churn.log`\n' \ + "$report_dir" + if [ "$wasix_lifecycle_memory_checkpoint_every" -gt 0 ]; then + printf -- '- WASIX lifecycle quiescent memory checkpoints: `%s`\n' \ + "$lifecycle_memory_checkpoints_tsv" + printf -- '- WASIX lifecycle memory plateau validation: `%s`\n' \ + "$lifecycle_memory_plateau_tsv" + fi + fi + if [ "$memory_map_snapshots" = "1" ]; then + printf -- '- Memory map snapshots: `%s//**/memory-maps/`\n' "$report_dir" + fi + if float_gt_zero "$sample_seconds"; then + printf -- '- WASIX sample outputs: `%s/wasix//sample.txt`\n' "$report_dir" + printf -- '- WASIX symbolized sample top TSV: `%s/wasix//symbolized-sample.top.tsv`\n' "$report_dir" + fi + if float_gt_zero "$pg_wait_sample_interval"; then + printf -- '- PostgreSQL wait samples: `%s///pg-wait-samples.tsv`\n' "$report_dir" + printf -- '- PostgreSQL wait summaries: `%s///pg-wait-summary.tsv`\n' "$report_dir" + fi +} >>"$summary" + +if [ "$overall_status" -ne 0 ]; then + printf 'failed: concurrent query suite; see %s\n' "$summary" >&2 +else + printf 'passed: concurrent query suite; see %s\n' "$summary" +fi +exit "$overall_status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-wasix-core-profiles.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-wasix-core-profiles.sh new file mode 100755 index 00000000..fdbec742 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-wasix-core-profiles.sh @@ -0,0 +1,468 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +profiles=() +skip_build=0 +skip_native=0 +start_port="${PGPORT:-55470}" +sql_file="$FRESH_ROOT/bench/sql/perf-probes.sql" +sql_timeout="${WASIX_BENCH_SQL_TIMEOUT:-180}" +bench_rows="${WASIX_BENCH_ROWS:-100000}" + +while [ "$#" -gt 0 ]; do + case "$1" in + --profile) + shift + if [ "$#" -eq 0 ]; then + echo "--profile requires a WASIX_CORE_PROFILE value" >&2 + exit 2 + fi + profiles+=("$(fresh_normalize_wasix_core_profile "$1")") + ;; + --profiles) + shift + if [ "$#" -eq 0 ]; then + echo "--profiles requires a space-separated profile list" >&2 + exit 2 + fi + for profile in $1; do + profiles+=("$(fresh_normalize_wasix_core_profile "$profile")") + done + ;; + --skip-build) + skip_build=1 + ;; + --skip-native) + skip_native=1 + ;; + --sql) + shift + if [ "$#" -eq 0 ]; then + echo "--sql requires a SQL file path" >&2 + exit 2 + fi + sql_file="$1" + ;; + --start-port) + shift + if [ "$#" -eq 0 ]; then + echo "--start-port requires a port number" >&2 + exit 2 + fi + start_port="$1" + ;; + --sql-timeout) + shift + if [ "$#" -eq 0 ]; then + echo "--sql-timeout requires a timeout in seconds, or 0 to disable" >&2 + exit 2 + fi + sql_timeout="$1" + ;; + --rows) + shift + if [ "$#" -eq 0 ]; then + echo "--rows requires the number of generated benchmark rows" >&2 + exit 2 + fi + bench_rows="$1" + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + exit 2 + ;; + esac + shift +done + +if [ "${#profiles[@]}" -eq 0 ]; then + profiles=(safe-o2 o3 o3-wasmopt o3-thinlto release-o3) +fi + +fresh_ensure_dirs + +if [ ! -f "$sql_file" ]; then + echo "SQL benchmark file does not exist: $sql_file" >&2 + exit 2 +fi + +matrix_report_dir="$FRESH_WORK_ROOT/reports/perf-matrix" +matrix_run_root="$FRESH_WORK_ROOT/run/perf-matrix" +summary_tsv="$matrix_report_dir/summary.tsv" +summary_md="$matrix_report_dir/summary.md" +mkdir -p "$matrix_report_dir" "$matrix_run_root" + +now_ms() { + if command -v perl >/dev/null 2>&1; then + perl -MTime::HiRes=time -e 'printf "%.0f\n", time() * 1000' + else + printf '%s000\n' "$(date +%s)" + fi +} + +record_result() { + local target="$1" + local profile="$2" + local phase="$3" + local status="$4" + local ms="$5" + local log="$6" + local notes="$7" + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$profile" "$phase" "$status" "$ms" "$log" "$notes" >>"$summary_tsv" +} + +stop_pid() { + local pid="${1:-}" + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi +} + +run_logged_timeout() { + local timeout_seconds="$1" + shift + local log="$1" + shift + local pid started_ms elapsed_seconds status + + "$@" >"$log" 2>&1 & + pid=$! + started_ms="$(now_ms)" + while kill -0 "$pid" 2>/dev/null; do + if [ "$timeout_seconds" != "0" ]; then + elapsed_seconds=$((( $(now_ms) - started_ms ) / 1000)) + if [ "$elapsed_seconds" -ge "$timeout_seconds" ]; then + { + printf '\nbenchmark command timed out after %s seconds\n' "$timeout_seconds" + printf 'command:' + printf ' %q' "$@" + printf '\n' + } >>"$log" + kill "$pid" 2>/dev/null || true + sleep 1 + kill -9 "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + return 124 + fi + fi + sleep 1 + done + if wait "$pid"; then + return 0 + fi + status=$? + return "$status" +} + +write_header() { + { + printf '# WASIX Core Performance Matrix\n\n' + printf -- '- Generated: `%s`\n' "$(fresh_timestamp)" + printf -- '- SQL probe: `%s`\n' "$sql_file" + printf -- '- SQL rows: `%s`\n' "$bench_rows" + printf -- '- SQL timeout: `%s seconds`\n' "$sql_timeout" + printf -- '- Profiles: `%s`\n' "${profiles[*]}" + printf -- '- Start port: `%s`\n' "$start_port" + printf -- '- Summary TSV: `%s`\n\n' "$summary_tsv" + printf '## Profiles\n\n' + for profile in "${profiles[@]}"; do + fresh_resolve_wasix_core_profile "$profile" + printf -- '- `%s`: %s\n' "$profile" "$FRESH_WASIX_CORE_PROFILE_DESCRIPTION" + printf ' - CFLAGS: `%s`\n' "$FRESH_WASIX_CORE_PROFILE_CFLAGS" + printf ' - LDFLAGS: `%s`\n' "$FRESH_WASIX_CORE_PROFILE_LDFLAGS" + printf ' - wasm-opt: `%s`\n' "$FRESH_WASIX_CORE_PROFILE_WASM_OPT" + printf ' - wasm-opt flags: `%s`\n' "$FRESH_WASIX_CORE_PROFILE_WASM_OPT_FLAGS" + printf ' - suppress implicit wasm-opt defaults: `%s`\n' "$FRESH_WASIX_CORE_PROFILE_WASM_OPT_SUPPRESS_DEFAULT" + printf ' - install: `%s`\n' "$(fresh_wasix_core_install_dir_for "$profile")" + done + printf '\n## Results\n\n' + printf 'Raw phase results are in `%s`.\n' "$summary_tsv" + } >"$summary_md" + + printf 'target\tprofile\tphase\tstatus\tms\tlog\tnotes\n' >"$summary_tsv" +} + +bench_native() { + local port="$1" + local run_root="$matrix_run_root/native" + local report_root="$matrix_report_dir/native" + local pgdata="$run_root/pgdata" + local server_log="$report_root/server.log" + local initdb_log="$report_root/initdb.log" + local wait_log="$report_root/wait.log" + local sql_log="$report_root/sql.log" + local server_pid="" + local started_ms ended_ms phase_ms status + + mkdir -p "$run_root" "$report_root" + rm -rf "$pgdata" + + if [ "$skip_build" -ne 1 ]; then + "$FRESH_ROOT/bin/build-native-oracle.sh" >/dev/null + fi + + started_ms="$(now_ms)" + set +e + "$NATIVE_INSTALL_DIR/bin/initdb" \ + -D "$pgdata" \ + -A trust \ + --no-locale \ + --encoding=UTF8 \ + --no-instructions \ + >"$initdb_log" 2>&1 + status=$? + set -e + ended_ms="$(now_ms)" + phase_ms=$((ended_ms - started_ms)) + record_result native native initdb "$status" "$phase_ms" "$initdb_log" "" + if [ "$status" -ne 0 ]; then + return "$status" + fi + + started_ms="$(now_ms)" + set +e + "$NATIVE_INSTALL_DIR/bin/postgres" \ + -D "$pgdata" \ + -h 127.0.0.1 \ + -p "$port" \ + -c unix_socket_directories= \ + >"$server_log" 2>&1 & + server_pid=$! + set -e + + : >"$wait_log" + status=2 + for _ in $(seq 1 300); do + if "$NATIVE_INSTALL_DIR/bin/psql" \ + "postgresql://$(id -un)@127.0.0.1:$port/postgres" \ + -X -q -c 'select 1' >>"$wait_log" 2>&1; then + status=0 + break + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + echo "native server exited before readiness" >>"$wait_log" + break + fi + sleep 0.1 + done + ended_ms="$(now_ms)" + phase_ms=$((ended_ms - started_ms)) + record_result native native start_ready "$status" "$phase_ms" "$wait_log" "port=$port" + if [ "$status" -ne 0 ]; then + stop_pid "$server_pid" + return "$status" + fi + + started_ms="$(now_ms)" + set +e + run_logged_timeout "$sql_timeout" "$sql_log" \ + "$NATIVE_INSTALL_DIR/bin/psql" \ + "postgresql://$(id -un)@127.0.0.1:$port/postgres" \ + -X -q -v "perf_rows=$bench_rows" -f "$sql_file" + status=$? + set -e + ended_ms="$(now_ms)" + phase_ms=$((ended_ms - started_ms)) + record_result native native sql_probe "$status" "$phase_ms" "$sql_log" "timeout=${sql_timeout}s" + + stop_pid "$server_pid" + return "$status" +} + +bench_wasix_profile() { + local profile="$1" + local port="$2" + local install_dir + local run_root="$matrix_run_root/$profile" + local report_root="$matrix_report_dir/$profile" + local pgdata="$run_root/pgdata" + local dev_shm="$run_root/dev-shm" + local initdb_log="$report_root/initdb.log" + local server_log="$report_root/server.log" + local wait_log="$report_root/wait.log" + local sql_log="$report_root/sql.log" + local precompile_log="$report_root/precompile.log" + local build_log="$report_root/build.log" + local backend_log="$report_root/wasmer-backend.log" + local wasmer_bin wasmer_cache_dir wasmer_compiler wasmer_llvm_opt_level wasmer_stack_size wasmer_compiler_threads + local started_ms ended_ms phase_ms status server_pid + + install_dir="$(fresh_wasix_core_install_dir_for "$profile")" + mkdir -p "$run_root" "$dev_shm" "$report_root" + rm -rf "$pgdata" "$dev_shm" + mkdir -p "$dev_shm" + + if [ "$skip_build" -ne 1 ]; then + started_ms="$(now_ms)" + set +e + WASIX_CORE_PROFILE="$profile" \ + WASIX_BUILD_DIR="$(fresh_wasix_core_build_dir_for "$profile")" \ + WASIX_INSTALL_DIR="$install_dir" \ + REPORT_DIR="$(fresh_wasix_core_report_dir_for "$profile")" \ + RUN_DIR="$(fresh_wasix_core_run_dir_for "$profile")" \ + "$FRESH_ROOT/bin/build-wasix-core.sh" >"$build_log" 2>&1 + status=$? + set -e + ended_ms="$(now_ms)" + phase_ms=$((ended_ms - started_ms)) + record_result wasix "$profile" build "$status" "$phase_ms" "$build_log" "install=$install_dir" + if [ "$status" -ne 0 ]; then + return "$status" + fi + elif [ ! -x "$install_dir/bin/postgres" ]; then + record_result wasix "$profile" build 2 0 "$build_log" "missing install with --skip-build" + return 2 + fi + + started_ms="$(now_ms)" + set +e + WASIX_CORE_PROFILE="$profile" \ + WASIX_BUILD_DIR="$(fresh_wasix_core_build_dir_for "$profile")" \ + WASIX_INSTALL_DIR="$install_dir" \ + REPORT_DIR="$(fresh_wasix_core_report_dir_for "$profile")" \ + RUN_DIR="$(fresh_wasix_core_run_dir_for "$profile")" \ + WASIX_PRECOMPILE_SCOPE=runtime \ + "$FRESH_ROOT/bin/precompile-wasix-core.sh" >"$precompile_log" 2>&1 + status=$? + set -e + ended_ms="$(now_ms)" + phase_ms=$((ended_ms - started_ms)) + record_result wasix "$profile" precompile "$status" "$phase_ms" "$precompile_log" "" + if [ "$status" -ne 0 ]; then + return "$status" + fi + + wasmer_bin="$(fresh_wasmer_bin)" + wasmer_cache_dir="$(fresh_wasmer_cache_dir "$wasmer_bin")" + wasmer_compiler="$(fresh_wasmer_compiler)" + wasmer_llvm_opt_level="${WASMER_LLVM_OPT_LEVEL:-aggressive}" + wasmer_stack_size="${WASMER_STACK_SIZE:-33554432}" + wasmer_compiler_threads="${WASMER_COMPILER_THREADS:-$(fresh_jobs)}" + if ! fresh_require_wasmer_compiler_cli "$wasmer_bin" "$wasmer_compiler" run >"$backend_log" 2>&1; then + record_result wasix "$profile" backend 2 0 "$backend_log" "compiler=$wasmer_compiler" + return 2 + fi + + wasmer_env=( + "WASMER_DIR=$FRESH_WORK_ROOT/tools/wasmer-home" + "WASMER_CACHE_DIR=$wasmer_cache_dir" + ) + wasmer_args=( + run + --quiet + ) + while IFS= read -r arg; do + wasmer_args+=("$arg") + done < <(fresh_wasmer_compiler_args_for "$wasmer_bin" run "$wasmer_compiler" "$wasmer_llvm_opt_level" "$wasmer_compiler_threads") + wasmer_args+=( + --stack-size "$wasmer_stack_size" + --enable-exceptions + --enable-threads + --net + --volume "$REPO_ROOT:$REPO_ROOT" + --volume "$install_dir/lib:/lib" + --volume "$dev_shm:/dev/shm" + ) + + started_ms="$(now_ms)" + set +e + env "${wasmer_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" "$install_dir/bin/initdb" -- \ + -D "$pgdata" \ + -A trust \ + --no-locale \ + --encoding=UTF8 \ + --no-instructions \ + >"$initdb_log" 2>&1 + status=$? + set -e + ended_ms="$(now_ms)" + phase_ms=$((ended_ms - started_ms)) + record_result wasix "$profile" initdb "$status" "$phase_ms" "$initdb_log" "" + if [ "$status" -ne 0 ]; then + return "$status" + fi + + started_ms="$(now_ms)" + set +e + env "${wasmer_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" "$install_dir/bin/postgres" -- \ + -D "$pgdata" \ + -h 127.0.0.1 \ + -p "$port" \ + -c unix_socket_directories= \ + >"$server_log" 2>&1 & + server_pid=$! + set -e + + : >"$wait_log" + status=2 + for _ in $(seq 1 300); do + if "$NATIVE_INSTALL_DIR/bin/psql" \ + "postgresql://wasix@127.0.0.1:$port/postgres" \ + -X -q -c 'select 1' >>"$wait_log" 2>&1; then + status=0 + break + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + echo "WASIX server exited before readiness" >>"$wait_log" + break + fi + sleep 0.1 + done + ended_ms="$(now_ms)" + phase_ms=$((ended_ms - started_ms)) + record_result wasix "$profile" start_ready "$status" "$phase_ms" "$wait_log" "port=$port" + if [ "$status" -ne 0 ]; then + stop_pid "$server_pid" + return "$status" + fi + + started_ms="$(now_ms)" + set +e + run_logged_timeout "$sql_timeout" "$sql_log" \ + "$NATIVE_INSTALL_DIR/bin/psql" \ + "postgresql://wasix@127.0.0.1:$port/postgres" \ + -X -q -v "perf_rows=$bench_rows" -f "$sql_file" + status=$? + set -e + ended_ms="$(now_ms)" + phase_ms=$((ended_ms - started_ms)) + record_result wasix "$profile" sql_probe "$status" "$phase_ms" "$sql_log" "timeout=${sql_timeout}s" + + stop_pid "$server_pid" + return "$status" +} + +write_header + +overall_status=0 +port="$start_port" + +if [ "$skip_native" -ne 1 ]; then + if ! bench_native "$port"; then + overall_status=1 + fi + port=$((port + 1)) +fi + +for profile in "${profiles[@]}"; do + if ! bench_wasix_profile "$profile" "$port"; then + overall_status=1 + fi + port=$((port + 1)) +done + +{ + printf '\n## Completion\n\n' + printf -- '- Exit code: `%s`\n' "$overall_status" + printf -- '- Finished: `%s`\n' "$(fresh_timestamp)" +} >>"$summary_md" + +printf 'performance matrix written to %s\n' "$summary_tsv" +exit "$overall_status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-wasix-query-suite.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-wasix-query-suite.sh new file mode 100755 index 00000000..2507e2b6 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/bench-wasix-query-suite.sh @@ -0,0 +1,833 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +profiles=() +workloads=() +postgres_gucs=() +wasmer_extra_args=() +skip_build=0 +skip_native=0 +skip_precompile=0 +precompile_scope="${WASIX_PRECOMPILE_SCOPE:-runtime}" +start_port="${PGPORT:-55520}" +bench_rows="${WASIX_BENCH_ROWS:-100000}" +update_rows="${WASIX_BENCH_UPDATE_ROWS:-$bench_rows}" +transaction_rows="${WASIX_BENCH_TRANSACTION_ROWS:-$bench_rows}" +sql_timeout="${WASIX_BENCH_SQL_TIMEOUT:-120}" +warmup_runs="${WASIX_BENCH_WARMUP_RUNS:-0}" +measure_runs="${WASIX_BENCH_MEASURE_RUNS:-1}" +workload_dir="$FRESH_ROOT/bench/sql/query-perf" +run_label="${WASIX_BENCH_LABEL:-}" + +default_workloads=( + bulk-insert + copy-out + index-build + indexed-insert + indexed-point-loop + indexed-read-hot + indexed-read + indexed-update + md5-scan + single-transaction-insert + transaction-update-batches + unlogged-bulk-insert + unlogged-constant-insert + unlogged-constant-insert-nocount + wal-insert-stats +) + +while [ "$#" -gt 0 ]; do + case "$1" in + --profile) + shift + if [ "$#" -eq 0 ]; then + echo "--profile requires a WASIX_CORE_PROFILE value" >&2 + exit 2 + fi + profiles+=("$(fresh_normalize_wasix_core_profile "$1")") + ;; + --profiles) + shift + if [ "$#" -eq 0 ]; then + echo "--profiles requires a space-separated profile list" >&2 + exit 2 + fi + for profile in $1; do + profiles+=("$(fresh_normalize_wasix_core_profile "$profile")") + done + ;; + --workload) + shift + if [ "$#" -eq 0 ]; then + echo "--workload requires a workload name or SQL path" >&2 + exit 2 + fi + workloads+=("$1") + ;; + --workloads) + shift + if [ "$#" -eq 0 ]; then + echo "--workloads requires a space-separated workload list" >&2 + exit 2 + fi + for workload in $1; do + workloads+=("$workload") + done + ;; + --skip-build) + skip_build=1 + ;; + --skip-native) + skip_native=1 + ;; + --skip-precompile) + skip_precompile=1 + ;; + --precompile-scope) + shift + if [ "$#" -eq 0 ]; then + echo "--precompile-scope requires minimal, runtime, or all" >&2 + exit 2 + fi + precompile_scope="$1" + ;; + --rows) + shift + if [ "$#" -eq 0 ]; then + echo "--rows requires a row count" >&2 + exit 2 + fi + bench_rows="$1" + ;; + --update-rows) + shift + if [ "$#" -eq 0 ]; then + echo "--update-rows requires a row count" >&2 + exit 2 + fi + update_rows="$1" + ;; + --transaction-rows) + shift + if [ "$#" -eq 0 ]; then + echo "--transaction-rows requires a row count" >&2 + exit 2 + fi + transaction_rows="$1" + ;; + --sql-timeout) + shift + if [ "$#" -eq 0 ]; then + echo "--sql-timeout requires a timeout in seconds, or 0 to disable" >&2 + exit 2 + fi + sql_timeout="$1" + ;; + --warmup-runs) + shift + if [ "$#" -eq 0 ]; then + echo "--warmup-runs requires a non-negative integer" >&2 + exit 2 + fi + case "$1" in + ''|*[!0-9]*) + echo "--warmup-runs requires a non-negative integer" >&2 + exit 2 + ;; + esac + warmup_runs="$1" + ;; + --measure-runs) + shift + if [ "$#" -eq 0 ]; then + echo "--measure-runs requires a positive integer" >&2 + exit 2 + fi + case "$1" in + ''|*[!0-9]*|0) + echo "--measure-runs requires a positive integer" >&2 + exit 2 + ;; + esac + measure_runs="$1" + ;; + --start-port) + shift + if [ "$#" -eq 0 ]; then + echo "--start-port requires a port number" >&2 + exit 2 + fi + start_port="$1" + ;; + --label) + shift + if [ "$#" -eq 0 ]; then + echo "--label requires a run label" >&2 + exit 2 + fi + case "$1" in + *[!A-Za-z0-9._-]*|"") + echo "--label may only contain letters, numbers, '.', '_', and '-'" >&2 + exit 2 + ;; + esac + run_label="$1" + ;; + --postgres-guc) + shift + if [ "$#" -eq 0 ]; then + echo "--postgres-guc requires a name=value setting" >&2 + exit 2 + fi + postgres_gucs+=("$1") + ;; + --wasmer-arg) + shift + if [ "$#" -eq 0 ]; then + echo "--wasmer-arg requires one wasmer run argument" >&2 + exit 2 + fi + wasmer_extra_args+=("$1") + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + exit 2 + ;; + esac + shift +done + +if [ "${#profiles[@]}" -eq 0 ]; then + profiles=(safe-o2) +fi +if [ "${#workloads[@]}" -eq 0 ]; then + workloads=("${default_workloads[@]}") +fi +if [ -n "${POSTGRES_GUCS:-}" ]; then + for guc in $POSTGRES_GUCS; do + postgres_gucs+=("$guc") + done +fi +if [ -n "${WASMER_RUN_EXTRA_ARGS:-}" ]; then + for arg in $WASMER_RUN_EXTRA_ARGS; do + wasmer_extra_args+=("$arg") + done +fi +case "$warmup_runs" in + ''|*[!0-9]*) + echo "WASIX_BENCH_WARMUP_RUNS/--warmup-runs requires a non-negative integer" >&2 + exit 2 + ;; +esac +case "$measure_runs" in + ''|*[!0-9]*|0) + echo "WASIX_BENCH_MEASURE_RUNS/--measure-runs requires a positive integer" >&2 + exit 2 + ;; +esac + +fresh_ensure_dirs + +query_report_dir="$FRESH_WORK_ROOT/reports/query-perf-matrix" +query_run_root="$FRESH_WORK_ROOT/run/query-perf-matrix" +if [ -n "$run_label" ]; then + query_report_dir="$query_report_dir/$run_label" + query_run_root="$query_run_root/$run_label" +fi +summary_tsv="$query_report_dir/summary.tsv" +summary_stats_tsv="$query_report_dir/summary-stats.tsv" +ratio_stats_tsv="$query_report_dir/ratio-stats.tsv" +summary_md="$query_report_dir/summary.md" +mkdir -p "$query_report_dir" "$query_run_root" + +now_ms() { + if command -v perl >/dev/null 2>&1; then + perl -MTime::HiRes=time -e 'printf "%.0f\n", time() * 1000' + else + printf '%s000\n' "$(date +%s)" + fi +} + +workload_path_for() { + local workload="$1" + case "$workload" in + */*) printf '%s\n' "$workload" ;; + *.sql) printf '%s/%s\n' "$workload_dir" "$workload" ;; + *) printf '%s/%s.sql\n' "$workload_dir" "$workload" ;; + esac +} + +workload_name_for() { + local workload="$1" + basename "${workload%.sql}" +} + +extract_psql_time_sum_ms() { + local log="$1" + awk ' + /^Time: [0-9.]+ ms/ { + sum += $2 + count += 1 + } + END { + if (count == 0) { + printf "" + } else { + printf "%.3f", sum + } + } + ' "$log" +} + +extract_psql_time_count() { + local log="$1" + awk '/^Time: [0-9.]+ ms/ { count += 1 } END { printf "%d", count }' "$log" +} + +record_result() { + local target="$1" + local profile="$2" + local workload="$3" + local status="$4" + local wall_ms="$5" + local psql_time_ms="$6" + local psql_time_count="$7" + local log="$8" + local notes="$9" + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$profile" "$workload" "$status" "$wall_ms" "$psql_time_ms" \ + "$psql_time_count" "$log" "$notes" >>"$summary_tsv" +} + +stop_pid() { + local pid="${1:-}" + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi +} + +run_logged_timeout() { + local timeout_seconds="$1" + shift + local log="$1" + shift + local pid started_ms elapsed_seconds status + + "$@" >"$log" 2>&1 & + pid=$! + started_ms="$(now_ms)" + while kill -0 "$pid" 2>/dev/null; do + if [ "$timeout_seconds" != "0" ]; then + elapsed_seconds=$((( $(now_ms) - started_ms ) / 1000)) + if [ "$elapsed_seconds" -ge "$timeout_seconds" ]; then + { + printf '\nbenchmark command timed out after %s seconds\n' "$timeout_seconds" + printf 'command:' + printf ' %q' "$@" + printf '\n' + } >>"$log" + kill "$pid" 2>/dev/null || true + sleep 1 + kill -9 "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + return 124 + fi + fi + sleep 0.1 + done + if wait "$pid"; then + return 0 + fi + status=$? + return "$status" +} + +run_psql_workload() { + local target="$1" + local profile="$2" + local workload="$3" + local conn="$4" + local report_root="$5" + local sql_path workload_name log started_ms ended_ms wall_ms status psql_time_ms psql_time_count + local warmup_index warmup_log measure_index workload_status + + sql_path="$(workload_path_for "$workload")" + workload_name="$(workload_name_for "$workload")" + log="$report_root/$workload_name.log" + if [ ! -f "$sql_path" ]; then + printf 'missing workload SQL: %s\n' "$sql_path" >"$log" + record_result "$target" "$profile" "$workload_name" 2 0 "" 0 "$log" "missing workload" + return 2 + fi + + for ((warmup_index = 1; warmup_index <= warmup_runs; warmup_index++)); do + warmup_log="$report_root/$workload_name.warmup-$warmup_index.log" + set +e + run_logged_timeout "$sql_timeout" "$warmup_log" \ + "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" \ + -X -q \ + -v "perf_rows=$bench_rows" \ + -v "update_rows=$update_rows" \ + -v "transaction_rows=$transaction_rows" \ + -f "$sql_path" + status=$? + set -e + if [ "$status" -ne 0 ]; then + record_result "$target" "$profile" "$workload_name" "$status" 0 "" 0 "$warmup_log" \ + "phase=warmup warmup_run=$warmup_index measure_runs=$measure_runs rows=$bench_rows update_rows=$update_rows transaction_rows=$transaction_rows timeout=${sql_timeout}s" + return "$status" + fi + done + + workload_status=0 + for ((measure_index = 1; measure_index <= measure_runs; measure_index++)); do + if [ "$measure_runs" -eq 1 ]; then + log="$report_root/$workload_name.log" + else + log="$report_root/$workload_name.run-$measure_index.log" + fi + started_ms="$(now_ms)" + set +e + run_logged_timeout "$sql_timeout" "$log" \ + "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" \ + -X -q \ + -v "perf_rows=$bench_rows" \ + -v "update_rows=$update_rows" \ + -v "transaction_rows=$transaction_rows" \ + -f "$sql_path" + status=$? + set -e + ended_ms="$(now_ms)" + wall_ms=$((ended_ms - started_ms)) + psql_time_ms="$(extract_psql_time_sum_ms "$log")" + psql_time_count="$(extract_psql_time_count "$log")" + record_result "$target" "$profile" "$workload_name" "$status" "$wall_ms" \ + "$psql_time_ms" "$psql_time_count" "$log" \ + "phase=measure measure_run=$measure_index measure_runs=$measure_runs rows=$bench_rows update_rows=$update_rows transaction_rows=$transaction_rows warmup_runs=$warmup_runs timeout=${sql_timeout}s" + if [ "$status" -ne 0 ]; then + workload_status="$status" + fi + done + return "$workload_status" +} + +write_stats() { + perl -MList::Util=sum,min,max -e ' + use strict; + use warnings; + my ($summary, $stats, $ratios) = @ARGV; + open my $in, "<", $summary or die "open $summary: $!"; + <$in>; + my (%values, %samples, %statuses); + while (my $line = <$in>) { + chomp $line; + my @f = split /\t/, $line, 9; + next unless @f >= 9; + my ($target, $profile, $workload, $status, $wall_ms, $psql_ms) = @f; + my $key = join "\t", $target, $profile, $workload; + $samples{$key}++; + $statuses{$key}{$status}++; + push @{ $values{$key} }, 0 + $psql_ms if $status == 0 && $psql_ms ne ""; + } + close $in; + + open my $stats_out, ">", $stats or die "open $stats: $!"; + print $stats_out join("\t", qw(target profile workload samples ok_samples statuses min_ms median_ms mean_ms p95_ms max_ms)), "\n"; + my (%median_by, %targets_by_workload); + for my $key (sort keys %samples) { + my ($target, $profile, $workload) = split /\t/, $key; + my @v = sort { $a <=> $b } @{ $values{$key} || [] }; + my $n = @v; + my $status_text = join ",", map { "$_:$statuses{$key}{$_}" } sort { $a <=> $b } keys %{ $statuses{$key} }; + my ($min, $median, $mean, $p95, $max) = ("", "", "", "", ""); + if ($n) { + $min = min(@v); + $max = max(@v); + $mean = sum(@v) / $n; + $median = $n % 2 ? $v[int($n / 2)] : ($v[$n / 2 - 1] + $v[$n / 2]) / 2; + my $p95_index = int(0.95 * $n + 0.999999) - 1; + $p95_index = 0 if $p95_index < 0; + $p95_index = $n - 1 if $p95_index >= $n; + $p95 = $v[$p95_index]; + $median_by{$target}{$profile}{$workload} = $median; + $targets_by_workload{$workload}{$target}{$profile} = 1; + } + printf $stats_out "%s\t%s\t%s\t%d\t%d\t%s\t%s\t%s\t%s\t%s\t%s\n", + $target, $profile, $workload, $samples{$key}, $n, $status_text, + map { $_ eq "" ? "" : sprintf("%.3f", $_) } ($min, $median, $mean, $p95, $max); + } + close $stats_out; + + open my $ratio_out, ">", $ratios or die "open $ratios: $!"; + print $ratio_out join("\t", qw(profile workload native_median_ms wasix_median_ms ratio_vs_native)), "\n"; + for my $workload (sort keys %targets_by_workload) { + my $native = $median_by{native}{native}{$workload}; + next unless defined $native && $native > 0; + for my $profile (sort keys %{ $targets_by_workload{$workload}{wasix} || {} }) { + my $wasix = $median_by{wasix}{$profile}{$workload}; + next unless defined $wasix; + printf $ratio_out "%s\t%s\t%.3f\t%.3f\t%.3f\n", $profile, $workload, $native, $wasix, $wasix / $native; + } + } + close $ratio_out; + ' "$summary_tsv" "$summary_stats_tsv" "$ratio_stats_tsv" +} + +write_header() { + local header_wasmer_bin="" header_wasmer_bin_hash="" header_wasmer_version="" header_wasmer_cache_dir="" + local header_wasmer_compiler header_wasmer_llvm_opt_level header_wasmer_stack_size + local header_wasmer_compiler_threads header_wasmer_artifact_version + + header_wasmer_compiler="$(fresh_wasmer_compiler)" + header_wasmer_llvm_opt_level="${WASMER_LLVM_OPT_LEVEL:-aggressive}" + header_wasmer_stack_size="${WASMER_STACK_SIZE:-33554432}" + header_wasmer_compiler_threads="${WASMER_COMPILER_THREADS:-$(fresh_jobs)}" + header_wasmer_artifact_version="${WASMER_CACHE_ARTIFACT_VERSION:-21}" + if header_wasmer_bin="$(fresh_wasmer_bin 2>/dev/null)"; then + header_wasmer_bin_hash="$(fresh_wasmer_bin_hash "$header_wasmer_bin")" + header_wasmer_version="$(fresh_wasmer_version "$header_wasmer_bin" 2>/dev/null || true)" + header_wasmer_cache_dir="$(fresh_wasmer_cache_dir "$header_wasmer_bin")" + fi + + { + printf '# WASIX Query Performance Matrix\n\n' + printf -- '- Generated: `%s`\n' "$(fresh_timestamp)" + printf -- '- Profiles: `%s`\n' "${profiles[*]}" + printf -- '- Workloads: `%s`\n' "${workloads[*]}" + printf -- '- Rows: `%s`\n' "$bench_rows" + printf -- '- Update rows: `%s`\n' "$update_rows" + printf -- '- Transaction rows: `%s`\n' "$transaction_rows" + printf -- '- SQL timeout: `%s seconds`\n' "$sql_timeout" + printf -- '- Warmup runs: `%s`\n' "$warmup_runs" + printf -- '- Measure runs: `%s`\n' "$measure_runs" + printf -- '- Label: `%s`\n' "${run_label:-default}" + printf -- '- PostgreSQL GUCs: `%s`\n' "${postgres_gucs[*]:-}" + printf -- '- Extra Wasmer args: `%s`\n' "${wasmer_extra_args[*]:-}" + printf -- '- Wasmer binary: `%s`\n' "${header_wasmer_bin:-unresolved}" + printf -- '- Wasmer binary hash: `%s`\n' "${header_wasmer_bin_hash:-unresolved}" + printf -- '- Wasmer version: `%s`\n' "${header_wasmer_version:-unresolved}" + printf -- '- Wasmer compiler: `%s`\n' "$header_wasmer_compiler" + printf -- '- Wasmer LLVM opt level: `%s`\n' "$header_wasmer_llvm_opt_level" + printf -- '- WASMER_LLVM_NATIVE_CPU: `%s`\n' "${WASMER_LLVM_NATIVE_CPU:-0}" + printf -- '- WASMER_LLVM_FULL_O3_PIPELINE: `%s`\n' "${WASMER_LLVM_FULL_O3_PIPELINE:-0}" + printf -- '- WASMER_LLVM_INDIRECT_CALL_CACHE: `%s`\n' "${WASMER_LLVM_INDIRECT_CALL_CACHE:-0}" + printf -- '- Wasmer cache dir: `%s`\n' "${header_wasmer_cache_dir:-unresolved}" + printf -- '- Pinned runtime: `%s`\n' "${FRESH_PINNED_RUNTIME_NAME:-}" + printf -- '- Pinned runtime root: `%s`\n' "${FRESH_PINNED_RUNTIME_ROOT:-}" + printf -- '- Pinned WASIX install dir: `%s`\n' "${FRESH_PINNED_WASIX_INSTALL_DIR:-}" + printf -- '- Pinned Wasmer cache dir: `%s`\n' "${FRESH_PINNED_WASMER_CACHE_DIR:-}" + printf -- '- Wasmer cache artifact version: `%s`\n' "$header_wasmer_artifact_version" + printf -- '- Wasmer compiler threads: `%s`\n' "$header_wasmer_compiler_threads" + printf -- '- Wasmer stack size: `%s`\n' "$header_wasmer_stack_size" + printf -- '- Precompile scope: `%s`\n' "$precompile_scope" + printf -- '- Skip precompile: `%s`\n' "$skip_precompile" + printf -- '- Summary TSV: `%s`\n\n' "$summary_tsv" + printf -- '- Summary stats TSV: `%s`\n' "$summary_stats_tsv" + printf -- '- Ratio stats TSV: `%s`\n\n' "$ratio_stats_tsv" + printf '## WASIX Profile Settings\n\n' + for profile in "${profiles[@]}"; do + fresh_resolve_wasix_core_profile "$profile" + printf -- '- `%s`: %s\n' "$profile" "$FRESH_WASIX_CORE_PROFILE_DESCRIPTION" + printf ' - CFLAGS: `%s`\n' "$FRESH_WASIX_CORE_EFFECTIVE_CFLAGS" + printf ' - LDFLAGS: `%s`\n' "$FRESH_WASIX_CORE_EFFECTIVE_LDFLAGS" + printf ' - wasm-opt: `%s`\n' "$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT" + printf ' - wasm-opt flags: `%s`\n' "$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT_FLAGS" + printf ' - suppress implicit wasm-opt defaults: `%s`\n' "$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT_SUPPRESS_DEFAULT" + done + printf '\n' + printf '## Measurement\n\n' + printf 'Each SQL workload keeps setup outside `psql` timing and enables timing only for the measured operation(s). Optional warmup runs execute the same workload before measurement and are logged separately without contributing to aggregate stats. Repeated measured runs are recorded in the raw TSV and summarized by median/mean/p95 in the stats TSVs.\n' + } >"$summary_md" + + printf 'target\tprofile\tworkload\tstatus\twall_ms\tpsql_time_ms\tpsql_time_count\tlog\tnotes\n' >"$summary_tsv" +} + +bench_native() { + local port="$1" + local run_root="$query_run_root/native" + local report_root="$query_report_dir/native" + local pgdata="$run_root/pgdata" + local initdb_log="$report_root/initdb.log" + local server_log="$report_root/server.log" + local wait_log="$report_root/wait.log" + local server_pid="" conn status started_ms ended_ms + local postgres_args + + mkdir -p "$run_root" "$report_root" + rm -rf "$pgdata" + + if [ "$skip_build" -ne 1 ]; then + "$FRESH_ROOT/bin/build-native-oracle.sh" >/dev/null + fi + + started_ms="$(now_ms)" + set +e + "$NATIVE_INSTALL_DIR/bin/initdb" \ + -D "$pgdata" \ + -A trust \ + --no-locale \ + --encoding=UTF8 \ + --no-instructions \ + >"$initdb_log" 2>&1 + status=$? + set -e + ended_ms="$(now_ms)" + if [ "$status" -ne 0 ]; then + record_result native native initdb "$status" "$((ended_ms - started_ms))" "" 0 "$initdb_log" "pgdata=$pgdata" + return "$status" + fi + + set +e + postgres_args=( + -D "$pgdata" + -h 127.0.0.1 + -p "$port" + -c unix_socket_directories= + ) + if [ "${#postgres_gucs[@]}" -gt 0 ]; then + for guc in "${postgres_gucs[@]}"; do + postgres_args+=(-c "$guc") + done + fi + "$NATIVE_INSTALL_DIR/bin/postgres" "${postgres_args[@]}" >"$server_log" 2>&1 & + server_pid=$! + set -e + + : >"$wait_log" + status=2 + conn="postgresql://$(id -un)@127.0.0.1:$port/postgres" + for _ in $(seq 1 300); do + if "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -X -q -c 'select 1' >>"$wait_log" 2>&1; then + status=0 + break + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + echo "native server exited before readiness" >>"$wait_log" + break + fi + sleep 0.1 + done + if [ "$status" -ne 0 ]; then + record_result native native readiness "$status" 0 "" 0 "$wait_log" "port=$port" + stop_pid "$server_pid" + return "$status" + fi + + for workload in "${workloads[@]}"; do + if ! run_psql_workload native native "$workload" "$conn" "$report_root"; then + status=1 + fi + done + + stop_pid "$server_pid" + return "$status" +} + +bench_wasix_profile() { + local profile="$1" + local port="$2" + local install_dir run_root report_root pgdata dev_shm initdb_log server_log wait_log backend_log + local wasmer_bin wasmer_cache_dir wasmer_compiler wasmer_llvm_opt_level wasmer_stack_size wasmer_compiler_threads + local pinned_profile + local server_pid="" conn status started_ms ended_ms + local postgres_args + + install_dir="$(fresh_wasix_core_install_dir_for "$profile")" + if [ -n "${FRESH_PINNED_WASIX_INSTALL_DIR:-}" ]; then + pinned_profile="$(fresh_normalize_wasix_core_profile "${FRESH_PINNED_WASIX_CORE_PROFILE:-$profile}")" + if [ "$profile" != "$pinned_profile" ]; then + report_root="$query_report_dir/$profile" + mkdir -p "$report_root" + record_result wasix "$profile" build 2 0 "" 0 "$report_root/build.log" \ + "pinned install profile=$pinned_profile cannot satisfy requested profile=$profile" + return 2 + fi + install_dir="$FRESH_PINNED_WASIX_INSTALL_DIR" + fi + run_root="$query_run_root/$profile" + report_root="$query_report_dir/$profile" + pgdata="$run_root/pgdata" + dev_shm="$run_root/dev-shm" + initdb_log="$report_root/initdb.log" + server_log="$report_root/server.log" + wait_log="$report_root/wait.log" + backend_log="$report_root/wasmer-backend.log" + + mkdir -p "$run_root" "$report_root" + rm -rf "$pgdata" "$dev_shm" + mkdir -p "$dev_shm" + + if [ -n "${FRESH_PINNED_WASIX_INSTALL_DIR:-}" ] && [ "$skip_build" -ne 1 ]; then + printf 'pinned runtime %s requires --skip-build\n' "${FRESH_PINNED_RUNTIME_NAME:-$FRESH_PINNED_WASIX_INSTALL_DIR}" >"$report_root/build.log" + record_result wasix "$profile" build 2 0 "" 0 "$report_root/build.log" \ + "pinned runtime requires --skip-build" + return 2 + fi + if [ -n "${FRESH_PINNED_WASMER_CACHE_DIR:-}" ] && [ "$skip_precompile" -ne 1 ]; then + printf 'pinned runtime %s requires --skip-precompile\n' "${FRESH_PINNED_RUNTIME_NAME:-$FRESH_PINNED_WASMER_CACHE_DIR}" >"$report_root/precompile.log" + record_result wasix "$profile" precompile 2 0 "" 0 "$report_root/precompile.log" \ + "pinned runtime requires --skip-precompile" + return 2 + fi + + if [ "$skip_build" -ne 1 ]; then + WASIX_CORE_PROFILE="$profile" \ + WASIX_BUILD_DIR="$(fresh_wasix_core_build_dir_for "$profile")" \ + WASIX_INSTALL_DIR="$install_dir" \ + REPORT_DIR="$(fresh_wasix_core_report_dir_for "$profile")" \ + RUN_DIR="$(fresh_wasix_core_run_dir_for "$profile")" \ + "$FRESH_ROOT/bin/build-wasix-core.sh" >/dev/null + elif [ ! -x "$install_dir/bin/postgres" ]; then + record_result wasix "$profile" build 2 0 "" 0 "$report_root/build.log" "missing install with --skip-build" + return 2 + fi + + if [ "$skip_precompile" -ne 1 ]; then + WASIX_CORE_PROFILE="$profile" \ + WASIX_BUILD_DIR="$(fresh_wasix_core_build_dir_for "$profile")" \ + WASIX_INSTALL_DIR="$install_dir" \ + REPORT_DIR="$(fresh_wasix_core_report_dir_for "$profile")" \ + RUN_DIR="$(fresh_wasix_core_run_dir_for "$profile")" \ + WASIX_PRECOMPILE_SCOPE="$precompile_scope" \ + "$FRESH_ROOT/bin/precompile-wasix-core.sh" >"$report_root/precompile.log" 2>&1 + else + printf 'precompile skipped\n' >"$report_root/precompile.log" + fi + + wasmer_bin="$(fresh_wasmer_bin)" + wasmer_cache_dir="$(fresh_wasmer_cache_dir "$wasmer_bin")" + wasmer_compiler="$(fresh_wasmer_compiler)" + wasmer_llvm_opt_level="${WASMER_LLVM_OPT_LEVEL:-aggressive}" + wasmer_stack_size="${WASMER_STACK_SIZE:-33554432}" + wasmer_compiler_threads="${WASMER_COMPILER_THREADS:-$(fresh_jobs)}" + if ! fresh_require_wasmer_compiler_cli "$wasmer_bin" "$wasmer_compiler" run >"$backend_log" 2>&1; then + record_result wasix "$profile" backend 2 0 "" 0 "$backend_log" "compiler=$wasmer_compiler" + return 2 + fi + + wasmer_env=( + "WASMER_DIR=$FRESH_WORK_ROOT/tools/wasmer-home" + "WASMER_CACHE_DIR=$wasmer_cache_dir" + ) + wasmer_args=( + run + --quiet + ) + while IFS= read -r arg; do + wasmer_args+=("$arg") + done < <(fresh_wasmer_compiler_args_for "$wasmer_bin" run "$wasmer_compiler" "$wasmer_llvm_opt_level" "$wasmer_compiler_threads") + wasmer_args+=( + --stack-size "$wasmer_stack_size" + --enable-exceptions + --enable-threads + --net + --volume "$REPO_ROOT:$REPO_ROOT" + --volume "$install_dir/lib:/lib" + --volume "$dev_shm:/dev/shm" + ) + if [ "${#wasmer_extra_args[@]}" -gt 0 ]; then + wasmer_args+=("${wasmer_extra_args[@]}") + fi + + started_ms="$(now_ms)" + set +e + env "${wasmer_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" "$install_dir/bin/initdb" -- \ + -D "$pgdata" \ + -A trust \ + --no-locale \ + --encoding=UTF8 \ + --no-instructions \ + >"$initdb_log" 2>&1 + status=$? + set -e + ended_ms="$(now_ms)" + if [ "$status" -ne 0 ]; then + record_result wasix "$profile" initdb "$status" "$((ended_ms - started_ms))" "" 0 "$initdb_log" "pgdata=$pgdata" + return "$status" + fi + + set +e + postgres_args=( + -D "$pgdata" + -h 127.0.0.1 + -p "$port" + -c unix_socket_directories= + ) + if [ "${#postgres_gucs[@]}" -gt 0 ]; then + for guc in "${postgres_gucs[@]}"; do + postgres_args+=(-c "$guc") + done + fi + env "${wasmer_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" "$install_dir/bin/postgres" -- \ + "${postgres_args[@]}" >"$server_log" 2>&1 & + server_pid=$! + set -e + + : >"$wait_log" + status=2 + conn="postgresql://wasix@127.0.0.1:$port/postgres" + for _ in $(seq 1 300); do + if "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -X -q -c 'select 1' >>"$wait_log" 2>&1; then + status=0 + break + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + echo "WASIX server exited before readiness" >>"$wait_log" + break + fi + sleep 0.1 + done + if [ "$status" -ne 0 ]; then + record_result wasix "$profile" readiness "$status" 0 "" 0 "$wait_log" "port=$port" + stop_pid "$server_pid" + return "$status" + fi + + for workload in "${workloads[@]}"; do + if ! run_psql_workload wasix "$profile" "$workload" "$conn" "$report_root"; then + status=1 + fi + done + + stop_pid "$server_pid" + return "$status" +} + +write_header + +overall_status=0 +port="$start_port" + +if [ "$skip_native" -ne 1 ]; then + if ! bench_native "$port"; then + overall_status=1 + fi + port=$((port + 1)) +fi + +for profile in "${profiles[@]}"; do + if ! bench_wasix_profile "$profile" "$port"; then + overall_status=1 + fi + port=$((port + 1)) +done + +write_stats + +{ + printf '\n## Completion\n\n' + printf -- '- Exit code: `%s`\n' "$overall_status" + printf -- '- Finished: `%s`\n' "$(fresh_timestamp)" +} >>"$summary_md" + +printf 'query performance matrix written to %s\n' "$summary_tsv" +exit "$overall_status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-native-oracle.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-native-oracle.sh new file mode 100755 index 00000000..777b12ff --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-native-oracle.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +lock_dir="$FRESH_WORK_ROOT/.native-build.lock" +managed_work_probe="$FRESH_WORK_ROOT/.managed-path-boundary" +fresh_require_managed_generated_path "$managed_work_probe" FRESH_WORK_ROOT +fresh_require_managed_generated_path "$NATIVE_BUILD_DIR" NATIVE_BUILD_DIR +fresh_require_managed_generated_path "$NATIVE_INSTALL_DIR" NATIVE_INSTALL_DIR +fresh_require_managed_generated_path "$REPORT_DIR" REPORT_DIR +fresh_require_managed_generated_path "$RUN_DIR" RUN_DIR +fresh_require_managed_generated_path "$lock_dir" native-build-lock + +fresh_ensure_dirs +fresh_require_command git +fresh_require_command make + +lock_waits=0 +until mkdir "$lock_dir" 2>/dev/null; do + lock_waits=$((lock_waits + 1)) + if [ "$lock_waits" -gt 900 ]; then + echo "timed out waiting for native build lock: $lock_dir" >&2 + exit 2 + fi + sleep 0.2 +done +trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT + +jobs="${JOBS:-$(fresh_jobs)}" + +"$FRESH_ROOT/bin/prepare-baseline.sh" >/dev/null + +baseline_head="$(git -C "$BASELINE_DIR" rev-parse HEAD)" + +report="$REPORT_DIR/native-build.md" +log="$REPORT_DIR/native-build.log" +fresh_require_managed_generated_path "$report" native-build-report +fresh_require_managed_generated_path "$log" native-build-log +fresh_write_report_header "$report" "Native PostgreSQL Oracle Build" + +configure_args=( + "--prefix=$NATIVE_INSTALL_DIR" + "--without-readline" + "--without-icu" + "--without-zlib" + "--without-llvm" + "--without-pam" + "--with-openssl=no" +) + +source_signature="$( + { + printf 'build_signature_version=2\n' + printf 'baseline=%s\n' "$baseline_head" + printf 'postgres_tag=%s\n' "$POSTGRES_TAG" + printf '%s\n' "${configure_args[@]}" + } | shasum -a 256 | awk '{print $1}' +)" +build_signature_file="$NATIVE_BUILD_DIR/.fresh-native-oracle-build-signature" +fresh_require_managed_generated_path "$build_signature_file" native-build-signature +if [ -f "$build_signature_file" ] && [ "$(cat "$build_signature_file")" = "$source_signature" ]; then + mkdir -p "$NATIVE_BUILD_DIR" "$NATIVE_INSTALL_DIR" +else + fresh_require_managed_generated_path "$NATIVE_BUILD_DIR" NATIVE_BUILD_DIR + fresh_require_managed_generated_path "$NATIVE_INSTALL_DIR" NATIVE_INSTALL_DIR + rm -rf "$NATIVE_BUILD_DIR" "$NATIVE_INSTALL_DIR" + mkdir -p "$NATIVE_BUILD_DIR" "$NATIVE_INSTALL_DIR" + printf '%s' "$source_signature" >"$build_signature_file" +fi + +{ + printf '## Configure\n\n' + printf '```text\n' + printf '%q ' "$BASELINE_DIR/configure" "${configure_args[@]}" + printf '\n```\n\n' + printf '## Source\n\n' + printf -- '- Baseline commit: `%s`\n' "$baseline_head" + printf -- '- Build signature: `%s`\n\n' "$source_signature" + printf '## Build Log\n\n' + printf 'See `%s`.\n' "$log" +} >>"$report" + +fresh_require_managed_generated_path "$NATIVE_BUILD_DIR" NATIVE_BUILD_DIR +fresh_require_managed_generated_path "$NATIVE_INSTALL_DIR" NATIVE_INSTALL_DIR +( + set -euo pipefail + cd "$NATIVE_BUILD_DIR" + if [ ! -f config.status ]; then + "$BASELINE_DIR/configure" "${configure_args[@]}" + fi + make -j "$jobs" + make install +) >"$log" 2>&1 + +{ + printf '\n## Result\n\n' + printf -- '- Status: `pass`\n' + printf -- '- Install directory: `%s`\n' "$NATIVE_INSTALL_DIR" +} >>"$report" + +printf 'built native PostgreSQL oracle at %s\n' "$NATIVE_INSTALL_DIR" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-sealed-headless-carrier.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-sealed-headless-carrier.sh new file mode 100755 index 00000000..a0839456 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-sealed-headless-carrier.sh @@ -0,0 +1,1508 @@ +#!/usr/bin/env bash + +set -euo pipefail + +wasix_core_profile_explicit=0 +if [[ -v WASIX_CORE_PROFILE ]] && [ -n "$WASIX_CORE_PROFILE" ]; then + wasix_core_profile_explicit=1 +fi +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" +source "$FRESH_ROOT/lib/sealed-carrier.sh" + +# These values are a closed compile-time contract with the product executor's +# sealed manifest parser. There is deliberately no environment override. +readonly SEALED_FILE_CACHE_REQUESTED_POLICY_ID="oliphaunt.wasix-postmaster.file-cache.adaptive-linux.v5" +readonly SEALED_FILE_CACHE_APPROVED_CONFIG_ID="oliphaunt.wasix-postmaster.file-cache.adaptive-linux.embedded-v4" +readonly SEALED_FILE_CACHE_CONFIG_SHA256="01668b856435cb8c34b2d2324ab55b7f1f5961b8b403c1ee49d9ee4b5c865f53" +readonly SEALED_FILE_CACHE_PORTABLE_FALLBACK_MODE="observe-only" + +usage() { + cat <<'EOF' +Usage: build-sealed-headless-carrier.sh [options] + +Build an atomic, compiler-free WASIX PostgreSQL carrier from an already +validated runtime receipt and precompiled AOT cache. + +Options: + --output DIR Final carrier directory (must not already exist). + By default, publish below the work-root carriers + directory under the exact payload-inventory digest. + --install-dir DIR WASIX PostgreSQL prefix (default: WASIX_INSTALL_DIR) + --postmaster-compiler FILE + Receipt-bound bounded-memory LLVM producer + --executor-role ROLE postmaster-product (default) or full-headless + --postmaster-executor FILE + Product-specific sealed-postmaster executor + --postmaster-executor-receipt FILE + Exact product executor build receipt + --start-proof-tool FILE Receipt-bound deterministic-start analyzer + --headless-wasmer FILE Full Wasmer headless control executor + --cache-bucket DIR Exact precompiled AOT bucket + --receipt FILE Canonical Wasmer build receipt + -h, --help Show this help + +The builder never compiles implicitly and never accepts host-native CPU AOT. +EOF +} + +fail() { + printf 'sealed carrier build: %s\n' "$*" >&2 + exit 2 +} + +output="" +install_dir="$WASIX_INSTALL_DIR" +postmaster_compiler="$FRESH_POSTMASTER_COMPILER_BIN" +headless_wasmer="$FRESH_UPSTREAM_WASMER_HEADLESS_BIN" +postmaster_executor="$FRESH_POSTMASTER_EXECUTOR_BIN" +postmaster_executor_receipt="$FRESH_POSTMASTER_EXECUTOR_BUILD_RECEIPT" +start_proof_tool="$FRESH_START_PROOF_BIN" +executor_role="$FRESH_POSTMASTER_EXECUTOR_ROLE" +executor_role_explicit=0 +headless_wasmer_explicit=0 +postmaster_executor_explicit=0 +postmaster_executor_receipt_explicit=0 +receipt="${WASMER_BUILD_RECEIPT:-$FRESH_WASMER_BUILD_RECEIPT}" +cache_bucket="" + +while [ "$#" -gt 0 ]; do + case "$1" in + --output|--install-dir|--postmaster-compiler|--executor-role|--postmaster-executor|--postmaster-executor-receipt|--start-proof-tool|--headless-wasmer|--cache-bucket|--receipt) + option="$1" + shift + [ "$#" -gt 0 ] || fail "$option requires a value" + case "$option" in + --output) output="$1" ;; + --install-dir) install_dir="$1" ;; + --postmaster-compiler) postmaster_compiler="$1" ;; + --executor-role) executor_role="$1"; executor_role_explicit=1 ;; + --postmaster-executor) postmaster_executor="$1"; postmaster_executor_explicit=1 ;; + --postmaster-executor-receipt) postmaster_executor_receipt="$1"; postmaster_executor_receipt_explicit=1 ;; + --start-proof-tool) start_proof_tool="$1" ;; + --headless-wasmer) headless_wasmer="$1"; headless_wasmer_explicit=1 ;; + --cache-bucket) cache_bucket="$1" ;; + --receipt) receipt="$1" ;; + esac + ;; + -h|--help) + usage + exit 0 + ;; + *) + fail "unknown argument: $1" + ;; + esac + shift +done + +if [ "$executor_role_explicit" -eq 0 ]; then + if [ "$headless_wasmer_explicit" -eq 1 ] && \ + { [ "$postmaster_executor_explicit" -eq 1 ] || \ + [ "$postmaster_executor_receipt_explicit" -eq 1 ]; } + then + fail 'executor overrides for both roles require an explicit --executor-role' + elif [ "$headless_wasmer_explicit" -eq 1 ]; then + # Preserve the historical meaning of the existing override while making + # the product executor the default for ordinary carrier construction. + executor_role=full-headless + fi +fi +case "$executor_role" in + postmaster-product) + [ "$headless_wasmer_explicit" -eq 0 ] || { + fail '--headless-wasmer is only valid with --executor-role full-headless' + } + selected_executor="$postmaster_executor" + ;; + full-headless) + { [ "$postmaster_executor_explicit" -eq 0 ] && \ + [ "$postmaster_executor_receipt_explicit" -eq 0 ]; } || { + fail 'postmaster executor overrides require --executor-role postmaster-product' + } + selected_executor="$headless_wasmer" + ;; + *) + fail "unknown executor role: $executor_role" + ;; +esac + +# Memory-image v2 proof generation is a carrier-build concern independent of +# which runtime executable will later validate the sealed carrier. +fresh_require_start_proof_tool "$start_proof_tool" "$postmaster_executor_receipt" + +fresh_require_command python3 +fresh_require_command cp +fresh_require_command cmp +fresh_require_command find +fresh_require_command flock +fresh_require_command sort + +[ "$wasix_core_profile_explicit" -eq 1 ] || { + fail 'WASIX_CORE_PROFILE must be explicit for a sealed qualification carrier' +} +core_profile="$(fresh_normalize_wasix_core_profile "$WASIX_CORE_PROFILE")" || exit +case "$core_profile" in + release-o3) ;; + *) + fail "sealed qualification carriers require a release-o3 guest with a qualified final fence inventory, got: $core_profile" + ;; +esac + +fresh_require_patched_postmaster_compiler \ + "$postmaster_compiler" \ + "$postmaster_executor_receipt" \ + "$receipt" \ + "$postmaster_executor" +case "$executor_role" in + postmaster-product) + fresh_require_patched_postmaster_executor \ + "$selected_executor" "$postmaster_executor_receipt" "$receipt" + ;; + full-headless) + WASMER_BUILD_RECEIPT="$receipt" \ + fresh_require_patched_wasmer_headless "$selected_executor" + ;; +esac + +runtime_abi_id="$(fresh_manifest_value "$receipt" runtime_abi_id)" +output_is_explicit=1 +if [ -n "$output" ]; then + case "$output" in + */.|*/..|.|..|/) fail "unsafe output directory: $output" ;; + esac + output_parent_input="$(dirname "$output")" + output_name="$(basename "$output")" + [ -n "$output_name" ] || fail "output directory has no basename: $output" + case "$output_name" in + *$'\n'*|*$'\r'*|*$'\t'*) fail "output directory basename contains a control delimiter" ;; + esac +else + # The complete payload identity is unavailable until manifest.json and the + # exact inventory have been generated. Keep unpublished construction in a + # generic, private staging name and resolve the public path immediately + # before the atomic rename. This prevents two PostgreSQL build profiles + # with the same runtime ABI from colliding at the old default path. + output_is_explicit=0 + output_parent_input="$FRESH_WORK_ROOT/carriers" + output_name="wasix-postmaster-$POSTGRES_VERSION-${runtime_abi_id:0:16}-unpublished" +fi +mkdir -p "$output_parent_input" +output_parent="$(cd "$output_parent_input" && pwd -P)" +if [ "$output_is_explicit" -eq 1 ]; then + output="$output_parent/$output_name" + [ ! -e "$output" ] && [ ! -L "$output" ] || fail "output already exists: $output" +fi + +[ -d "$install_dir" ] && [ ! -L "$install_dir" ] || fail "missing regular WASIX install prefix: $install_dir" +install_dir="$(cd "$install_dir" && pwd -P)" +guest_build_receipt_source="$install_dir/guest-build.receipt" +[ -f "$guest_build_receipt_source" ] && [ ! -L "$guest_build_receipt_source" ] || { + fail "missing regular guest build receipt: $guest_build_receipt_source" +} +python3 - "$guest_build_receipt_source" "$core_profile" "$POSTGRES_TAG" \ + "$POSTGRES_VERSION" "$WASIXCC_SYSROOT_VARIANT" <<'PY' +import re +import sys + +path, expected_profile, postgres_tag, postgres_version, sysroot_variant = sys.argv[1:] +keys = ( + "schema", + "core_profile", + "guest_source_signature_sha256", + "installed_closure_sha256", + "child_backend", + "effective_cflags", + "effective_ldflags", + "effective_wasm_opt", + "effective_wasm_opt_flags", + "effective_wasm_opt_suppress_default", + "atomic_fence_total", + "atomic_fence_set_latch", + "atomic_fence_reset_latch", + "atomic_fence_wait_event_set_wait", + "latch_state_contract", + "final_wasm_concurrency_receipt_sha256", + "linear_memory_profile_id", + "linear_memory_install_receipt_sha256", + "postgres_tag", + "postgres_version", + "sysroot_variant", +) +with open(path, encoding="utf-8", newline="") as stream: + text = stream.read() +if not text.endswith("\n") or "\r" in text: + raise SystemExit("guest build receipt is not canonical newline text") +lines = text.splitlines() +if len(lines) != len(keys): + raise SystemExit("guest build receipt field count differs") +values = {} +for expected, line in zip(keys, lines, strict=True): + if "=" not in line: + raise SystemExit(f"guest build receipt field has no separator: {expected}") + key, value = line.split("=", 1) + if key != expected or not value: + raise SystemExit(f"guest build receipt field differs: {expected}") + values[key] = value +if values["schema"] != "oliphaunt.wasix-postmaster.guest-build.v4": + raise SystemExit("guest build receipt schema differs") +if values["core_profile"] != expected_profile: + raise SystemExit("guest build receipt profile differs from explicit carrier profile") +if re.fullmatch(r"[0-9a-f]{64}", values["guest_source_signature_sha256"]) is None: + raise SystemExit("guest build source signature is not a SHA-256") +if re.fullmatch(r"[0-9a-f]{64}", values["installed_closure_sha256"]) is None: + raise SystemExit("guest build installed closure identity is not a SHA-256") +if values["child_backend"] != "exec": + raise SystemExit("sealed postmaster carrier requires the exec child backend") +if values["effective_wasm_opt"] not in {"yes", "no"}: + raise SystemExit("guest build receipt wasm-opt mode differs") +if values["effective_wasm_opt_suppress_default"] != "yes": + raise SystemExit("guest build receipt must suppress implicit wasm-opt defaults") +expected_fences = { + "atomic_fence_set_latch": "2", + "atomic_fence_reset_latch": "1", + "atomic_fence_wait_event_set_wait": "1", +} +expected_fences["atomic_fence_total"] = { + "release-o3": "995", +}[expected_profile] +for key, expected in expected_fences.items(): + if values[key] != expected: + raise SystemExit(f"guest build receipt concurrency fence contract differs: {key}") +if values["latch_state_contract"] != "packed-atomic-v1": + raise SystemExit("guest build receipt latch-state contract differs") +if re.fullmatch( + r"[0-9a-f]{64}", values["final_wasm_concurrency_receipt_sha256"] +) is None: + raise SystemExit("guest build final Wasm concurrency receipt identity differs") +if values["linear_memory_profile_id"] != "oliphaunt.wasix-postmaster.linear-memory.wasm32-max256m-u64-static4g-guard2g.v1": + raise SystemExit("guest build linear-memory profile differs") +if re.fullmatch( + r"[0-9a-f]{64}", values["linear_memory_install_receipt_sha256"] +) is None: + raise SystemExit("guest build linear-memory install receipt identity differs") +if values["postgres_tag"] != postgres_tag or values["postgres_version"] != postgres_version: + raise SystemExit("guest build receipt PostgreSQL version differs") +if values["sysroot_variant"] != sysroot_variant: + raise SystemExit("guest build receipt sysroot variant differs") +PY +final_wasm_concurrency_receipt_source="$install_dir/share/postgresql/wasix-postmaster.final-wasm-concurrency.receipt" +[ -f "$final_wasm_concurrency_receipt_source" ] && \ + [ ! -L "$final_wasm_concurrency_receipt_source" ] || { + fail "missing regular final Wasm concurrency receipt: $final_wasm_concurrency_receipt_source" +} +expected_final_wasm_concurrency_receipt_sha256="$( + fresh_manifest_value "$guest_build_receipt_source" \ + final_wasm_concurrency_receipt_sha256 +)" +actual_final_wasm_concurrency_receipt_sha256="$( + fresh_wasmer_bin_hash "$final_wasm_concurrency_receipt_source" +)" +[ "$actual_final_wasm_concurrency_receipt_sha256" = \ + "$expected_final_wasm_concurrency_receipt_sha256" ] || { + fail 'final Wasm concurrency receipt differs from guest build receipt' +} +linear_memory_receipt_relative="share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json" +linear_memory_receipt_source="$install_dir/$linear_memory_receipt_relative" +[ -f "$linear_memory_receipt_source" ] && [ ! -L "$linear_memory_receipt_source" ] || { + fail "missing regular linear-memory install receipt: $linear_memory_receipt_source" +} +linear_memory_install_receipt_sha256="$(fresh_wasmer_bin_hash "$linear_memory_receipt_source")" +fresh_is_sha256 "$linear_memory_install_receipt_sha256" || + fail 'linear-memory install receipt identity is invalid' +[ "$(fresh_manifest_value "$guest_build_receipt_source" linear_memory_profile_id)" = \ + "$FRESH_LINEAR_MEMORY_PROFILE_ID" ] || { + fail 'guest build receipt linear-memory profile differs' +} +[ "$(fresh_manifest_value "$guest_build_receipt_source" linear_memory_install_receipt_sha256)" = \ + "$linear_memory_install_receipt_sha256" ] || { + fail 'linear-memory install receipt differs from guest build receipt' +} +case "$core_profile" in + release-o3) expected_atomic_fence_total=995 ;; + *) fail "unsupported sealed carrier profile: $core_profile" ;; +esac +python3 "$FRESH_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" \ + --expected-total "$expected_atomic_fence_total" \ + --latch-state-contract packed-atomic-v1 \ + --verified-receipt "$final_wasm_concurrency_receipt_source" \ + --receipt-only \ + "$install_dir/bin/postgres" >/dev/null || { + fail 'final Wasm concurrency receipt contract validation failed' +} +guest_build_recipe_sha256="$(fresh_wasmer_bin_hash "$guest_build_receipt_source")" +fresh_is_sha256 "$guest_build_recipe_sha256" || fail 'invalid guest build recipe identity' +guest_installed_closure_sha256="$( + fresh_manifest_value "$guest_build_receipt_source" installed_closure_sha256 +)" +fresh_is_sha256 "$guest_installed_closure_sha256" || { + fail 'invalid guest installed closure identity' +} +actual_guest_installed_closure_sha256="$( + python3 "$FRESH_ROOT/lib/guest_build_provenance.py" identity "$install_dir" +)" || exit +[ "$actual_guest_installed_closure_sha256" = \ + "$guest_installed_closure_sha256" ] || { + fail 'guest install bytes differ from their build receipt' +} +share_source="$install_dir/share/postgresql" +[ -d "$share_source" ] && [ ! -L "$share_source" ] || fail "missing PostgreSQL support tree: $share_source" + +compiler="$(fresh_wasmer_compiler)" +[ "$compiler" = llvm ] || fail "sealed carriers currently require the LLVM AOT producer" +llvm_opt_level="${WASMER_LLVM_OPT_LEVEL:-aggressive}" +[ "$llvm_opt_level" = aggressive ] || fail "sealed carriers currently require WASMER_LLVM_OPT_LEVEL=aggressive" +capture_stack_size="${WASMER_STACK_SIZE:-33554432}" +case "$capture_stack_size" in + ''|*[!0-9]*) fail "WASMER_STACK_SIZE must be a positive integer" ;; +esac +[ "$capture_stack_size" -gt 0 ] || fail "WASMER_STACK_SIZE must be greater than zero" +if fresh_wasmer_llvm_native_cpu_enabled; then + fail "sealed carriers refuse WASMER_LLVM_NATIVE_CPU; use the generic baseline CPU policy" +fi +compiler_config="$(fresh_wasmer_compiler_cache_bucket \ + "$compiler" "$llvm_opt_level" "$FRESH_WASMER_ARTIFACT_ABI_VERSION")" +[ -z "${FRESH_PINNED_WASMER_CACHE_DIR:-}" ] || { + fail "sealed product carriers refuse pinned or foreign AOT cache roots: $FRESH_PINNED_WASMER_CACHE_DIR" +} +expected_cache_bucket="$(fresh_wasmer_cache_dir "$postmaster_compiler")/compiled/$compiler_config" +if [ -z "$cache_bucket" ]; then + cache_bucket="$expected_cache_bucket" +fi +[ -d "$cache_bucket" ] && [ ! -L "$cache_bucket" ] || fail "missing regular AOT cache bucket: $cache_bucket" +cache_bucket="$(cd "$cache_bucket" && pwd -P)" +[ -d "$expected_cache_bucket" ] && [ ! -L "$expected_cache_bucket" ] || { + fail "missing receipt-bound AOT cache bucket: $expected_cache_bucket" +} +expected_cache_bucket="$(cd "$expected_cache_bucket" && pwd -P)" +[ "$cache_bucket" = "$expected_cache_bucket" ] || { + fail "AOT cache bucket is not bound to the selected producer: expected $expected_cache_bucket, got $cache_bucket" +} + +required_modules=( + bin/initdb + bin/postgres + lib/libpq.so.5.18 + lib/postgresql/dict_snowball.so + lib/postgresql/plpgsql.so +) +for relative in "${required_modules[@]}"; do + source_path="$install_dir/$relative" + [ -f "$source_path" ] && [ ! -L "$source_path" ] || fail "missing regular runtime-closure module: $source_path" +done +if find "$share_source" -type l -print -quit | grep -q .; then + fail "PostgreSQL support tree contains a symbolic link: $share_source" +fi +if find "$share_source" ! -type d ! -type f -print -quit | grep -q .; then + fail "PostgreSQL support tree contains a special file: $share_source" +fi + +staging="$(mktemp -d "$output_parent/.${output_name}.tmp.XXXXXX")" +validation_root="" +chmod 0755 "$staging" +cleanup_validation_root() { + if [ -n "${validation_root:-}" ] && [ -d "$validation_root" ]; then + chmod -R u+w "$validation_root" 2>/dev/null || true + rm -rf -- "$validation_root" + fi + validation_root="" +} +cleanup() { + cleanup_validation_root + if [ -n "${staging:-}" ] && [ -d "$staging" ]; then + chmod -R u+w "$staging" 2>/dev/null || true + rm -rf -- "$staging" + fi +} +handle_signal() { + local status="$1" + trap - EXIT HUP INT TERM + cleanup + exit "$status" +} +trap cleanup EXIT +trap 'handle_signal 129' HUP +trap 'handle_signal 130' INT +trap 'handle_signal 143' TERM + +mkdir -p \ + "$staging/bin" \ + "$staging/lib/postgresql" \ + "$staging/share/postgresql" \ + "$staging/aot" \ + "$staging/memory" +cp -p "$selected_executor" "$staging/bin/wasmer-headless" +chmod 0555 "$staging/bin/wasmer-headless" +cp -pR "$share_source/." "$staging/share/postgresql/" + +artifact_rows="$staging/.artifact-rows.tsv" +memory_rows="$staging/.memory-rows.tsv" +: >"$artifact_rows" +: >"$memory_rows" + +copy_artifact() { + local name="$1" + local kind="$2" + local relative="$3" + local alias="$4" + local module_source="$install_dir/$relative" + local module_sha256 + local module_hash + local artifact_source + local artifact_relative + local artifact_sha256 + local module_size + local artifact_size + + module_sha256="$(fresh_wasmer_bin_hash "$module_source")" + fresh_is_sha256 "$module_sha256" || fail "invalid module digest: $module_source" + module_hash="${module_sha256^^}" + artifact_source="$cache_bucket/$module_hash.bin" + [ -f "$artifact_source" ] && [ ! -L "$artifact_source" ] && [ -s "$artifact_source" ] || { + fail "missing regular precompiled AOT artifact for $relative: $artifact_source" + } + + mkdir -p "$staging/$(dirname "$relative")" + cp -p "$module_source" "$staging/$relative" + artifact_relative="aot/$module_hash.bin" + cp -p "$artifact_source" "$staging/$artifact_relative" + chmod 0444 "$staging/$artifact_relative" + "$postmaster_compiler" verify-aot \ + "$staging/$relative" "$staging/$artifact_relative" >/dev/null || { + fail "AOT artifact failed product compiler admission: $relative" + } + + artifact_sha256="$(fresh_wasmer_bin_hash "$staging/$artifact_relative")" + module_size="$(wc -c <"$staging/$relative" | tr -d '[:space:]')" + artifact_size="$(wc -c <"$staging/$artifact_relative" | tr -d '[:space:]')" + [ "$module_sha256" = "$(fresh_wasmer_bin_hash "$staging/$relative")" ] || { + fail "module changed while copying: $module_source" + } + [ "$artifact_sha256" = "$(fresh_wasmer_bin_hash "$artifact_source")" ] || { + fail "AOT artifact changed while copying: $artifact_source" + } + + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$name" "$kind" "$artifact_relative" "$relative" "$artifact_sha256" \ + "$artifact_size" "$module_sha256" "$module_size" "$alias" >>"$artifact_rows" +} + +copy_artifact runtime:initdb executable bin/initdb /bin/initdb +copy_artifact runtime:postgres executable bin/postgres /bin/postgres +copy_artifact runtime:libpq.so.5.18 side-module lib/libpq.so.5.18 "" +copy_artifact runtime:dict_snowball.so side-module lib/postgresql/dict_snowball.so "" +copy_artifact runtime:plpgsql.so side-module lib/postgresql/plpgsql.so "" + +# WASIX's dynamic loader resolves the two normal libpq soname aliases. Keep +# each alias a regular carrier file; a symlink would violate the sealed-path +# open policy and could become dangling after relocation. +cp -p "$staging/lib/libpq.so.5.18" "$staging/lib/libpq.so.5" +cp -p "$staging/lib/libpq.so.5.18" "$staging/lib/libpq.so" + +if find "$staging" -type l -print -quit | grep -q .; then + fail "staged carrier contains a symbolic link" +fi +if find "$staging" ! -type d ! -type f -print -quit | grep -q .; then + fail "staged carrier contains a special file" +fi + +sealed_receipt="$staging/wasmer-build.receipt" +cp -p "$receipt" "$sealed_receipt" +chmod 0444 "$sealed_receipt" +sealed_postmaster_executor_receipt="" +sealed_product_build_receipt="" +if [ "$executor_role" = postmaster-product ]; then + sealed_postmaster_executor_receipt="$staging/postmaster-executor.receipt" + cp -p "$postmaster_executor_receipt" "$sealed_postmaster_executor_receipt" + chmod 0444 "$sealed_postmaster_executor_receipt" + sealed_product_build_receipt="$sealed_postmaster_executor_receipt" +else + sealed_product_build_receipt="$staging/postmaster-compiler.receipt" + cp -p "$postmaster_executor_receipt" "$sealed_product_build_receipt" + chmod 0444 "$sealed_product_build_receipt" +fi +guest_build_receipt="$staging/guest-build.receipt" +cp -p "$guest_build_receipt_source" "$guest_build_receipt" +chmod 0444 "$guest_build_receipt" +[ "$(fresh_wasmer_bin_hash "$guest_build_receipt")" = \ + "$guest_build_recipe_sha256" ] || { + fail 'guest build receipt changed while packaging the carrier' +} +staged_guest_installed_closure_sha256="$( + python3 "$FRESH_ROOT/lib/guest_build_provenance.py" identity "$staging" +)" || exit +[ "$staged_guest_installed_closure_sha256" = \ + "$guest_installed_closure_sha256" ] || { + fail 'staged guest bytes differ from their build receipt' +} +actual_guest_installed_closure_sha256="$( + python3 "$FRESH_ROOT/lib/guest_build_provenance.py" identity "$install_dir" +)" || exit +[ "$actual_guest_installed_closure_sha256" = \ + "$guest_installed_closure_sha256" ] || { + fail 'guest install changed while the carrier was staged' +} + +# From this point onward, derive every manifest identity from the immutable +# carrier snapshot, not from a mutable external pathname. Revalidating both +# binaries against that snapshot also closes the receipt/executor copy window. +fresh_require_patched_postmaster_compiler \ + "$postmaster_compiler" \ + "$sealed_product_build_receipt" \ + "$sealed_receipt" \ + "$postmaster_executor" +case "$executor_role" in + postmaster-product) + fresh_require_patched_postmaster_executor \ + "$staging/bin/wasmer-headless" \ + "$sealed_postmaster_executor_receipt" \ + "$sealed_receipt" + ;; + full-headless) + WASMER_BUILD_RECEIPT="$sealed_receipt" \ + fresh_require_patched_wasmer_headless "$staging/bin/wasmer-headless" + ;; +esac +snapshot_runtime_abi_id="$(fresh_manifest_value "$sealed_receipt" runtime_abi_id)" +[ "$snapshot_runtime_abi_id" = "$runtime_abi_id" ] || { + fail "runtime ABI changed while snapshotting the build receipt" +} +receipt="$sealed_receipt" + +source_fingerprint="$(python3 - "$staging" <<'PY' +import hashlib +import os +import stat +import sys + +root = os.path.realpath(sys.argv[1]) +hasher = hashlib.sha256() +for subtree in ("bin", "lib", "share"): + for current, dirs, files in os.walk(os.path.join(root, subtree), followlinks=False): + dirs.sort() + files.sort() + for name in files: + path = os.path.join(current, name) + info = os.lstat(path) + if not stat.S_ISREG(info.st_mode): + raise SystemExit(f"non-regular carrier input: {path}") + relative = os.path.relpath(path, root) + if relative == "bin/wasmer-headless": + continue + digest = hashlib.sha256() + with open(path, "rb", buffering=0) as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + for value in (relative, str(info.st_size), digest.hexdigest()): + encoded = value.encode("utf-8") + hasher.update(len(encoded).to_bytes(8, "big")) + hasher.update(encoded) +print(hasher.hexdigest()) +PY +)" +fresh_is_sha256 "$source_fingerprint" || fail "failed to compute PostgreSQL carrier fingerprint" + +executor_sha256="$(fresh_wasmer_bin_hash "$staging/bin/wasmer-headless")" +executor_size="$(wc -c <"$staging/bin/wasmer-headless" | tr -d '[:space:]')" +target_triple="$(fresh_manifest_value "$receipt" rustc_host)" +host_abi="$(fresh_manifest_value "$receipt" host_abi)" +wasmer_source_commit="$(fresh_manifest_value "$receipt" wasmer_source_commit)" +wasmer_patch_sha256="$(fresh_manifest_value "$receipt" wasmer_patch_sha256)" +wasmer_cargo_lock_sha256="$(fresh_manifest_value "$receipt" wasmer_cargo_lock_sha256)" +producer_recipe_sha256="$(fresh_aot_producer_recipe_sha256 \ + "$receipt" "$sealed_product_build_receipt" "$compiler_config" \ + "$target_triple" "$source_fingerprint")" +fresh_is_sha256 "$producer_recipe_sha256" || fail "failed to compute AOT producer recipe identity" + +write_sealed_manifest() { + local mode="$1" + local output_path="$2" + + python3 - \ + "$artifact_rows" \ + "$memory_rows" \ + "$staging" \ + "$mode" \ + "$output_path" \ + "$source_fingerprint" \ + "$core_profile" \ + "$guest_build_recipe_sha256" \ + "$target_triple" \ + "$host_abi" \ + "$compiler_config" \ + "$wasmer_source_commit" \ + "$wasmer_patch_sha256" \ + "$wasmer_cargo_lock_sha256" \ + "$runtime_abi_id" \ + "$producer_recipe_sha256" \ + "$executor_sha256" \ + "$executor_size" \ + "$POSTGRES_VERSION" \ + "$FRESH_WASMER_VERSION" \ + "$FRESH_WASMER_WASIX_VERSION" \ + "$FRESH_WASMER_ARTIFACT_ABI_VERSION" \ + "$SEALED_FILE_CACHE_REQUESTED_POLICY_ID" \ + "$SEALED_FILE_CACHE_APPROVED_CONFIG_ID" \ + "$SEALED_FILE_CACHE_CONFIG_SHA256" \ + "$SEALED_FILE_CACHE_PORTABLE_FALLBACK_MODE" \ + "$linear_memory_receipt_relative" \ + "$linear_memory_install_receipt_sha256" <<'PY' +import hashlib +import json +import os +import sys + +( + rows_path, + memory_rows_path, + carrier_root, + mode, + output_path, + source_fingerprint, + core_profile, + guest_build_recipe_sha256, + target_triple, + host_abi, + compiler_config, + wasmer_source_commit, + wasmer_patch_sha256, + wasmer_cargo_lock_sha256, + runtime_abi_id, + producer_recipe_sha256, + executor_sha256, + executor_size, + postgres_version, + wasmer_version, + wasmer_wasix_version, + artifact_abi_version, + file_cache_requested_policy_id, + file_cache_approved_config_id, + file_cache_config_sha256, + file_cache_portable_fallback_mode, + linear_memory_receipt_path, + linear_memory_receipt_sha256, +) = sys.argv[1:] + +if mode not in {"capture", "final"}: + raise SystemExit(f"invalid manifest mode: {mode}") + +with open(os.path.join(carrier_root, linear_memory_receipt_path), "r", encoding="utf-8") as stream: + linear_memory_receipt = json.load(stream) +if linear_memory_receipt.get("schema") != "oliphaunt.wasix-postmaster.linear-memory-install.v1": + raise SystemExit("linear-memory install receipt schema differs") +profile_id = linear_memory_receipt.get("profile-id") +expected_profile = { + "profile-id": "oliphaunt.wasix-postmaster.linear-memory.wasm32-max256m-u64-static4g-guard2g.v1", + "address-width": "wasm32", + "supported-host-pointer-width": "u64", + "maximum-pages": 4096, + "maximum-bytes": 268435456, + "static-bound-pages": 65536, + "static-offset-guard-bytes": 2147483648, + "static-access-lowering": "wasmer-llvm-unchecked-reservation-and-guard-v1", +} +for key, expected in expected_profile.items(): + if linear_memory_receipt.get(key) != expected: + raise SystemExit(f"linear-memory install receipt profile differs: {key}") +linear_memory_modules = {} +for record in linear_memory_receipt.get("modules", []): + path = record.get("path") + if not isinstance(path, str) or path in linear_memory_modules: + raise SystemExit("linear-memory install receipt has invalid module paths") + linear_memory_modules[path] = record + +memory_images = {} +if mode == "final": + with open(memory_rows_path, "r", encoding="utf-8", newline="") as rows: + for line_number, line in enumerate(rows, 1): + fields = line.rstrip("\n").split("\t") + if len(fields) != 5: + raise SystemExit(f"invalid memory-image metadata row {line_number}") + name, image_path, image_hash, image_size, receipt_path = fields + if name in memory_images: + raise SystemExit(f"duplicate memory image for {name}") + with open(os.path.join(carrier_root, receipt_path), "r", encoding="utf-8") as stream: + receipt = json.load(stream) + memory_images[name] = { + "path": image_path, + "size": int(image_size), + "sha256": image_hash, + **receipt, + } + +artifacts = [] +executable_names = set() +with open(rows_path, "r", encoding="utf-8", newline="") as rows: + for line_number, line in enumerate(rows, 1): + fields = line.rstrip("\n").split("\t") + if len(fields) != 9: + raise SystemExit(f"invalid artifact metadata row {line_number}") + name, kind, path, module_path, artifact_hash, artifact_size, module_hash, module_size, alias = fields + try: + linear_memory_record = linear_memory_modules[module_path] + except KeyError: + raise SystemExit(f"linear-memory receipt has no record for {module_path}") + if linear_memory_record.get("module-sha256") != module_hash.lower(): + raise SystemExit(f"linear-memory receipt module digest differs for {module_path}") + artifact = { + "name": name, + "kind": kind, + "path": path, + "module-path": module_path, + "sha256": artifact_hash, + "raw-sha256": artifact_hash, + "raw-size": int(artifact_size), + "module-sha256": module_hash, + "module-size": int(module_size), + "linear-memory": { + "profile-id": profile_id, + "source-module-sha256": linear_memory_record["source-module-sha256"], + "install-receipt-sha256": linear_memory_receipt_sha256, + }, + "compressed": False, + "exec-aliases": [alias] if alias else [], + } + if kind == "executable": + executable_names.add(name) + if mode == "final": + try: + artifact["preinitialized-memory"] = memory_images[name] + except KeyError: + raise SystemExit(f"final manifest executable has no memory image: {name}") + elif name in memory_images: + raise SystemExit(f"side module unexpectedly has a memory image: {name}") + artifacts.append(artifact) + +if mode == "final" and set(memory_images) != executable_names: + unexpected = sorted(set(memory_images) - executable_names) + raise SystemExit(f"memory images do not match executable closure: unexpected={unexpected}") + +manifest = { + "format-version": 4 if mode == "capture" else 6, + "schema": ( + "oliphaunt.wasix-postmaster.sealed-aot.v3" + if mode == "capture" + else "oliphaunt.wasix-postmaster.sealed-aot.v5" + ), + "source-lane": "wasix-postmaster", + "source-fingerprint": source_fingerprint, + "core-profile": core_profile, + "guest-build-recipe-sha256": guest_build_recipe_sha256, + "postgres-version": postgres_version, + "target-triple": target_triple, + "host-abi": host_abi, + "engine": "llvm-opta", + "compiler-config": compiler_config, + "cpu-policy": "generic-baseline", + "cpu-features": [], + "wasmer-version": wasmer_version, + "wasmer-wasix-version": wasmer_wasix_version, + "wasmer-source-commit": wasmer_source_commit, + "wasmer-patch-sha256": wasmer_patch_sha256, + "wasmer-cargo-lock-sha256": wasmer_cargo_lock_sha256, + "artifact-abi-version": int(artifact_abi_version), + "runtime-abi-id": runtime_abi_id, + "producer-recipe-sha256": producer_recipe_sha256, + "executor-engine": "engine-headless", + "executor-sha256": executor_sha256, + "executor-size": int(executor_size), + "linear-memory-profile": { + "id": profile_id, + "address-width": linear_memory_receipt["address-width"], + "supported-host-pointer-width": linear_memory_receipt["supported-host-pointer-width"], + "maximum-pages": linear_memory_receipt["maximum-pages"], + "maximum-bytes": linear_memory_receipt["maximum-bytes"], + "static-bound-pages": linear_memory_receipt["static-bound-pages"], + "static-offset-guard-bytes": linear_memory_receipt["static-offset-guard-bytes"], + "static-access-lowering": linear_memory_receipt["static-access-lowering"], + "install-receipt-path": linear_memory_receipt_path, + "install-receipt-sha256": linear_memory_receipt_sha256, + }, + "file-cache-policy": { + "requested-policy-id": file_cache_requested_policy_id, + "approved-config-id": file_cache_approved_config_id, + "config-sha256": file_cache_config_sha256, + "portable-fallback-mode": file_cache_portable_fallback_mode, + }, + "wasm-features": ["exceptions", "threads"], + "entrypoint": "runtime:postgres", + "artifacts": artifacts, +} +with open(output_path, "x", encoding="utf-8", newline="\n") as output: + json.dump(manifest, output, ensure_ascii=False, indent=2) + output.write("\n") +PY +} + +capture_manifest="$staging/.capture-manifest.json" +write_sealed_manifest capture "$capture_manifest" +chmod 0444 "$capture_manifest" + +upgrade_memory_image_receipt() { + local receipt_path="$1" + local proof_path="$2" + local module_sha256="$3" + + python3 - "$receipt_path" "$proof_path" "$module_sha256" "$runtime_abi_id" <<'PY' +import hashlib +import json +import os +import sys + +receipt_path, proof_path, module_sha256, runtime_abi_id = sys.argv[1:] +with open(receipt_path, "r", encoding="utf-8") as stream: + receipt = json.load(stream) +with open(proof_path, "r", encoding="utf-8") as stream: + proof = json.load(stream) + +v1_keys = { + "schema", "module-sha256", "runtime-abi-id", "phase", + "mapping-alignment", "mapped-size", "memory-minimum-pages", + "memory-maximum-pages", "memory-shared", "memory-base", + "dylink-memory-size", "dylink-memory-alignment", "stack-low", +} +proof_keys = { + "schema", "analyzer-policy", "module-sha256", "proof-sha256", + "start-function-index", "start-function-export", + "transitive-function-indices", "imported-function-calls", + "memory-reads", "memory-effects", "global-effects", "table-effects", + "requires-fresh-zeroed-memory", "ordinary-start-execution-per-instance", + "first-instance-full-byte-validation", +} +if set(receipt) != v1_keys: + raise SystemExit("capture receipt is not the exact memory-image v1 shape") +if receipt["schema"] != "oliphaunt.wasix-postmaster.memory-image.v1": + raise SystemExit("capture receipt is not memory-image v1") +if receipt["module-sha256"] != module_sha256.lower(): + raise SystemExit("capture receipt module digest mismatch") +if receipt["runtime-abi-id"] != runtime_abi_id: + raise SystemExit("capture receipt runtime ABI mismatch") +if set(proof) != proof_keys: + raise SystemExit("deterministic-start proof fields differ") +if proof["schema"] != "oliphaunt.wasix-postmaster.deterministic-start-proof.v1": + raise SystemExit("deterministic-start proof schema mismatch") +if proof["analyzer-policy"] != "llvm-shared-memory-init-restricted-effects.v1": + raise SystemExit("deterministic-start analyzer policy mismatch") +if proof["module-sha256"] != module_sha256.lower(): + raise SystemExit("deterministic-start proof module digest mismatch") +if proof["imported-function-calls"] != 0: + raise SystemExit("deterministic-start proof admits imported calls") +if proof["memory-reads"] != "fresh-zero-atomic-guard-only": + raise SystemExit("deterministic-start proof memory-read policy mismatch") +if proof["memory-effects"] != "passive-data-init-zero-fill-atomic-guard-only": + raise SystemExit("deterministic-start proof memory-effect policy mismatch") +if proof["global-effects"] != "local-numeric-relocations-only": + raise SystemExit("deterministic-start proof global-effect policy mismatch") +if proof["table-effects"] != "none": + raise SystemExit("deterministic-start proof admits table effects") +for field in ( + "requires-fresh-zeroed-memory", + "ordinary-start-execution-per-instance", + "first-instance-full-byte-validation", +): + if proof[field] is not True: + raise SystemExit(f"deterministic-start proof requires {field}=true") +if type(proof["start-function-index"]) is not int or proof["start-function-index"] < 0: + raise SystemExit("deterministic-start start function index is invalid") +if proof["start-function-export"] != "__wasm_init_memory": + raise SystemExit("deterministic-start export mismatch") +closure = proof["transitive-function-indices"] +if ( + not isinstance(closure, list) + or not closure + or any(type(index) is not int or index < 0 for index in closure) + or closure != sorted(set(closure)) + or proof["start-function-index"] not in closure +): + raise SystemExit("deterministic-start function closure is invalid") +digest = proof["proof-sha256"] +if not isinstance(digest, str) or len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest): + raise SystemExit("deterministic-start proof digest is not lowercase SHA-256") + +receipt["schema"] = "oliphaunt.wasix-postmaster.memory-image.v2" +receipt["deterministic-start-proof"] = proof +canonical_proof = json.dumps( + proof, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), +).encode("utf-8") +receipt["deterministic-start-proof-output-sha256"] = hashlib.sha256( + canonical_proof +).hexdigest() +temporary = receipt_path + ".v2" +with open(temporary, "x", encoding="utf-8", newline="\n") as stream: + json.dump(receipt, stream, ensure_ascii=False, indent=2) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) +os.replace(temporary, receipt_path) +PY +} + +validate_memory_image_receipt() { + local receipt_path="$1" + local image_path="$2" + local module_sha256="$3" + + python3 - "$receipt_path" "$image_path" "$module_sha256" "$runtime_abi_id" <<'PY' +import hashlib +import json +import os +import sys + +receipt_path, image_path, module_sha256, runtime_abi_id = sys.argv[1:] +with open(receipt_path, "r", encoding="utf-8") as stream: + receipt = json.load(stream) + +expected_keys = { + "schema", + "module-sha256", + "runtime-abi-id", + "phase", + "mapping-alignment", + "mapped-size", + "memory-minimum-pages", + "memory-maximum-pages", + "memory-shared", + "memory-base", + "dylink-memory-size", + "dylink-memory-alignment", + "stack-low", + "deterministic-start-proof", + "deterministic-start-proof-output-sha256", +} +if set(receipt) != expected_keys: + raise SystemExit( + f"memory image receipt fields differ: missing={sorted(expected_keys - set(receipt))} " + f"unknown={sorted(set(receipt) - expected_keys)}" + ) +if receipt["schema"] != "oliphaunt.wasix-postmaster.memory-image.v2": + raise SystemExit("memory image receipt schema mismatch") +if receipt["module-sha256"] != module_sha256.lower(): + raise SystemExit("memory image receipt module digest mismatch") +if receipt["runtime-abi-id"] != runtime_abi_id: + raise SystemExit("memory image receipt runtime ABI mismatch") +if receipt["phase"] != "post-module-start-pre-link-relocations-v1": + raise SystemExit("memory image receipt phase mismatch") + +proof = receipt["deterministic-start-proof"] +proof_keys = { + "schema", "analyzer-policy", "module-sha256", "proof-sha256", + "start-function-index", "start-function-export", + "transitive-function-indices", "imported-function-calls", + "memory-reads", "memory-effects", "global-effects", "table-effects", + "requires-fresh-zeroed-memory", "ordinary-start-execution-per-instance", + "first-instance-full-byte-validation", +} +if not isinstance(proof, dict) or set(proof) != proof_keys: + raise SystemExit("memory image deterministic-start proof fields differ") +if proof["schema"] != "oliphaunt.wasix-postmaster.deterministic-start-proof.v1": + raise SystemExit("memory image deterministic-start proof schema mismatch") +if proof["analyzer-policy"] != "llvm-shared-memory-init-restricted-effects.v1": + raise SystemExit("memory image deterministic-start analyzer policy mismatch") +if proof["module-sha256"] != module_sha256.lower(): + raise SystemExit("memory image deterministic-start module digest mismatch") +if proof["imported-function-calls"] != 0: + raise SystemExit("memory image deterministic-start proof admits imported calls") +if proof["memory-reads"] != "fresh-zero-atomic-guard-only": + raise SystemExit("memory image deterministic-start read policy mismatch") +if proof["memory-effects"] != "passive-data-init-zero-fill-atomic-guard-only": + raise SystemExit("memory image deterministic-start memory policy mismatch") +if proof["global-effects"] != "local-numeric-relocations-only" or proof["table-effects"] != "none": + raise SystemExit("memory image deterministic-start non-memory effects mismatch") +if any(proof[field] is not True for field in ( + "requires-fresh-zeroed-memory", + "ordinary-start-execution-per-instance", + "first-instance-full-byte-validation", +)): + raise SystemExit("memory image deterministic-start execution contract mismatch") +closure = proof["transitive-function-indices"] +if ( + type(proof["start-function-index"]) is not int + or proof["start-function-index"] < 0 + or proof["start-function-export"] != "__wasm_init_memory" + or not isinstance(closure, list) + or not closure + or any(type(index) is not int or index < 0 for index in closure) + or closure != sorted(set(closure)) + or proof["start-function-index"] not in closure +): + raise SystemExit("memory image deterministic-start function closure mismatch") +proof_digest = proof["proof-sha256"] +if not isinstance(proof_digest, str) or len(proof_digest) != 64 or any(c not in "0123456789abcdef" for c in proof_digest): + raise SystemExit("memory image deterministic-start digest is not lowercase SHA-256") +canonical_proof = json.dumps( + proof, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), +).encode("utf-8") +expected_output_digest = hashlib.sha256(canonical_proof).hexdigest() +if receipt["deterministic-start-proof-output-sha256"] != expected_output_digest: + raise SystemExit("memory image deterministic-start analyzer output digest mismatch") + +integer_fields = ( + "mapping-alignment", + "mapped-size", + "memory-minimum-pages", + "memory-base", + "dylink-memory-size", + "dylink-memory-alignment", + "stack-low", +) +for field in integer_fields: + if type(receipt[field]) is not int or receipt[field] < 0: + raise SystemExit(f"memory image receipt {field} must be a nonnegative integer") +maximum_pages = receipt["memory-maximum-pages"] +if maximum_pages is not None and (type(maximum_pages) is not int or maximum_pages < 0): + raise SystemExit("memory image receipt memory-maximum-pages must be null or nonnegative") +if type(receipt["memory-shared"]) is not bool or not receipt["memory-shared"]: + raise SystemExit("WASIX PostgreSQL memory images require shared linear memory") + +alignment = receipt["mapping-alignment"] +mapped_size = receipt["mapped-size"] +minimum_pages = receipt["memory-minimum-pages"] +memory_base = receipt["memory-base"] +dylink_size = receipt["dylink-memory-size"] +dylink_alignment = receipt["dylink-memory-alignment"] +stack_low = receipt["stack-low"] +if alignment != 65536 or mapped_size == 0 or mapped_size % alignment: + raise SystemExit("memory image receipt has an invalid 64-KiB mapped range") +if os.path.getsize(image_path) != mapped_size: + raise SystemExit("captured memory image size does not match receipt") +if minimum_pages == 0 or mapped_size > minimum_pages * 65536: + raise SystemExit("captured memory image exceeds the initial linear memory") +if maximum_pages is not None and maximum_pages < minimum_pages: + raise SystemExit("memory image maximum pages are below the minimum") +if dylink_alignment > 63 or memory_base % (1 << dylink_alignment): + raise SystemExit("memory image dylink memory base is misaligned") +initialized_end = memory_base + dylink_size +if not mapped_size <= initialized_end <= stack_low: + raise SystemExit("memory image range, dylink data, and stack are inconsistent") +if not mapped_size <= stack_low < mapped_size + alignment: + raise SystemExit("memory image mapped size is not the 64-KiB floor of stack-low") + +with open(image_path, "rb", buffering=0) as stream: + digest = hashlib.sha256() + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) +if len(digest.digest()) != 32: + raise SystemExit("failed to hash captured memory image") +PY +} + +capture_memory_image() { + local artifact_name="$1" + local module_relative="$2" + local module_sha256="$3" + local capture_name="${artifact_name#runtime:}" + local capture_root="$staging/.memory-capture/$capture_name" + local attempt + local attempt_root + local image_path + local receipt_path + local proof_path + local proof_log + local proof_status + local capture_log + local capture_status + + for attempt in 1 2; do + attempt_root="$capture_root/$attempt" + mkdir -p "$attempt_root/home" "$attempt_root/cache" + image_path="$attempt_root/image.bin" + receipt_path="$attempt_root/receipt.json" + proof_path="$attempt_root/start-proof.json" + proof_log="$attempt_root/start-proof.log" + capture_log="$attempt_root/capture.log" + [ "$(fresh_wasmer_bin_hash "$staging/$module_relative")" = "$module_sha256" ] || { + fail "raw module changed before deterministic-start analysis $attempt for $artifact_name" + } + set +e + "$start_proof_tool" "$staging/$module_relative" >"$proof_path" 2>"$proof_log" + proof_status=$? + set -e + if [ "$proof_status" -ne 0 ]; then + sed 's/^/deterministic-start analysis: /' "$proof_log" >&2 + fail "deterministic-start analysis $attempt failed for $artifact_name" + fi + [ -f "$proof_path" ] && [ ! -L "$proof_path" ] && [ -s "$proof_path" ] || { + fail "deterministic-start analysis $attempt did not emit a regular proof for $artifact_name" + } + [ "$(fresh_wasmer_bin_hash "$staging/$module_relative")" = "$module_sha256" ] || { + fail "raw module changed during deterministic-start analysis $attempt for $artifact_name" + } + set +e + env \ + WASMER_DIR="$attempt_root/home" \ + WASMER_CACHE_DIR="$attempt_root/cache" \ + "$staging/bin/wasmer-headless" run \ + --disable-cache \ + --stack-size "$capture_stack_size" \ + --sealed-module-manifest "$capture_manifest" \ + --emit-preinitialized-memory-image "$image_path" \ + --emit-preinitialized-memory-receipt "$receipt_path" \ + --enable-exceptions \ + --enable-threads \ + --net \ + --volume "$staging/lib:/lib" \ + "$staging/$module_relative" -- --version >"$capture_log" 2>&1 + capture_status=$? + set -e + if [ "$capture_status" -ne 0 ]; then + sed 's/^/memory image capture: /' "$capture_log" >&2 + fail "headless executor failed memory image capture $attempt for $artifact_name" + fi + [ -f "$image_path" ] && [ ! -L "$image_path" ] && [ -s "$image_path" ] || { + fail "memory image capture $attempt did not emit a regular image for $artifact_name" + } + [ -f "$receipt_path" ] && [ ! -L "$receipt_path" ] && [ -s "$receipt_path" ] || { + fail "memory image capture $attempt did not emit a regular receipt for $artifact_name" + } + [ "$(fresh_wasmer_bin_hash "$staging/$module_relative")" = "$module_sha256" ] || { + fail "raw module changed during memory image capture $attempt for $artifact_name" + } + upgrade_memory_image_receipt "$receipt_path" "$proof_path" "$module_sha256" || { + fail "could not bind deterministic-start proof to memory image receipt $attempt for $artifact_name" + } + validate_memory_image_receipt "$receipt_path" "$image_path" "$module_sha256" || { + fail "invalid memory image receipt $attempt for $artifact_name" + } + done + + cmp -s "$capture_root/1/image.bin" "$capture_root/2/image.bin" || { + fail "independent memory image captures differ for $artifact_name" + } + cmp -s "$capture_root/1/receipt.json" "$capture_root/2/receipt.json" || { + fail "independent memory image receipts differ for $artifact_name" + } + local module_hash="${module_sha256^^}" + local image_relative="memory/$module_hash.bin" + local receipt_relative="memory/$module_hash.receipt.json" + local image_sha256 + local image_size + + cp -p "$capture_root/1/image.bin" "$staging/$image_relative" + cp -p "$capture_root/1/receipt.json" "$staging/$receipt_relative" + chmod 0444 "$staging/$image_relative" "$staging/$receipt_relative" + image_sha256="$(fresh_wasmer_bin_hash "$staging/$image_relative")" + image_size="$(wc -c <"$staging/$image_relative" | tr -d '[:space:]')" + [ "$image_sha256" = "$(fresh_wasmer_bin_hash "$capture_root/1/image.bin")" ] || { + fail "memory image changed while packaging $artifact_name" + } + [ "$(fresh_wasmer_bin_hash "$staging/$receipt_relative")" = \ + "$(fresh_wasmer_bin_hash "$capture_root/1/receipt.json")" ] || { + fail "memory image receipt changed while packaging $artifact_name" + } + printf '%s\t%s\t%s\t%s\t%s\n' \ + "$artifact_name" "$image_relative" "$image_sha256" "$image_size" \ + "$receipt_relative" >>"$memory_rows" +} + +capture_memory_image \ + runtime:initdb bin/initdb "$(fresh_wasmer_bin_hash "$staging/bin/initdb")" +capture_memory_image \ + runtime:postgres bin/postgres "$(fresh_wasmer_bin_hash "$staging/bin/postgres")" + +rm -rf -- "$staging/.memory-capture" +rm -f -- "$capture_manifest" +write_sealed_manifest final "$staging/manifest.json" +rm "$artifact_rows" "$memory_rows" +chmod 0444 "$staging/manifest.json" + +# Exercise the final memory-image-bearing carrier before publication. A +# version probe is sufficient for the postgres entrypoint, but initdb must run +# its real bootstrap lifecycle: it reads the packaged share tree, loads libpq, +# creates writable relation files, and EXEC_BACKEND-spawns the sealed postgres +# alias. Keep every writable path outside staging and remove it through the +# same signal-safe cleanup path as the unpublished carrier. +validation_root="$(mktemp -d "$output_parent/.${output_name}.validate.XXXXXX")" +mkdir -p \ + "$validation_root/home" \ + "$validation_root/cache" \ + "$validation_root/pgdata" \ + "$validation_root/dev-shm" +chmod 0700 "$validation_root/pgdata" +chmod 1777 "$validation_root/dev-shm" + +# HostFS volumes are writable mappings. Remove write permission from every +# staged payload before exposing it to the guest, and verify its complete +# content-and-mode fingerprint afterwards. Only the disposable PGDATA and +# /dev/shm mappings are intentionally writable. +carrier_mode_snapshot="$validation_root/carrier-modes.json" +python3 - "$staging" "$carrier_mode_snapshot" <<'PY' +import json +import os +import stat +import sys + +root = os.path.realpath(sys.argv[1]) +modes = {} +for current, dirs, files in os.walk(root, followlinks=False): + dirs.sort() + files.sort() + for name in [*dirs, *files]: + path = os.path.join(current, name) + modes[os.path.relpath(path, root)] = stat.S_IMODE(os.lstat(path).st_mode) +modes["."] = stat.S_IMODE(os.lstat(root).st_mode) +with open(sys.argv[2], "x", encoding="utf-8", newline="\n") as stream: + json.dump(modes, stream, sort_keys=True) + stream.write("\n") +PY +chmod -R a-w "$staging" +carrier_validation_fingerprint() { + python3 - "$staging" <<'PY' +import hashlib +import os +import stat +import sys + +root = os.path.realpath(sys.argv[1]) +digest = hashlib.sha256() +for current, dirs, files in os.walk(root, followlinks=False): + dirs.sort() + files.sort() + relative_directory = os.path.relpath(current, root) + directory_mode = stat.S_IMODE(os.lstat(current).st_mode) + digest.update(f"d\0{relative_directory}\0{directory_mode:o}\0".encode()) + for name in files: + path = os.path.join(current, name) + info = os.lstat(path) + if not stat.S_ISREG(info.st_mode): + raise SystemExit(f"carrier validation input is not regular: {path}") + relative = os.path.relpath(path, root) + file_digest = hashlib.sha256() + with open(path, "rb", buffering=0) as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + file_digest.update(chunk) + digest.update( + f"f\0{relative}\0{stat.S_IMODE(info.st_mode):o}\0{info.st_size}\0{file_digest.hexdigest()}\0".encode() + ) +print(digest.hexdigest()) +PY +} +validation_fingerprint="$(carrier_validation_fingerprint)" +fresh_is_sha256 "$validation_fingerprint" || fail "failed to fingerprint carrier before validation" + +validation_common_args=( + run + --disable-cache + --stack-size "$capture_stack_size" + --sealed-module-manifest "$staging/manifest.json" + --enable-exceptions + --enable-threads + --net + --volume "$staging:$staging" + --volume "$staging/share:/share" + --volume "$staging/lib:/lib" + --volume "$validation_root/pgdata:/pgdata" + --volume "$validation_root/dev-shm:/dev/shm" +) + +postgres_validation_log="$validation_root/postgres.log" +set +e +env \ + WASMER_DIR="$validation_root/home" \ + WASMER_CACHE_DIR="$validation_root/cache" \ + "$staging/bin/wasmer-headless" "${validation_common_args[@]}" \ + "$staging/bin/postgres" -- --version >"$postgres_validation_log" 2>&1 +postgres_validation_status=$? +set -e +if [ "$postgres_validation_status" -ne 0 ]; then + sed 's/^/sealed postgres load check: /' "$postgres_validation_log" >&2 + fail "headless executor rejected sealed postgres with its memory image" +fi + +initdb_validation_log="$validation_root/initdb.log" +set +e +env \ + WASMER_DIR="$validation_root/home" \ + WASMER_CACHE_DIR="$validation_root/cache" \ + "$staging/bin/wasmer-headless" "${validation_common_args[@]}" \ + "$staging/bin/initdb" -- \ + -D /pgdata \ + -A trust \ + --no-locale \ + --encoding=UTF8 \ + --no-instructions >"$initdb_validation_log" 2>&1 +initdb_validation_status=$? +set -e +if [ "$initdb_validation_status" -ne 0 ]; then + sed 's/^/sealed initdb lifecycle check: /' "$initdb_validation_log" >&2 + fail "headless executor failed the sealed initdb lifecycle" +fi +for initialized_path in PG_VERSION global/pg_control; do + if ! { [ -f "$validation_root/pgdata/$initialized_path" ] \ + && [ ! -L "$validation_root/pgdata/$initialized_path" ] \ + && [ -s "$validation_root/pgdata/$initialized_path" ]; } + then + fail "sealed initdb lifecycle did not create regular non-empty $initialized_path" + fi +done + +[ "$(carrier_validation_fingerprint)" = "$validation_fingerprint" ] || { + fail "sealed validation mutated the staged carrier" +} +python3 - "$staging" "$carrier_mode_snapshot" <<'PY' +import json +import os +import sys + +root = os.path.realpath(sys.argv[1]) +with open(sys.argv[2], encoding="utf-8") as stream: + modes = json.load(stream) +for relative, mode in modes.items(): + path = root if relative == "." else os.path.join(root, relative) + os.chmod(path, mode, follow_symlinks=False) +PY +cleanup_validation_root + +# The payload inventory covers every published regular file except itself. +# It also provides a portable verification surface for support files that are +# intentionally outside the strict AOT loader schema. +python3 - "$staging" "$staging/payload.files" <<'PY' +import hashlib +import os +import stat +import sys + +root = os.path.realpath(sys.argv[1]) +inventory = os.path.realpath(sys.argv[2]) +rows = [] +for current, dirs, files in os.walk(root, followlinks=False): + dirs.sort() + files.sort() + for name in files: + path = os.path.join(current, name) + if os.path.realpath(path) == inventory: + continue + info = os.lstat(path) + if not stat.S_ISREG(info.st_mode): + raise SystemExit(f"carrier contains non-regular file: {path}") + relative = os.path.relpath(path, root) + if any(character in relative for character in ("\n", "\r", "\t")): + raise SystemExit(f"carrier path contains a control delimiter: {relative!r}") + digest = hashlib.sha256() + with open(path, "rb", buffering=0) as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + rows.append((relative, info.st_size, digest.hexdigest())) +with open(inventory, "x", encoding="utf-8", newline="\n") as output: + output.write("schema=oliphaunt.wasix-postmaster.payload-files.v1\n") + for relative, size, digest in sorted(rows): + output.write(f"{digest}\t{size}\t{relative}\n") +PY +chmod 0444 "$staging/payload.files" + +# A sealed carrier is an immutable deployment input, not a runtime cache or a +# scratch directory. Normalize the published mode surface after the complete +# payload has been assembled: every directory is traversable/read-only and +# every regular file is read-only, while preserving whether a file was meant +# to be directly executable by the host. The loader must place any ephemeral +# AOT snapshot in its separate scratch tier. The cleanup trap deliberately +# restores owner write permission if a later publication check fails. +python3 - "$staging" <<'PY' +import os +import stat +import sys + +root = os.path.realpath(sys.argv[1]) +for current, directories, files in os.walk(root, topdown=False, followlinks=False): + for name in files: + path = os.path.join(current, name) + info = os.lstat(path) + if not stat.S_ISREG(info.st_mode): + raise SystemExit(f"sealed carrier contains a non-regular file: {path}") + executable = bool(stat.S_IMODE(info.st_mode) & 0o111) + os.chmod(path, 0o555 if executable else 0o444, follow_symlinks=False) + for name in directories: + path = os.path.join(current, name) + info = os.lstat(path) + if not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode): + raise SystemExit(f"sealed carrier contains a non-directory: {path}") + os.chmod(path, 0o555, follow_symlinks=False) +os.chmod(root, 0o555, follow_symlinks=False) +PY + +# Reconsume the finished staging tree through the same verifier used by every +# sealed runtime entrypoint. This proves that the inventory is exact and that +# its manifest, receipt, executor, modules, AOT, and memory images form one +# internally consistent closure before any path is published. +fresh_verify_sealed_headless_carrier "$staging" || { + fail "finished sealed carrier failed complete payload verification" +} + +payload_inventory_sha256="$(fresh_wasmer_bin_hash "$staging/payload.files")" +fresh_is_sha256 "$payload_inventory_sha256" || { + fail "failed to compute sealed carrier payload identity" +} +if [ "$output_is_explicit" -eq 0 ]; then + output_name="wasix-postmaster-$POSTGRES_VERSION-${runtime_abi_id:0:16}-$payload_inventory_sha256" + output="$output_parent/$output_name" + [ ! -e "$output" ] && [ ! -L "$output" ] || { + fail "content-addressed output already exists: $output" + } +fi + +# Durability is scoped to the carrier: flush each regular file, then each +# directory bottom-up. This avoids a global sync while ensuring rename never +# publishes a directory whose verified bytes only lived in page cache. +python3 - "$staging" <<'PY' +import os +import stat +import sys + +root = os.path.realpath(sys.argv[1]) +directories = [] +for current, dirs, files in os.walk(root, topdown=True, followlinks=False): + directories.append(current) + for name in files: + path = os.path.join(current, name) + info = os.lstat(path) + if not stat.S_ISREG(info.st_mode): + raise SystemExit(f"carrier contains non-regular file: {path}") + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) + descriptor = os.open(path, flags) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) +for directory in reversed(directories): + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_DIRECTORY", 0) + descriptor = os.open(directory, flags) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) +PY + +publication_lock_path="$output_parent/.${output_name}.publish.lock" +exec {publication_lock_fd}>"$publication_lock_path" +chmod 0600 "$publication_lock_path" +flock -n "$publication_lock_fd" || + fail "another process is publishing the same carrier output: $output" +[ ! -e "$output" ] && [ ! -L "$output" ] || + fail "carrier output appeared before atomic publication: $output" +fresh_atomic_publish_directory_noreplace "$staging" "$output" || + fail "could not atomically publish sealed carrier: $output" +staging="" +python3 - "$output_parent" <<'PY' +import os +import sys + +flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_DIRECTORY", 0) +descriptor = os.open(sys.argv[1], flags) +try: + os.fsync(descriptor) +finally: + os.close(descriptor) +PY +trap - EXIT HUP INT TERM + +printf 'built sealed headless WASIX PostgreSQL carrier: %s\n' "$output" +printf 'executor role: %s\n' "$executor_role" +printf 'runtime ABI ID: %s\n' "$runtime_abi_id" +printf 'source fingerprint: %s\n' "$source_fingerprint" +printf 'payload inventory SHA-256: %s\n' "$payload_inventory_sha256" +printf 'preinitialized memory images: %s\n' "$output/memory" +printf 'payload inventory: %s\n' "$output/payload.files" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-sealed-headless-carrier.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-sealed-headless-carrier.test.sh new file mode 100755 index 00000000..116f308a --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-sealed-headless-carrier.test.sh @@ -0,0 +1,1078 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +test_root="$(mktemp -d "${TMPDIR:-/tmp}/oliphaunt-sealed-carrier.XXXXXX")" +cleanup_test_root() { + chmod -R u+w "$test_root" 2>/dev/null || true + rm -rf -- "$test_root" +} +trap cleanup_test_root EXIT + +export FRESH_WORK_ROOT="$test_root/work" +export FRESH_UPSTREAM_WASMER_BIN="$test_root/wasmer" +export FRESH_UPSTREAM_WASMER_HEADLESS_BIN="$test_root/wasmer-headless" +export FRESH_POSTMASTER_EXECUTOR_BIN="$test_root/postmaster-executor" +export FRESH_START_PROOF_BIN="$test_root/start-proof" +export FRESH_POSTMASTER_COMPILER_BIN="$test_root/postmaster-compiler" +export FRESH_WASMER_BUILD_RECEIPT="$test_root/wasmer-build.receipt" +export FRESH_POSTMASTER_EXECUTOR_BUILD_RECEIPT="$test_root/postmaster-executor-build.receipt" +export WASIX_INSTALL_DIR="$test_root/install" +export WASIX_CORE_PROFILE=release-o3 +export WASMER_COMPILER=llvm +export WASMER_LLVM_OPT_LEVEL=aggressive +export FAKE_WASMER_CAPTURE_LOG="$test_root/memory-captures.log" +export FAKE_WASMER_VALIDATION_LOG="$test_root/final-validations.log" +unset FRESH_PINNED_WASMER_CACHE_DIR FRESH_ALLOW_PINNED_CACHE_WRITE + +source "$project_root/lib/common.sh" +source "$project_root/lib/sealed-carrier.sh" + +atomic_parent="$test_root/atomic-publication" +mkdir -p "$atomic_parent/source" "$atomic_parent/competitor" +printf 'owned-by-competitor\n' >"$atomic_parent/competitor/sentinel" +if fresh_atomic_publish_directory_noreplace \ + "$atomic_parent/source" "$atomic_parent/competitor" >/dev/null 2>&1; then + printf 'atomic carrier publication replaced a competitor unexpectedly\n' >&2 + exit 1 +fi +[ -d "$atomic_parent/source" ] && \ + [ "$(cat "$atomic_parent/competitor/sentinel")" = owned-by-competitor ] || { + printf 'failed atomic publication mutated source or competitor output\n' >&2 + exit 1 +} +mkdir "$atomic_parent/publishable" +printf 'published\n' >"$atomic_parent/publishable/payload" +fresh_atomic_publish_directory_noreplace \ + "$atomic_parent/publishable" "$atomic_parent/published" +[ ! -e "$atomic_parent/publishable" ] && \ + [ "$(cat "$atomic_parent/published/payload")" = published ] || { + printf 'atomic carrier publication did not rename the exact source directory\n' >&2 + exit 1 +} + +mkdir -p \ + "$WASIX_INSTALL_DIR/bin" \ + "$WASIX_INSTALL_DIR/lib/postgresql" \ + "$WASIX_INSTALL_DIR/share/postgresql" +cp "$project_root/testdata/fake-sealed-wasmer.py" "$FRESH_UPSTREAM_WASMER_BIN" +cp "$project_root/testdata/fake-sealed-wasmer.py" "$FRESH_UPSTREAM_WASMER_HEADLESS_BIN" +cp "$project_root/testdata/fake-sealed-wasmer.py" "$FRESH_POSTMASTER_EXECUTOR_BIN" +printf '# product-executor-fixture\n' >>"$FRESH_POSTMASTER_EXECUTOR_BIN" +cp "$project_root/testdata/fake-start-proof.py" "$FRESH_START_PROOF_BIN" +cp "$project_root/testdata/fake-postmaster-compiler.py" "$FRESH_POSTMASTER_COMPILER_BIN" +chmod +x "$FRESH_UPSTREAM_WASMER_BIN" "$FRESH_UPSTREAM_WASMER_HEADLESS_BIN" \ + "$FRESH_POSTMASTER_EXECUTOR_BIN" "$FRESH_START_PROOF_BIN" \ + "$FRESH_POSTMASTER_COMPILER_BIN" +printf 'initdb-wasm\n' >"$WASIX_INSTALL_DIR/bin/initdb" +printf 'postgres-wasm\n' >"$WASIX_INSTALL_DIR/bin/postgres" +printf 'libpq-wasm\n' >"$WASIX_INSTALL_DIR/lib/libpq.so.5.18" +printf 'snowball-wasm\n' >"$WASIX_INSTALL_DIR/lib/postgresql/dict_snowball.so" +printf 'plpgsql-wasm\n' >"$WASIX_INSTALL_DIR/lib/postgresql/plpgsql.so" +printf 'sample-config\n' >"$WASIX_INSTALL_DIR/share/postgresql/postgresql.conf.sample" +python3 "$project_root/testdata/make-sealed-export-fixture.py" \ + --install-root "$WASIX_INSTALL_DIR" \ + --project-root "$project_root" +chmod 0644 "$WASIX_INSTALL_DIR/share/postgresql/postgresql.conf.sample" +postgres_sha256="$(fresh_wasmer_bin_hash "$WASIX_INSTALL_DIR/bin/postgres")" +final_wasm_concurrency_receipt="$WASIX_INSTALL_DIR/share/postgresql/wasix-postmaster.final-wasm-concurrency.receipt" +{ + printf 'schema=oliphaunt.wasix-postmaster.final-wasm-concurrency.v1\n' + printf 'postgres_sha256=%s\n' "$postgres_sha256" + printf 'wasm_dis_sha256=%064d\n' 2 + printf 'wasm_dis_version=fake-wasm-dis version 130\n' + printf 'latch_state_contract=packed-atomic-v1\n' + printf 'atomic_fence_total=995\n' + printf 'atomic_fence_set_latch=2\n' + printf 'atomic_fence_reset_latch=1\n' + printf 'atomic_fence_wait_event_set_wait=1\n' + printf 'i32_atomic_load_total=2\n' + printf 'i32_atomic_load_wait_event_set_wait=2\n' + printf 'i32_atomic_rmw_and_total=7\n' + printf 'i32_atomic_rmw_and_reset_latch=1\n' + printf 'i32_atomic_rmw_and_wait_event_set_wait=2\n' + printf 'i32_atomic_rmw_or_total=117\n' + printf 'i32_atomic_rmw_or_set_latch=1\n' + printf 'i32_atomic_rmw_or_wait_event_set_wait=1\n' +} >"$final_wasm_concurrency_receipt" +chmod 0444 "$final_wasm_concurrency_receipt" +final_wasm_concurrency_receipt_sha256="$( + fresh_wasmer_bin_hash "$final_wasm_concurrency_receipt" +)" + +sealed_export_receipt="$WASIX_INSTALL_DIR/share/postgresql/wasix-postmaster.sealed-export.structure.receipt" +linear_memory_receipt="$WASIX_INSTALL_DIR/share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json" +python3 - "$WASIX_INSTALL_DIR" "$sealed_export_receipt" "$linear_memory_receipt" "$project_root" <<'PY' +import hashlib +import json +import sys +from pathlib import Path + +root = Path(sys.argv[1]) +predecessor = Path(sys.argv[2]) +output = Path(sys.argv[3]) +project_root = Path(sys.argv[4]) +side_manifest = project_root / "runtime/policies/sealed-side-modules.v1.tsv" +side_paths = [ + line.split("\t", 1)[0] + for line in side_manifest.read_text(encoding="utf-8").splitlines() + if line and not line.startswith("#") +] +module_paths = ("bin/initdb", "bin/postgres", *side_paths) +records = [] +for relative in module_paths: + data = (root / relative).read_bytes() + records.append( + { + "path": relative, + "source-module-sha256": hashlib.sha256(data).hexdigest(), + "module-sha256": hashlib.sha256(data).hexdigest(), + "initial-pages": 1, + "maximum-pages": 4096, + "maximum-bytes": 268435456, + "shared": True, + "import-module": "env", + "import-name": "memory", + "transformation": "pinned-wasixcc-65536-to-embedded-4096-reversible-v1", + } + ) +records.sort(key=lambda record: record["path"]) + +def closure_hash(field): + digest = hashlib.sha256() + for value in ( + "oliphaunt.wasix-postmaster.linear-memory-install-closure.v1", + field, + ): + encoded = value.encode() + digest.update(len(encoded).to_bytes(8, "big")) + digest.update(encoded) + for record in records: + for value in (record["path"], record[field]): + encoded = value.encode() + digest.update(len(encoded).to_bytes(8, "big")) + digest.update(encoded) + return digest.hexdigest() + +receipt = { + "schema": "oliphaunt.wasix-postmaster.linear-memory-install.v1", + "profile-id": "oliphaunt.wasix-postmaster.linear-memory.wasm32-max256m-u64-static4g-guard2g.v1", + "address-width": "wasm32", + "supported-host-pointer-width": "u64", + "maximum-pages": 4096, + "maximum-bytes": 268435456, + "static-bound-pages": 65536, + "static-offset-guard-bytes": 2147483648, + "static-access-lowering": "wasmer-llvm-unchecked-reservation-and-guard-v1", + "requires-shared": True, + "requires-import": "env.memory", + "excludes-wasm32-end-wrap": True, + "predecessor-export-closure-receipt": predecessor.relative_to(root).as_posix(), + "predecessor-export-closure-receipt-sha256": hashlib.sha256(predecessor.read_bytes()).hexdigest(), + "source-module-closure-sha256": closure_hash("source-module-sha256"), + "module-closure-sha256": closure_hash("module-sha256"), + "module-count": len(records), + "modules": records, +} +with output.open("x", encoding="utf-8", newline="\n") as stream: + json.dump(receipt, stream, indent=2, sort_keys=True) + stream.write("\n") +PY +chmod 0444 "$sealed_export_receipt" "$linear_memory_receipt" +linear_memory_install_receipt_sha256="$(fresh_wasmer_bin_hash "$linear_memory_receipt")" + +installed_closure_sha256="$( + python3 "$project_root/lib/guest_build_provenance.py" \ + identity "$WASIX_INSTALL_DIR" +)" +{ + printf 'schema=oliphaunt.wasix-postmaster.guest-build.v4\n' + printf 'core_profile=release-o3\n' + printf 'guest_source_signature_sha256=%064d\n' 1 + printf 'installed_closure_sha256=%s\n' "$installed_closure_sha256" + printf 'child_backend=exec\n' + printf 'effective_cflags=-O3 -g0 -flto=thin\n' + printf 'effective_ldflags=-flto=thin\n' + printf 'effective_wasm_opt=yes\n' + printf 'effective_wasm_opt_flags=--converge:--strip-debug:--strip-producers\n' + printf 'effective_wasm_opt_suppress_default=yes\n' + printf 'atomic_fence_total=995\n' + printf 'atomic_fence_set_latch=2\n' + printf 'atomic_fence_reset_latch=1\n' + printf 'atomic_fence_wait_event_set_wait=1\n' + printf 'latch_state_contract=packed-atomic-v1\n' + printf 'final_wasm_concurrency_receipt_sha256=%s\n' \ + "$final_wasm_concurrency_receipt_sha256" + printf 'linear_memory_profile_id=%s\n' "$FRESH_LINEAR_MEMORY_PROFILE_ID" + printf 'linear_memory_install_receipt_sha256=%s\n' \ + "$linear_memory_install_receipt_sha256" + printf 'postgres_tag=%s\n' "$POSTGRES_TAG" + printf 'postgres_version=%s\n' "$POSTGRES_VERSION" + printf 'sysroot_variant=%s\n' "$WASIXCC_SYSROOT_VARIANT" +} >"$WASIX_INSTALL_DIR/guest-build.receipt" + +cargo_lock_sha256="$(printf test-cargo-lock | fresh_sha256_stream)" +runtime_abi_id="$(fresh_runtime_abi_id \ + "$cargo_lock_sha256" "$(fresh_host_arch | sed 's/linux-amd64/x86_64-unknown-linux-gnu/; s/linux-arm64/aarch64-unknown-linux-gnu/; s/darwin-amd64/x86_64-apple-darwin/; s/darwin-arm64/aarch64-apple-darwin/')" \ + "$(fresh_host_arch)" "$(fresh_host_abi)")" +target_triple="$(fresh_host_arch | sed 's/linux-amd64/x86_64-unknown-linux-gnu/; s/linux-arm64/aarch64-unknown-linux-gnu/; s/darwin-amd64/x86_64-apple-darwin/; s/darwin-arm64/aarch64-apple-darwin/')" + +{ + printf 'schema=oliphaunt.wasix-postmaster.wasmer-build.v2\n' + printf 'build_recipe_sha256=%s\n' "$(fresh_runtime_build_recipe_sha256)" + printf 'wasmer_source_commit=%s\n' "$FRESH_WASMER_SOURCE_COMMIT" + printf 'wasmer_napi_commit=%s\n' "$FRESH_WASMER_NAPI_COMMIT" + printf 'wasmer_test_files_commit=%s\n' "$FRESH_WASMER_TEST_FILES_COMMIT" + printf 'wasmer_spec_commit=%s\n' "$FRESH_WASMER_SPEC_COMMIT" + printf 'wasmer_patch_sha256=%s\n' "$(fresh_wasmer_bin_hash "$project_root/runtime/patches/wasmer/0001-postgres-wasix-blockers.patch")" + printf 'wasmer_prepared_signature_sha256=%064d\n' 0 + printf 'wasmer_cargo_lock_sha256=%s\n' "$cargo_lock_sha256" + printf 'wasmer_binary_sha256=%s\n' "$(fresh_wasmer_bin_hash "$FRESH_UPSTREAM_WASMER_BIN")" + printf 'wasmer_features=%s\n' "$FRESH_WASMER_COMPILER_FEATURES" + printf 'wasmer_headless_binary_sha256=%s\n' "$(fresh_wasmer_bin_hash "$FRESH_UPSTREAM_WASMER_HEADLESS_BIN")" + printf 'wasmer_headless_features=%s\n' "$FRESH_WASMER_HEADLESS_FEATURES" + printf 'runtime_abi_id=%s\n' "$runtime_abi_id" + printf 'artifact_abi_version=%s\n' "$FRESH_WASMER_ARTIFACT_ABI_VERSION" + printf 'wasix_libc_source_commit=%s\n' "$FRESH_WASIX_LIBC_SOURCE_COMMIT" + printf 'wasix_libc_patch_sha256=%s\n' "$(fresh_wasmer_bin_hash "$project_root/runtime/patches/wasix-libc/0001-postgres-wasix-blockers.patch")" + printf 'wasix_libc_prepared_signature_sha256=%064d\n' 0 + printf 'sysroot_carrier_manifest_sha256=%064d\n' 0 + printf 'sysroot_variant=%s\n' "$WASIXCC_SYSROOT_VARIANT" + printf 'sysroot_variant_manifest_sha256=%064d\n' 0 + printf 'host_platform=%s\n' "$(fresh_host_arch)" + printf 'host_abi=%s\n' "$(fresh_host_abi)" + printf 'rustc_host=%s\n' "$target_triple" + printf 'rustc_version=test-rustc\n' + printf 'llvm_version=22.1.0\n' +} >"$FRESH_WASMER_BUILD_RECEIPT" + +{ + printf 'schema=oliphaunt.wasix-postmaster.postmaster-executor-build.v3\n' + printf 'build_recipe_sha256=%s\n' "$(fresh_runtime_build_recipe_sha256)" + printf 'wasmer_build_receipt_sha256=%s\n' "$(fresh_wasmer_bin_hash "$FRESH_WASMER_BUILD_RECEIPT")" + printf 'wasmer_source_commit=%s\n' "$FRESH_WASMER_SOURCE_COMMIT" + printf 'wasmer_patch_sha256=%s\n' "$(fresh_manifest_value "$FRESH_WASMER_BUILD_RECEIPT" wasmer_patch_sha256)" + printf 'wasmer_prepared_signature_sha256=%s\n' "$(fresh_manifest_value "$FRESH_WASMER_BUILD_RECEIPT" wasmer_prepared_signature_sha256)" + printf 'wasmer_cargo_lock_sha256=%s\n' "$cargo_lock_sha256" + printf 'runtime_abi_id=%s\n' "$runtime_abi_id" + printf 'artifact_abi_version=%s\n' "$FRESH_WASMER_ARTIFACT_ABI_VERSION" + printf 'executor_package=%s\n' "$FRESH_POSTMASTER_EXECUTOR_PACKAGE" + printf 'executor_binary=%s\n' "$FRESH_POSTMASTER_EXECUTOR_BINARY" + printf 'executor_features=%s\n' "$FRESH_POSTMASTER_EXECUTOR_FEATURES" + printf 'executor_role=%s\n' "$FRESH_POSTMASTER_EXECUTOR_ROLE" + printf 'runtime_policy_id=%s\n' "$FRESH_POSTMASTER_EXECUTOR_RUNTIME_POLICY_ID" + printf 'cli_contract=%s\n' "$FRESH_POSTMASTER_EXECUTOR_CLI_CONTRACT" + printf 'executor_binary_sha256=%s\n' "$(fresh_wasmer_bin_hash "$FRESH_POSTMASTER_EXECUTOR_BIN")" + printf 'start_proof_binary=%s\n' "$FRESH_START_PROOF_BINARY" + printf 'start_proof_features=%s\n' "$FRESH_START_PROOF_FEATURES" + printf 'start_proof_policy=%s\n' "$FRESH_START_PROOF_POLICY" + printf 'start_proof_binary_sha256=%s\n' "$(fresh_wasmer_bin_hash "$FRESH_START_PROOF_BIN")" + printf 'memory_profile_binary=%s\n' "$FRESH_MEMORY_PROFILE_BINARY" + printf 'memory_profile_features=%s\n' "$FRESH_MEMORY_PROFILE_FEATURES" + printf 'linear_memory_profile_id=%s\n' "$FRESH_LINEAR_MEMORY_PROFILE_ID" + printf 'memory_profile_binary_sha256=%064d\n' 9 + printf 'postmaster_compiler_binary=%s\n' "$FRESH_POSTMASTER_COMPILER_BINARY" + printf 'postmaster_compiler_features=%s\n' "$FRESH_POSTMASTER_COMPILER_FEATURES" + printf 'compiler_cpu_policy=generic-baseline\n' + printf 'compiler_cpu_features=none\n' + printf 'postmaster_compiler_binary_sha256=%s\n' \ + "$(fresh_wasmer_bin_hash "$FRESH_POSTMASTER_COMPILER_BIN")" + printf 'host_platform=%s\n' "$(fresh_host_arch)" + printf 'host_abi=%s\n' "$(fresh_host_abi)" + printf 'rustc_host=%s\n' "$target_triple" + printf 'rustc_version=test-rustc\n' +} >"$FRESH_POSTMASTER_EXECUTOR_BUILD_RECEIPT" + +cache_bucket="$(fresh_wasmer_cache_dir "$FRESH_POSTMASTER_COMPILER_BIN")/compiled/$(fresh_wasmer_compiler_cache_bucket llvm aggressive "$FRESH_WASMER_ARTIFACT_ABI_VERSION")" +mkdir -p "$cache_bucket" +for relative in \ + bin/initdb \ + bin/postgres \ + lib/libpq.so.5.18 \ + lib/postgresql/dict_snowball.so \ + lib/postgresql/plpgsql.so +do + module="$WASIX_INSTALL_DIR/$relative" + module_hash="$(fresh_wasmer_module_hash "$module")" + "$FRESH_POSTMASTER_COMPILER_BIN" \ + --llvm --llvm-opt-level aggressive --compiler-threads 1 \ + --enable-exceptions --enable-threads \ + -o "$cache_bucket/$module_hash.bin" "$module" +done + +cp "$WASIX_INSTALL_DIR/share/postgresql/postgresql.conf.sample" \ + "$test_root/postgresql.conf.sample.saved" +printf 'stale-install-mutation\n' \ + >>"$WASIX_INSTALL_DIR/share/postgresql/postgresql.conf.sample" +if "$project_root/bin/build-sealed-headless-carrier.sh" \ + --output "$test_root/stale-guest-receipt-carrier" \ + --cache-bucket "$cache_bucket" >/dev/null 2>&1 +then + printf 'carrier builder accepted guest bytes differing from their build receipt\n' >&2 + exit 1 +fi +mv "$test_root/postgresql.conf.sample.saved" \ + "$WASIX_INSTALL_DIR/share/postgresql/postgresql.conf.sample" + +postgres_hash="$(fresh_wasmer_module_hash "$WASIX_INSTALL_DIR/bin/postgres")" +mv "$cache_bucket/$postgres_hash.bin" "$test_root/postgres-aot.saved" +if "$project_root/bin/build-sealed-headless-carrier.sh" \ + --output "$test_root/missing-artifact-carrier" \ + --cache-bucket "$cache_bucket" >/dev/null 2>&1 +then + printf 'carrier builder compiled or ignored a missing AOT artifact\n' >&2 + exit 1 +fi +mv "$test_root/postgres-aot.saved" "$cache_bucket/$postgres_hash.bin" + +plpgsql_hash="$(fresh_wasmer_module_hash "$WASIX_INSTALL_DIR/lib/postgresql/plpgsql.so")" +mv "$cache_bucket/$plpgsql_hash.bin" "$test_root/plpgsql-aot.saved" +printf 'wrong-plan-or-module-fixture\n' >"$cache_bucket/$plpgsql_hash.bin" +if "$project_root/bin/build-sealed-headless-carrier.sh" \ + --output "$test_root/invalid-inactive-aot-carrier" \ + --cache-bucket "$cache_bucket" >/dev/null 2>&1 +then + printf 'carrier builder accepted an invalid inactive side-module AOT artifact\n' >&2 + exit 1 +fi +mv "$test_root/plpgsql-aot.saved" "$cache_bucket/$plpgsql_hash.bin" + +if FAKE_WASMER_NONDETERMINISTIC=1 \ + "$project_root/bin/build-sealed-headless-carrier.sh" \ + --output "$test_root/nondeterministic-memory-carrier" \ + --cache-bucket "$cache_bucket" >/dev/null 2>&1 +then + printf 'carrier builder accepted divergent independent memory captures\n' >&2 + exit 1 +fi +[ ! -e "$test_root/nondeterministic-memory-carrier" ] + +if FAKE_WASMER_RECEIPT_MISMATCH=1 \ + "$project_root/bin/build-sealed-headless-carrier.sh" \ + --output "$test_root/nondeterministic-receipt-carrier" \ + --cache-bucket "$cache_bucket" >/dev/null 2>&1 +then + printf 'carrier builder accepted divergent independent memory receipts\n' >&2 + exit 1 +fi +[ ! -e "$test_root/nondeterministic-receipt-carrier" ] + +if FAKE_WASMER_INVALID_RECEIPT=1 \ + "$project_root/bin/build-sealed-headless-carrier.sh" \ + --output "$test_root/invalid-receipt-carrier" \ + --cache-bucket "$cache_bucket" >/dev/null 2>&1 +then + printf 'carrier builder accepted a self-consistent but invalid memory receipt\n' >&2 + exit 1 +fi +[ ! -e "$test_root/invalid-receipt-carrier" ] + +if FAKE_START_PROOF_INVALID=1 \ + "$project_root/bin/build-sealed-headless-carrier.sh" \ + --output "$test_root/invalid-start-proof-carrier" \ + --cache-bucket "$cache_bucket" >/dev/null 2>&1 +then + printf 'carrier builder accepted analyzer output outside the restricted policy\n' >&2 + exit 1 +fi +[ ! -e "$test_root/invalid-start-proof-carrier" ] + +if FAKE_START_PROOF_WRONG_MODULE=1 \ + "$project_root/bin/build-sealed-headless-carrier.sh" \ + --output "$test_root/wrong-module-start-proof-carrier" \ + --cache-bucket "$cache_bucket" >/dev/null 2>&1 +then + printf 'carrier builder accepted analyzer output bound to another module\n' >&2 + exit 1 +fi +[ ! -e "$test_root/wrong-module-start-proof-carrier" ] + +: >"$FAKE_WASMER_VALIDATION_LOG" +failed_validation_output="$test_root/failed-initdb-carrier" +if FAKE_WASMER_FAIL_FINAL_INITDB=1 \ + "$project_root/bin/build-sealed-headless-carrier.sh" \ + --output "$failed_validation_output" \ + --cache-bucket "$cache_bucket" >/dev/null 2>&1 +then + printf 'carrier builder published after the final initdb lifecycle failed\n' >&2 + exit 1 +fi +[ ! -e "$failed_validation_output" ] +if find "$test_root" -maxdepth 1 -type d \ + \( -name '.failed-initdb-carrier.tmp.*' -o -name '.failed-initdb-carrier.validate.*' \) \ + -print -quit | grep -q . +then + printf 'carrier builder left staging or validation state after initdb failure\n' >&2 + exit 1 +fi +python3 - "$FAKE_WASMER_VALIDATION_LOG" <<'PY' +import json +import os +import sys + +with open(sys.argv[1], encoding="utf-8") as stream: + records = [json.loads(line) for line in stream] +assert [record["program"] for record in records] == ["postgres", "initdb"] +assert records[0]["arguments"] == ["--version"] +assert records[1]["arguments"] != ["--version"] +for record in records: + for volume in record["volumes"]: + host, guest = volume.rsplit(":", 1) + if guest in {"/pgdata", "/dev/shm"}: + assert not os.path.exists(host), (guest, host) +PY + +: >"$FAKE_WASMER_CAPTURE_LOG" +: >"$FAKE_WASMER_VALIDATION_LOG" + +if FRESH_PINNED_WASMER_CACHE_DIR="$test_root/foreign-pinned-cache" \ + "$project_root/bin/build-sealed-headless-carrier.sh" \ + --output "$test_root/pinned-cache-carrier" \ + --cache-bucket "$cache_bucket" >/dev/null 2>&1 +then + printf 'carrier builder admitted a pinned or foreign AOT cache root\n' >&2 + exit 1 +fi +[ ! -e "$test_root/pinned-cache-carrier" ] + +output="$test_root/carrier" +"$project_root/bin/build-sealed-headless-carrier.sh" \ + --output "$output" \ + --cache-bucket "$cache_bucket" + +for required in \ + bin/wasmer-headless \ + bin/initdb \ + bin/postgres \ + lib/libpq.so \ + lib/libpq.so.5 \ + lib/libpq.so.5.18 \ + lib/postgresql/dict_snowball.so \ + lib/postgresql/plpgsql.so \ + share/postgresql/postgresql.conf.sample \ + share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json \ + share/postgresql/wasix-postmaster.sealed-export.structure.receipt \ + guest-build.receipt \ + manifest.json \ + payload.files \ + postmaster-executor.receipt \ + wasmer-build.receipt +do + [ -f "$output/$required" ] && [ ! -L "$output/$required" ] || { + printf 'missing regular carrier test output: %s\n' "$required" >&2 + exit 1 + } +done +[ "$(find "$output/aot" -type f -name '*.bin' | wc -l | tr -d '[:space:]')" -eq 5 ] +[ "$(find "$output/memory" -type f -name '*.bin' | wc -l | tr -d '[:space:]')" -eq 2 ] +[ "$(find "$output/memory" -type f -name '*.receipt.json' | wc -l | tr -d '[:space:]')" -eq 2 ] +[ "$(wc -l <"$FAKE_WASMER_CAPTURE_LOG" | tr -d '[:space:]')" -eq 4 ] +[ "$(grep -c '^initdb[[:space:]]' "$FAKE_WASMER_CAPTURE_LOG")" -eq 2 ] +[ "$(grep -c '^postgres[[:space:]]' "$FAKE_WASMER_CAPTURE_LOG")" -eq 2 ] +[ "$(wc -l <"$FAKE_WASMER_VALIDATION_LOG" | tr -d '[:space:]')" -eq 2 ] +[ "$(stat -c %a "$output" 2>/dev/null || stat -f %Lp "$output")" = 555 ] +[ "$(stat -c %a "$output/share/postgresql/postgresql.conf.sample" 2>/dev/null || stat -f %Lp "$output/share/postgresql/postgresql.conf.sample")" = 444 ] + +python3 - "$output" <<'PY' +import hashlib +import json +import os +import sys + +root = sys.argv[1] +with open(os.path.join(root, "manifest.json"), encoding="utf-8") as stream: + manifest = json.load(stream) +assert manifest["format-version"] == 6 +assert manifest["schema"] == "oliphaunt.wasix-postmaster.sealed-aot.v5" +assert manifest["core-profile"] == "release-o3" +assert manifest["file-cache-policy"] == { + "requested-policy-id": "oliphaunt.wasix-postmaster.file-cache.adaptive-linux.v5", + "approved-config-id": "oliphaunt.wasix-postmaster.file-cache.adaptive-linux.embedded-v4", + "config-sha256": "01668b856435cb8c34b2d2324ab55b7f1f5961b8b403c1ee49d9ee4b5c865f53", + "portable-fallback-mode": "observe-only", +} +with open(os.path.join(root, "guest-build.receipt"), "rb") as stream: + guest_build_receipt = stream.read() +assert manifest["guest-build-recipe-sha256"] == hashlib.sha256( + guest_build_receipt +).hexdigest() +assert manifest["entrypoint"] == "runtime:postgres" +linear_profile = manifest["linear-memory-profile"] +assert linear_profile == { + "id": "oliphaunt.wasix-postmaster.linear-memory.wasm32-max256m-u64-static4g-guard2g.v1", + "address-width": "wasm32", + "supported-host-pointer-width": "u64", + "maximum-pages": 4096, + "maximum-bytes": 268435456, + "static-bound-pages": 65536, + "static-offset-guard-bytes": 2147483648, + "static-access-lowering": "wasmer-llvm-unchecked-reservation-and-guard-v1", + "install-receipt-path": "share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json", + "install-receipt-sha256": hashlib.sha256( + open( + os.path.join( + root, + "share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json", + ), + "rb", + ).read() + ).hexdigest(), +} +with open(os.path.join(root, "postmaster-executor.receipt"), encoding="utf-8") as stream: + executor_receipt = dict(line.rstrip("\n").split("=", 1) for line in stream) +assert executor_receipt["schema"] == "oliphaunt.wasix-postmaster.postmaster-executor-build.v3" +assert executor_receipt["linear_memory_profile_id"] == linear_profile["id"] +assert executor_receipt["executor_role"] == "postmaster-product" +assert executor_receipt["executor_binary_sha256"] == manifest["executor-sha256"] +assert len(manifest["artifacts"]) == 5 +assert {tuple(item["exec-aliases"]) for item in manifest["artifacts"] if item["kind"] == "executable"} == { + ("/bin/initdb",), + ("/bin/postgres",), +} +expected_receipt_keys = { + "schema", + "module-sha256", + "runtime-abi-id", + "phase", + "mapping-alignment", + "mapped-size", + "memory-minimum-pages", + "memory-maximum-pages", + "memory-shared", + "memory-base", + "dylink-memory-size", + "dylink-memory-alignment", + "stack-low", + "deterministic-start-proof", + "deterministic-start-proof-output-sha256", +} +for artifact in manifest["artifacts"]: + assert artifact["linear-memory"] == { + "profile-id": linear_profile["id"], + "source-module-sha256": artifact["linear-memory"]["source-module-sha256"], + "install-receipt-sha256": linear_profile["install-receipt-sha256"], + } + memory = artifact.get("preinitialized-memory") + if artifact["kind"] == "side-module": + assert memory is None + continue + assert set(memory) == {"path", "size", "sha256"}.union(expected_receipt_keys) + assert memory["schema"] == "oliphaunt.wasix-postmaster.memory-image.v2" + assert memory["module-sha256"] == artifact["module-sha256"] + assert memory["runtime-abi-id"] == manifest["runtime-abi-id"] + assert memory["phase"] == "post-module-start-pre-link-relocations-v1" + assert memory["mapping-alignment"] == 65536 + assert memory["mapped-size"] == memory["size"] == 65536 + assert memory["memory-shared"] is True + assert memory["memory-maximum-pages"] == 4096 + proof = memory["deterministic-start-proof"] + assert proof["module-sha256"] == artifact["module-sha256"] + assert proof["analyzer-policy"] == "llvm-shared-memory-init-restricted-effects.v1" + canonical_proof = json.dumps( + proof, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + assert memory["deterministic-start-proof-output-sha256"] == hashlib.sha256( + canonical_proof + ).hexdigest() + image_path = os.path.join(root, memory["path"]) + assert os.path.getsize(image_path) == memory["size"] + with open(image_path, "rb") as image: + assert hashlib.sha256(image.read()).hexdigest() == memory["sha256"] + receipt_path = os.path.join( + root, + "memory", + f"{artifact['module-sha256'].upper()}.receipt.json", + ) + with open(receipt_path, encoding="utf-8") as stream: + receipt = json.load(stream) + assert receipt == {key: memory[key] for key in expected_receipt_keys} +with open(os.path.join(root, "payload.files"), encoding="utf-8") as stream: + assert stream.readline().strip() == "schema=oliphaunt.wasix-postmaster.payload-files.v1" + listed_payloads = set() + for line in stream: + digest, size, relative = line.rstrip("\n").split("\t") + assert relative not in listed_payloads + listed_payloads.add(relative) + path = os.path.join(root, relative) + assert os.path.getsize(path) == int(size) + with open(path, "rb") as payload: + assert hashlib.sha256(payload.read()).hexdigest() == digest +expected_payloads = set() +for current, dirs, files in os.walk(root): + dirs.sort() + files.sort() + for name in files: + relative = os.path.relpath(os.path.join(current, name), root) + if relative != "payload.files": + expected_payloads.add(relative) +assert listed_payloads == expected_payloads +assert not any(path.startswith(".") for path in listed_payloads) +PY + +python3 - "$FAKE_WASMER_VALIDATION_LOG" <<'PY' +import json +import os +import sys + +with open(sys.argv[1], encoding="utf-8") as stream: + records = [json.loads(line) for line in stream] +assert [record["program"] for record in records] == ["postgres", "initdb"] +assert records[0]["arguments"] == ["--version"] +assert records[1]["arguments"] == [ + "-D", + "/pgdata", + "-A", + "trust", + "--no-locale", + "--encoding=UTF8", + "--no-instructions", +] +for record in records: + guest_volumes = {} + for volume in record["volumes"]: + host, guest = volume.rsplit(":", 1) + assert guest not in guest_volumes + guest_volumes[guest] = host + carrier_root = next(host for guest, host in guest_volumes.items() if guest == host) + assert guest_volumes["/lib"] == os.path.join(carrier_root, "lib") + assert guest_volumes["/share"] == os.path.join(carrier_root, "share") + for guest in ("/pgdata", "/dev/shm"): + assert not os.path.exists(guest_volumes[guest]), (guest, guest_volumes[guest]) +PY + +if find "$test_root" -maxdepth 1 -type d \ + \( -name '.carrier.tmp.*' -o -name '.carrier.validate.*' \) \ + -print -quit | grep -q . +then + printf 'carrier builder left staging or validation state after success\n' >&2 + exit 1 +fi + +manifest_source_fingerprint="$(python3 - "$output/manifest.json" <<'PY' +import json +import sys +with open(sys.argv[1], encoding="utf-8") as stream: + print(json.load(stream)["source-fingerprint"]) +PY +)" +manifest_producer_recipe="$(python3 - "$output/manifest.json" <<'PY' +import json +import sys +with open(sys.argv[1], encoding="utf-8") as stream: + print(json.load(stream)["producer-recipe-sha256"]) +PY +)" +compiler_config="$(fresh_wasmer_compiler_cache_bucket \ + llvm aggressive "$FRESH_WASMER_ARTIFACT_ABI_VERSION")" +expected_producer_recipe="$(fresh_aot_producer_recipe_sha256 \ + "$output/wasmer-build.receipt" \ + "$output/postmaster-executor.receipt" \ + "$compiler_config" \ + "$target_triple" \ + "$manifest_source_fingerprint")" +[ "$manifest_producer_recipe" = "$expected_producer_recipe" ] || { + printf 'manifest AOT producer recipe is not reproducible from packaged inputs\n' >&2 + exit 1 +} +recipe_fixture="$test_root/producer-recipe-fixture" +mkdir -p "$recipe_fixture/bin" "$recipe_fixture/lib" +cp "$project_root/bin/precompile-wasix-core.sh" "$recipe_fixture/bin/" +cp "$project_root/bin/build-sealed-headless-carrier.sh" "$recipe_fixture/bin/" +cp "$project_root/lib/sealed-carrier.sh" "$recipe_fixture/lib/" +cp "$project_root/lib/verify-sealed-carrier.py" "$recipe_fixture/lib/" +cp "$project_root/lib/sealed_export_chain.py" "$recipe_fixture/lib/" +fixture_producer_recipe="$(FRESH_ROOT="$recipe_fixture" \ + fresh_aot_producer_recipe_sha256 \ + "$output/wasmer-build.receipt" \ + "$output/postmaster-executor.receipt" \ + "$compiler_config" \ + "$target_triple" \ + "$manifest_source_fingerprint")" +[ "$manifest_producer_recipe" = "$fixture_producer_recipe" ] || { + printf 'AOT producer recipe depends on paths outside its declared inputs\n' >&2 + exit 1 +} +printf '# verifier policy mutation\n' >>"$recipe_fixture/lib/verify-sealed-carrier.py" +[ "$manifest_producer_recipe" != "$(FRESH_ROOT="$recipe_fixture" \ + fresh_aot_producer_recipe_sha256 \ + "$output/wasmer-build.receipt" \ + "$output/postmaster-executor.receipt" \ + "$compiler_config" \ + "$target_triple" \ + "$manifest_source_fingerprint")" ] || { + printf 'AOT producer recipe does not bind the carrier verifier policy\n' >&2 + exit 1 +} +cp "$project_root/lib/verify-sealed-carrier.py" "$recipe_fixture/lib/" +printf '# export chain policy mutation\n' >>"$recipe_fixture/lib/sealed_export_chain.py" +[ "$manifest_producer_recipe" != "$(FRESH_ROOT="$recipe_fixture" \ + fresh_aot_producer_recipe_sha256 \ + "$output/wasmer-build.receipt" \ + "$output/postmaster-executor.receipt" \ + "$compiler_config" \ + "$target_triple" \ + "$manifest_source_fingerprint")" ] || { + printf 'AOT producer recipe does not bind sealed export lineage policy\n' >&2 + exit 1 +} +[ "$manifest_producer_recipe" != "$(fresh_manifest_value "$output/wasmer-build.receipt" build_recipe_sha256)" ] || { + printf 'AOT producer recipe collapsed to the runtime build recipe\n' >&2 + exit 1 +} +different_source_fingerprint="$(printf different-source | fresh_sha256_stream)" +[ "$manifest_producer_recipe" != "$(fresh_aot_producer_recipe_sha256 \ + "$output/wasmer-build.receipt" \ + "$output/postmaster-executor.receipt" \ + "$compiler_config" \ + "$target_triple" \ + "$different_source_fingerprint")" ] || { + printf 'AOT producer recipe does not bind the guest source fingerprint\n' >&2 + exit 1 +} +sed 's/^rustc_version=.*/rustc_version=alternate-test-rustc/' \ + "$output/wasmer-build.receipt" >"$test_root/alternate-wasmer-build.receipt" +[ "$manifest_producer_recipe" != "$(fresh_aot_producer_recipe_sha256 \ + "$test_root/alternate-wasmer-build.receipt" \ + "$output/postmaster-executor.receipt" \ + "$compiler_config" \ + "$target_triple" \ + "$manifest_source_fingerprint")" ] || { + printf 'AOT producer recipe does not bind the canonical build receipt\n' >&2 + exit 1 +} +[ "$manifest_producer_recipe" != "$(fresh_aot_producer_recipe_sha256 \ + "$output/wasmer-build.receipt" \ + "$output/postmaster-executor.receipt" \ + "${compiler_config}-different" \ + "$target_triple" \ + "$manifest_source_fingerprint")" ] || { + printf 'AOT producer recipe does not bind the compiler configuration\n' >&2 + exit 1 +} +[ "$manifest_producer_recipe" != "$(WASMER_STACK_SIZE=16777216 \ + fresh_aot_producer_recipe_sha256 \ + "$output/wasmer-build.receipt" \ + "$output/postmaster-executor.receipt" \ + "$compiler_config" \ + "$target_triple" \ + "$manifest_source_fingerprint")" ] || { + printf 'AOT producer recipe does not bind the memory capture stack size\n' >&2 + exit 1 +} +if fresh_aot_producer_recipe_sha256 \ + "$output/wasmer-build.receipt" \ + "$output/postmaster-executor.receipt" \ + "$compiler_config" \ + different-target \ + "$manifest_source_fingerprint" >/dev/null 2>&1 +then + printf 'AOT producer recipe accepted a target inconsistent with its receipt\n' >&2 + exit 1 +fi + +if "$project_root/bin/build-sealed-headless-carrier.sh" \ + --output "$output" \ + --cache-bucket "$cache_bucket" >/dev/null 2>&1 +then + printf 'carrier builder replaced an existing output unexpectedly\n' >&2 + exit 1 +fi + +"$project_root/bin/verify-sealed-headless-carrier.sh" "$output" >/dev/null +[ "$(python3 "$project_root/lib/verify-sealed-carrier.py" executor-selection "$output")" = \ + $'postmaster-product\tpostmaster-executor.receipt\t'"$(fresh_wasmer_bin_hash "$output/postmaster-executor.receipt")"$'\t'"$(fresh_wasmer_bin_hash "$output/bin/wasmer-headless")" ] + +full_control_output="$test_root/full-headless-control-carrier" +"$project_root/bin/build-sealed-headless-carrier.sh" \ + --executor-role full-headless \ + --output "$full_control_output" \ + --cache-bucket "$cache_bucket" >/dev/null +[ ! -e "$full_control_output/postmaster-executor.receipt" ] +[ -f "$full_control_output/postmaster-compiler.receipt" ] && \ + [ ! -L "$full_control_output/postmaster-compiler.receipt" ] +"$project_root/bin/verify-sealed-headless-carrier.sh" "$full_control_output" >/dev/null +[ "$(python3 "$project_root/lib/verify-sealed-carrier.py" executor-selection "$full_control_output")" = \ + $'full-headless\twasmer-build.receipt\t'"$(fresh_wasmer_bin_hash "$full_control_output/wasmer-build.receipt")"$'\t'"$(fresh_wasmer_bin_hash "$full_control_output/bin/wasmer-headless")" ] + +current_evidence="$test_root/current-evidence.json" +"$project_root/bin/current-evidence-manifest.py" write \ + --carrier "$output" \ + --output "$current_evidence" >/dev/null +"$project_root/bin/current-evidence-manifest.py" verify \ + --carrier "$output" \ + --output "$current_evidence" >/dev/null +if "$project_root/bin/current-evidence-manifest.py" write \ + --carrier "$output" \ + --output "$output/current-evidence.json" >/dev/null 2>&1 +then + printf 'current-evidence writer mutated the sealed carrier\n' >&2 + exit 1 +fi +if "$project_root/bin/current-evidence-manifest.py" write \ + --carrier "$output" \ + --output "$current_evidence" >/dev/null 2>&1 +then + printf 'current-evidence writer replaced an existing output\n' >&2 + exit 1 +fi +qualified_evidence="$test_root/forged-qualified-evidence.json" +python3 - "$current_evidence" "$qualified_evidence" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as source: + evidence = json.load(source) +evidence["qualification-status"] = "qualified" +with open(sys.argv[2], "w", encoding="utf-8", newline="\n") as output: + json.dump(evidence, output, indent=2, sort_keys=True) + output.write("\n") +PY +if "$project_root/bin/current-evidence-manifest.py" verify \ + --carrier "$output" \ + --output "$qualified_evidence" >/dev/null 2>&1 +then + printf 'current-evidence verifier accepted a forged qualification status\n' >&2 + exit 1 +fi + +# The implicit publication path is derived from the finished exact payload +# inventory, not merely from the runtime ABI. This keeps distinct PostgreSQL +# build profiles from racing for or aliasing one default directory. +default_build_log="$test_root/default-build.log" +"$project_root/bin/build-sealed-headless-carrier.sh" \ + --cache-bucket "$cache_bucket" >"$default_build_log" +default_output="$(sed -n 's/^built sealed headless WASIX PostgreSQL carrier: //p' "$default_build_log")" +[ -n "$default_output" ] && [ -d "$default_output" ] || { + printf 'default carrier output was not published\n' >&2 + exit 1 +} +default_payload_sha256="$(fresh_wasmer_bin_hash "$default_output/payload.files")" +expected_default_output="$FRESH_WORK_ROOT/carriers/wasix-postmaster-$POSTGRES_VERSION-${runtime_abi_id:0:16}-$default_payload_sha256" +[ "$default_output" = "$expected_default_output" ] || { + printf 'default carrier output is not content-addressed: expected %s, got %s\n' \ + "$expected_default_output" "$default_output" >&2 + exit 1 +} +grep -Fx "payload inventory SHA-256: $default_payload_sha256" "$default_build_log" >/dev/null +"$project_root/bin/verify-sealed-headless-carrier.sh" "$default_output" >/dev/null +if "$project_root/bin/build-sealed-headless-carrier.sh" \ + --cache-bucket "$cache_bucket" >/dev/null 2>&1 +then + printf 'default carrier builder replaced an existing content identity\n' >&2 + exit 1 +fi + +expect_verifier_failure() { + local label="$1" + local carrier="$2" + + if "$project_root/bin/verify-sealed-headless-carrier.sh" "$carrier" \ + >"$test_root/$label.stdout" 2>"$test_root/$label.stderr" + then + printf 'sealed carrier verifier accepted %s\n' "$label" >&2 + exit 1 + fi +} + +reindex_carrier() { + local carrier="$1" + + chmod u+w "$carrier/payload.files" + python3 - "$carrier" <<'PY' +import hashlib +import os +import stat +import sys + +root = os.path.realpath(sys.argv[1]) +inventory = os.path.join(root, "payload.files") +rows = [] +for current, dirs, files in os.walk(root, followlinks=False): + dirs.sort() + files.sort() + for name in files: + path = os.path.join(current, name) + if os.path.realpath(path) == inventory: + continue + info = os.lstat(path) + if not stat.S_ISREG(info.st_mode): + continue + digest = hashlib.sha256() + with open(path, "rb", buffering=0) as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + rows.append((os.path.relpath(path, root), info.st_size, digest.hexdigest())) +with open(inventory, "w", encoding="utf-8", newline="\n") as output: + output.write("schema=oliphaunt.wasix-postmaster.payload-files.v1\n") + for relative, size, digest in sorted(rows): + output.write(f"{digest}\t{size}\t{relative}\n") +PY + chmod 0444 "$carrier/payload.files" +} + +legacy_manifest="$test_root/verifier-legacy-manifest-v4" +cp -a "$output" "$legacy_manifest" +chmod u+w "$legacy_manifest/manifest.json" +python3 - "$legacy_manifest/manifest.json" <<'PY' +import json +import sys + +path = sys.argv[1] +with open(path, encoding="utf-8") as stream: + manifest = json.load(stream) +manifest["schema"] = "oliphaunt.wasix-postmaster.sealed-aot.v4" +manifest["format-version"] = 5 +with open(path, "w", encoding="utf-8", newline="\n") as stream: + json.dump(manifest, stream, ensure_ascii=False, indent=2) + stream.write("\n") +PY +chmod 0444 "$legacy_manifest/manifest.json" +reindex_carrier "$legacy_manifest" +expect_verifier_failure legacy-manifest-v4 "$legacy_manifest" + +legacy_guest="$test_root/verifier-legacy-guest-v3" +cp -a "$output" "$legacy_guest" +chmod u+w "$legacy_guest/guest-build.receipt" "$legacy_guest/manifest.json" +sed 's/^schema=oliphaunt.wasix-postmaster.guest-build.v4$/schema=oliphaunt.wasix-postmaster.guest-build.v3/' \ + "$output/guest-build.receipt" >"$legacy_guest/guest-build.receipt" +python3 - "$legacy_guest" <<'PY' +import hashlib +import json +import os +import sys + +root = sys.argv[1] +guest = os.path.join(root, "guest-build.receipt") +manifest_path = os.path.join(root, "manifest.json") +with open(guest, "rb") as stream: + digest = hashlib.sha256(stream.read()).hexdigest() +with open(manifest_path, encoding="utf-8") as stream: + manifest = json.load(stream) +manifest["guest-build-recipe-sha256"] = digest +with open(manifest_path, "w", encoding="utf-8", newline="\n") as stream: + json.dump(manifest, stream, ensure_ascii=False, indent=2) + stream.write("\n") +PY +chmod 0444 "$legacy_guest/guest-build.receipt" "$legacy_guest/manifest.json" +reindex_carrier "$legacy_guest" +expect_verifier_failure legacy-guest-v3 "$legacy_guest" + +tampered="$test_root/verifier-tampered" +cp -a "$output" "$tampered" +chmod u+w "$tampered/bin/postgres" +printf 'tampered\n' >>"$tampered/bin/postgres" +chmod 0555 "$tampered/bin/postgres" +expect_verifier_failure tampered-payload "$tampered" + +missing="$test_root/verifier-missing" +cp -a "$output" "$missing" +chmod u+w "$missing/share/postgresql" +rm "$missing/share/postgresql/postgresql.conf.sample" +chmod 0555 "$missing/share/postgresql" +expect_verifier_failure missing-payload "$missing" + +unexpected="$test_root/verifier-unexpected" +cp -a "$output" "$unexpected" +chmod u+w "$unexpected" +printf 'unexpected\n' >"$unexpected/unexpected.txt" +chmod 0444 "$unexpected/unexpected.txt" +chmod 0555 "$unexpected" +expect_verifier_failure unexpected-payload "$unexpected" + +symlinked="$test_root/verifier-symlink" +cp -a "$output" "$symlinked" +chmod u+w "$symlinked" +ln -s bin/postgres "$symlinked/postgres-link" +chmod 0555 "$symlinked" +expect_verifier_failure symlink-entry "$symlinked" + +special="$test_root/verifier-special" +cp -a "$output" "$special" +chmod u+w "$special" +mkfifo "$special/unexpected.fifo" +chmod 0555 "$special" +expect_verifier_failure special-entry "$special" + +empty_directory="$test_root/verifier-empty-directory" +cp -a "$output" "$empty_directory" +chmod u+w "$empty_directory" +mkdir "$empty_directory/unrepresented-directory" +chmod 0555 "$empty_directory/unrepresented-directory" "$empty_directory" +expect_verifier_failure unrepresented-directory "$empty_directory" + +writable_file="$test_root/verifier-writable-file" +cp -a "$output" "$writable_file" +chmod u+w "$writable_file/bin/postgres" +expect_verifier_failure writable-file "$writable_file" + +writable_directory="$test_root/verifier-writable-directory" +cp -a "$output" "$writable_directory" +chmod u+w "$writable_directory/aot" +expect_verifier_failure writable-directory "$writable_directory" + +unsafe_inventory="$test_root/verifier-unsafe-inventory" +cp -a "$output" "$unsafe_inventory" +chmod u+w "$unsafe_inventory/payload.files" +printf '%064d\t0\t../outside\n' 0 >>"$unsafe_inventory/payload.files" +chmod 0444 "$unsafe_inventory/payload.files" +expect_verifier_failure unsafe-inventory-path "$unsafe_inventory" + +wrong_executor="$test_root/verifier-wrong-executor" +cp -a "$output" "$wrong_executor" +chmod u+w "$wrong_executor/bin/wasmer-headless" +printf 'different executor\n' >>"$wrong_executor/bin/wasmer-headless" +chmod 0555 "$wrong_executor/bin/wasmer-headless" +reindex_carrier "$wrong_executor" +expect_verifier_failure headless-receipt-identity "$wrong_executor" + +wrong_manifest="$test_root/verifier-wrong-manifest" +cp -a "$output" "$wrong_manifest" +chmod u+w "$wrong_manifest/manifest.json" +python3 - "$wrong_manifest/manifest.json" <<'PY' +import json +import sys + +path = sys.argv[1] +with open(path, encoding="utf-8") as stream: + manifest = json.load(stream) +manifest["executor-sha256"] = "0" * 64 +with open(path, "w", encoding="utf-8", newline="\n") as stream: + json.dump(manifest, stream, ensure_ascii=False, indent=2) + stream.write("\n") +PY +chmod 0444 "$wrong_manifest/manifest.json" +reindex_carrier "$wrong_manifest" +expect_verifier_failure manifest-executor-identity "$wrong_manifest" + +wrong_receipt="$test_root/verifier-wrong-receipt" +cp -a "$output" "$wrong_receipt" +chmod u+w "$wrong_receipt/wasmer-build.receipt" +sed 's/^wasmer_headless_binary_sha256=.*/wasmer_headless_binary_sha256=0000000000000000000000000000000000000000000000000000000000000000/' \ + "$output/wasmer-build.receipt" >"$wrong_receipt/wasmer-build.receipt" +chmod 0444 "$wrong_receipt/wasmer-build.receipt" +reindex_carrier "$wrong_receipt" +expect_verifier_failure receipt-headless-identity "$wrong_receipt" + +wrong_product_receipt="$test_root/verifier-wrong-product-receipt" +cp -a "$output" "$wrong_product_receipt" +chmod u+w "$wrong_product_receipt/postmaster-executor.receipt" +sed 's/^executor_binary_sha256=.*/executor_binary_sha256=0000000000000000000000000000000000000000000000000000000000000000/' \ + "$output/postmaster-executor.receipt" \ + >"$wrong_product_receipt/postmaster-executor.receipt" +chmod 0444 "$wrong_product_receipt/postmaster-executor.receipt" +reindex_carrier "$wrong_product_receipt" +expect_verifier_failure product-receipt-executor-identity "$wrong_product_receipt" + +missing_product_receipt="$test_root/verifier-missing-product-receipt" +cp -a "$output" "$missing_product_receipt" +chmod u+w "$missing_product_receipt" +rm "$missing_product_receipt/postmaster-executor.receipt" +chmod 0555 "$missing_product_receipt" +reindex_carrier "$missing_product_receipt" +expect_verifier_failure missing-product-role-sidecar "$missing_product_receipt" + +printf 'sealed headless carrier packaging tests passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-wasix-core.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-wasix-core.sh new file mode 100755 index 00000000..d0e72f9e --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-wasix-core.sh @@ -0,0 +1,723 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +configure_only=0 +force_clean=0 +while [ "$#" -gt 0 ]; do + case "$1" in + --configure-only) + configure_only=1 + ;; + --clean) + force_clean=1 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + exit 2 + ;; + esac + shift +done + +managed_work_probe="$FRESH_WORK_ROOT/.managed-path-boundary" +fresh_require_managed_generated_path "$managed_work_probe" FRESH_WORK_ROOT +fresh_require_managed_generated_path "$WASIX_BUILD_DIR" WASIX_BUILD_DIR +fresh_require_managed_generated_path "$WASIX_INSTALL_DIR" WASIX_INSTALL_DIR +fresh_require_managed_generated_path "$REPORT_DIR" REPORT_DIR +fresh_require_managed_generated_path "$RUN_DIR" RUN_DIR + +fresh_ensure_dirs +fresh_require_command flock +fresh_require_command git +fresh_require_command python3 +fresh_require_command shasum +fresh_require_patched_wasixcc_sysroot + +durable_publication="$FRESH_ROOT/lib/durable_publication.py" +[ -f "$durable_publication" ] && [ ! -L "$durable_publication" ] || { + printf 'missing regular durable-publication helper: %s\n' "$durable_publication" >&2 + exit 2 +} + +if [ -n "${FRESH_PINNED_WASIX_INSTALL_DIR:-}" ] && [ "$WASIX_INSTALL_DIR" = "$FRESH_PINNED_WASIX_INSTALL_DIR" ] && [ "${FRESH_ALLOW_PINNED_INSTALL_WRITE:-0}" != "1" ]; then + { + printf 'refusing to build into pinned WASIX install: %s\n' "$FRESH_PINNED_WASIX_INSTALL_DIR" + printf 'Unset FRESH_PINNED_WASIX_INSTALL_DIR or set FRESH_ALLOW_PINNED_INSTALL_WRITE=1 if you are intentionally replacing the pin.\n' + } >&2 + exit 2 +fi + +# Serialize the complete producer, not only its final receipt writes. Without +# this lock a second invocation can remove INSTALL_DIR while the first is +# sealing it, and per-stage locks cannot make that cross-stage race safe. +build_lock_dir="$FRESH_WORK_ROOT/runtime/build-locks" +fresh_require_managed_generated_path "$build_lock_dir" wasix-core-build-locks +mkdir -p "$build_lock_dir" +[ -d "$build_lock_dir" ] && [ ! -L "$build_lock_dir" ] || { + printf 'unsafe WASIX core build lock directory: %s\n' "$build_lock_dir" >&2 + exit 2 +} +build_lock_subject="$(python3 - "$WASIX_INSTALL_DIR" <<'PY' +import os +import stat +import sys + +install = os.path.abspath(sys.argv[1]) +name = os.path.basename(os.path.normpath(install)) +if name in {"", ".", ".."} or any(character in name for character in "\n\r\t"): + raise SystemExit("unsafe WASIX install basename") +parent = os.path.realpath(os.path.dirname(install)) +metadata = os.stat(parent) +if not stat.S_ISDIR(metadata.st_mode): + raise SystemExit("WASIX install parent is not a directory") +print(f"{metadata.st_dev}:{metadata.st_ino}:{name}") +PY +)" || exit +build_lock_key="$(printf '%s' "$build_lock_subject" | shasum -a 256 | awk '{print $1}')" +fresh_is_sha256 "$build_lock_key" || { + printf 'could not derive WASIX core build lock identity\n' >&2 + exit 2 +} +build_lock="$build_lock_dir/$build_lock_key.lock" +fresh_require_managed_generated_path "$build_lock" wasix-core-build-lock +[ ! -L "$build_lock" ] || { + printf 'unsafe WASIX core build lock: %s\n' "$build_lock" >&2 + exit 2 +} +exec {build_lock_fd}>"$build_lock" +[ -f "$build_lock" ] && [ ! -L "$build_lock" ] || { + printf 'WASIX core build lock changed while opening: %s\n' "$build_lock" >&2 + exit 2 +} +flock -x "$build_lock_fd" || { + printf 'could not lock WASIX core build for %s\n' "$WASIX_INSTALL_DIR" >&2 + exit 2 +} + +jobs="${JOBS:-$(fresh_jobs)}" +docker_bin="$(fresh_docker_bin)" +fresh_resolve_wasix_core_profile +wasix_core_child_backend="${WASIX_CORE_CHILD_BACKEND:-exec}" +wasix_core_cflags="$FRESH_WASIX_CORE_EFFECTIVE_CFLAGS" +wasix_core_ldflags="$FRESH_WASIX_CORE_EFFECTIVE_LDFLAGS" +wasix_core_latch_state_contract="upstream-sig-atomic-v1" +wasixcc_run_wasm_opt="$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT" +wasixcc_wasm_opt_flags="$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT_FLAGS" +wasixcc_wasm_opt_suppress_default="$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT_SUPPRESS_DEFAULT" +expected_atomic_fence_total="$FRESH_WASIX_CORE_EXPECTED_ATOMIC_FENCE_TOTAL" +expected_final_atomic_fence_total="$FRESH_WASIX_CORE_EXPECTED_FINAL_ATOMIC_FENCE_TOTAL" + +case "$wasix_core_child_backend" in + exec|exec-backend) + wasix_core_child_backend="exec" + wasix_core_cflags="$wasix_core_cflags -DPG_WASIX_ATOMIC_LATCH_STATE=1" + wasix_core_latch_state_contract="packed-atomic-v1" + ;; + copied-fork|fork) + wasix_core_child_backend="copied-fork" + wasix_core_cflags="$wasix_core_cflags -DPG_WASIX_COPIED_FORK_BACKEND=1" + ;; + *) + printf 'unknown WASIX_CORE_CHILD_BACKEND=%s; expected exec or copied-fork\n' "$wasix_core_child_backend" >&2 + exit 2 + ;; +esac + +"$FRESH_ROOT/bin/apply-wasix-core-overlay.sh" >/dev/null + +source_signature="$( + { + cat "$WASIX_SRC_DIR/.fresh-wasix-core-signature" + shasum -a 256 "$0" + shasum -a 256 \ + "$FRESH_ROOT/runtime/bin/verify-postmaster-wasm-import.py" \ + "$FRESH_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" \ + "$FRESH_ROOT/bin/seal-wasix-core-exports.sh" \ + "$FRESH_ROOT/bin/seal-wasix-linear-memory.sh" \ + "$FRESH_ROOT/lib/guest_build_provenance.py" \ + "$FRESH_ROOT/lib/linear_memory_transaction.py" \ + "$FRESH_ROOT/lib/sealed_export_chain.py" \ + "$FRESH_ROOT/runtime/policies/sealed-main-runtime-exports.v1.txt" \ + "$FRESH_ROOT/runtime/policies/sealed-main-dlsym-exports.v1.txt" \ + "$FRESH_ROOT/runtime/policies/sealed-side-modules.v1.tsv" \ + "$FRESH_ROOT/tools/sealed-export-closure/Cargo.toml" \ + "$FRESH_ROOT/tools/sealed-export-closure/Cargo.lock" \ + "$FRESH_ROOT/tools/sealed-export-closure/src/main.rs" \ + "$durable_publication" + printf 'WASIXCC_SYSROOT_PREFIX=%s\n' "${WASIXCC_SYSROOT_PREFIX:-}" + printf 'WASIXCC_SYSROOT=%s\n' "${WASIXCC_SYSROOT:-}" + if [ -n "${WASIXCC_SYSROOT_PREFIX:-}" ] && [ -f "$WASIXCC_SYSROOT_PREFIX/.fresh-sysroot-signature" ]; then + printf 'WASIXCC_SYSROOT_PREFIX_SIGNATURE=' + cat "$WASIXCC_SYSROOT_PREFIX/.fresh-sysroot-signature" + fi + if [ -n "${WASIXCC_SYSROOT:-}" ] && [ -f "$WASIXCC_SYSROOT/.fresh-sysroot-signature" ]; then + printf 'WASIXCC_SYSROOT_SIGNATURE=' + cat "$WASIXCC_SYSROOT/.fresh-sysroot-signature" + fi + printf 'WASIX_CORE_PROFILE=%s\n' "$WASIX_CORE_PROFILE" + printf 'WASIX_CORE_CHILD_BACKEND=%s\n' "$wasix_core_child_backend" + printf 'WASIX_CORE_LATCH_STATE_CONTRACT=%s\n' "$wasix_core_latch_state_contract" + printf 'WASIX_CORE_CFLAGS=%s\n' "$wasix_core_cflags" + printf 'WASIX_CORE_LDFLAGS=%s\n' "$wasix_core_ldflags" + printf 'WASIXCC_RUN_WASM_OPT=%s\n' "$wasixcc_run_wasm_opt" + printf 'WASIXCC_WASM_OPT_FLAGS=%s\n' "$wasixcc_wasm_opt_flags" + printf 'WASIXCC_WASM_OPT_SUPPRESS_DEFAULT=%s\n' "$wasixcc_wasm_opt_suppress_default" + printf 'EXPECTED_ATOMIC_FENCE_TOTAL=%s\n' "${expected_atomic_fence_total:-profile-unlocked}" + printf 'EXPECTED_FINAL_ATOMIC_FENCE_TOTAL=%s\n' \ + "${expected_final_atomic_fence_total:-profile-unlocked}" + printf 'LINEAR_MEMORY_PROFILE_ID=%s\n' "$FRESH_LINEAR_MEMORY_PROFILE_ID" + printf 'LINEAR_MEMORY_MAXIMUM_PAGES=%s\n' "$FRESH_LINEAR_MEMORY_MAXIMUM_PAGES" + printf 'LINEAR_MEMORY_STATIC_BOUND_PAGES=%s\n' "$FRESH_LINEAR_MEMORY_STATIC_BOUND_PAGES" + printf 'LINEAR_MEMORY_STATIC_OFFSET_GUARD_BYTES=%s\n' \ + "$FRESH_LINEAR_MEMORY_STATIC_OFFSET_GUARD_BYTES" + git -C "$WASIX_SRC_DIR" diff -- \ + src/include \ + src/template \ + src/makefiles \ + src/backend/port \ + src/backend/utils/mb/conversion_procs \ + src/interfaces/libpq/Makefile + } | shasum -a 256 | awk '{print $1}' +)" +build_signature_file="$WASIX_BUILD_DIR/.fresh-wasix-core-build-signature" +fresh_require_managed_generated_path "$build_signature_file" wasix-core-build-signature +if [ "$force_clean" -eq 0 ] && [ -f "$build_signature_file" ] && [ "$(cat "$build_signature_file")" = "$source_signature" ]; then + mkdir -p "$WASIX_BUILD_DIR" "$WASIX_INSTALL_DIR" +else + fresh_require_managed_generated_path "$WASIX_BUILD_DIR" WASIX_BUILD_DIR + rm -rf "$WASIX_BUILD_DIR" + mkdir -p "$WASIX_BUILD_DIR" "$WASIX_INSTALL_DIR" + printf '%s' "$source_signature" >"$build_signature_file" +fi + +report="$REPORT_DIR/wasix-core-build.md" +log="$REPORT_DIR/wasix-core-build.log" +fresh_require_managed_generated_path "$report" wasix-core-build-report +fresh_require_managed_generated_path "$log" wasix-core-build-log +fresh_write_report_header "$report" "WASIX Core PostgreSQL Build" + +{ + printf '## Scope\n\n' + printf -- '- Source: clean PostgreSQL `%s` plus `postgres/overlays/wasix-core` and the explicit patch series.\n' "$POSTGRES_TAG" + printf -- '- Template: `--with-template=wasix-core`.\n' + printf -- '- Build profile: `%s`.\n' "$WASIX_CORE_PROFILE" + printf -- '- Profile description: `%s`.\n' "$FRESH_WASIX_CORE_PROFILE_DESCRIPTION" + printf -- '- Child backend: `%s`.\n' "$wasix_core_child_backend" + printf -- '- Shared latch state contract: `%s`.\n' "$wasix_core_latch_state_contract" + printf -- '- Build lane: optimized core server/tools, PL/pgSQL, snowball dictionary, and core encoding conversion modules; no contrib or regression test binaries.\n' + printf -- '- wasixcc sysroot prefix: `%s`.\n' "${WASIXCC_SYSROOT_PREFIX:-}" + printf -- '- wasixcc sysroot: `%s`.\n' "${WASIXCC_SYSROOT:-}" + printf -- '- Build directory: `%s`.\n' "$WASIX_BUILD_DIR" + printf -- '- Install directory: `%s`.\n' "$WASIX_INSTALL_DIR" + printf -- '- CFLAGS: `%s`.\n' "$wasix_core_cflags" + printf -- '- LDFLAGS: `%s`.\n' "$wasix_core_ldflags" + printf -- '- wasixcc wasm-opt: `%s`.\n' "$wasixcc_run_wasm_opt" + printf -- '- wasixcc wasm-opt flags: `%s`.\n' "$wasixcc_wasm_opt_flags" + printf -- '- wasixcc suppress implicit wasm-opt defaults: `%s`.\n' "$wasixcc_wasm_opt_suppress_default" + printf -- '- Final-module critical fence contract: `SetLatch=2`, `ResetLatch=1`, and `WaitEventSetWait=1`.\n' + printf -- '- Main export policy: exact typed packaged-side closure, followed only by Binaryen module-element reachability DCE and final proof replay.\n' + printf -- '- Linear-memory ABI: `%s` (bounded 256 MiB guest maximum; 64-bit-host static lowering only).\n' "$FRESH_LINEAR_MEMORY_PROFILE_ID" + printf -- '- Profile-locked pre-seal fence inventory: `%s`.\n' "${expected_atomic_fence_total:-critical-functions-only}" + printf -- '- Profile-locked final sealed-module fence inventory: `%s`.\n' \ + "${expected_final_atomic_fence_total:-critical-functions-only}" + printf -- '- Configure wasm-opt: `no`.\n' + printf -- '- Largefile support: not disabled.\n' + printf -- '- Spinlocks: not disabled.\n' + printf -- '- Single-user compatibility macros: not used.\n\n' + printf '## Build Log\n\n' + printf 'See `%s`.\n' "$log" +} >>"$report" + +mode="build" +if [ "$configure_only" -eq 1 ]; then + mode="configure-only" +fi + +: >"$log" +if ! "$docker_bin" info >>"$log" 2>&1; then + { + printf '\n## Result\n\n' + printf -- '- Status: `blocked`\n' + printf -- '- Mode: `%s`\n' "$mode" + printf -- '- Blocker: Docker daemon is not reachable.\n\n' + printf 'Start Docker or run this script inside an environment with the pinned WASIX toolchain already available.\n' + } >>"$report" + printf 'blocked: Docker daemon is not reachable; see %s\n' "$log" >&2 + exit 2 +fi + +set +e +fresh_ensure_docker_image >>"$log" 2>&1 +image_status=$? +set -e +if [ "$image_status" -ne 0 ]; then + { + printf '\n## Result\n\n' + printf -- '- Status: `fail`\n' + printf -- '- Mode: `%s`\n' "$mode" + printf -- '- Exit code: `%s`\n' "$image_status" + printf -- '- Failure: could not prepare Docker image `%s`.\n' "$FRESH_WASIX_DOCKER_IMAGE" + } >>"$report" + printf 'WASIX Docker image preparation failed; see %s\n' "$log" >&2 + exit "$image_status" +fi + +if ! DOCKER_IMAGE="$FRESH_WASIX_DOCKER_IMAGE" \ + "$FRESH_ROOT/runtime/bin/run-blocker-probes.sh" --validate-sysroot-only >>"$log" 2>&1; then + { + printf '\n## Result\n\n' + printf -- '- Status: `fail`\n' + printf -- '- Mode: `%s`\n' "$mode" + printf -- '- Failure: exact patched WASIX libc carrier validation failed.\n' + } >>"$report" + printf 'WASIX libc carrier validation failed; see %s\n' "$log" >&2 + exit 2 +fi + +fresh_require_managed_generated_path "$WASIX_BUILD_DIR" WASIX_BUILD_DIR +fresh_require_managed_generated_path "$WASIX_INSTALL_DIR" WASIX_INSTALL_DIR +set +e +printf '\n## docker run\n\n' >>"$log" +docker_env=() +if [ -n "${WASIXCC_SYSROOT_PREFIX:-}" ]; then + docker_env+=(-e "WASIXCC_SYSROOT_PREFIX=$(fresh_docker_path_for "$WASIXCC_SYSROOT_PREFIX")") +fi +if [ -n "${WASIXCC_SYSROOT:-}" ]; then + docker_env+=(-e "WASIXCC_SYSROOT=$(fresh_docker_path_for "$WASIXCC_SYSROOT")") +fi +"$docker_bin" run --rm \ + -v "$REPO_ROOT:/work" \ + -w /work \ + -e JOBS="$jobs" \ + -e PGSRC="${WASIX_SRC_DIR#$REPO_ROOT/}" \ + -e BUILD_DIR="${WASIX_BUILD_DIR#$REPO_ROOT/}" \ + -e INSTALL_DIR="${WASIX_INSTALL_DIR#$REPO_ROOT/}" \ + -e MODE="$mode" \ + -e WASIX_CORE_CFLAGS="$wasix_core_cflags" \ + -e WASIX_CORE_LDFLAGS="$wasix_core_ldflags" \ + -e WASIXCC_RUN_WASM_OPT="$wasixcc_run_wasm_opt" \ + -e WASIXCC_WASM_OPT_FLAGS="$wasixcc_wasm_opt_flags" \ + -e WASIXCC_WASM_OPT_SUPPRESS_DEFAULT="$wasixcc_wasm_opt_suppress_default" \ + -e EXPECTED_ATOMIC_FENCE_TOTAL="$expected_atomic_fence_total" \ + -e WASIX_CORE_LATCH_STATE_CONTRACT="$wasix_core_latch_state_contract" \ + -e "HOST_UID=$(id -u)" \ + -e "HOST_GID=$(id -g)" \ + "${docker_env[@]}" \ + "$FRESH_WASIX_DOCKER_IMAGE" \ + bash -lc ' + set -euo pipefail + source ./src/runtimes/liboliphaunt/wasix/assets/build/docker_wasix_env.sh + cd /work + + restore_host_ownership() { + local command_status="$?" + local ownership_failed=0 + local output_path + + trap - EXIT + for output_path in "/work/$BUILD_DIR" "/work/$INSTALL_DIR"; do + if [ -e "$output_path" ] && ! chown -R "$HOST_UID:$HOST_GID" "$output_path"; then + printf "failed to restore host ownership for %s\n" "$output_path" >&2 + ownership_failed=1 + fi + done + if [ "$command_status" -eq 0 ] && [ "$ownership_failed" -ne 0 ]; then + command_status="$ownership_failed" + fi + exit "$command_status" + } + trap restore_host_ownership EXIT + + mkdir -p "$BUILD_DIR" "$INSTALL_DIR" + cd "$BUILD_DIR" + configure_args=( + "--prefix=/" + "--bindir=/bin" + "--libdir=/lib" + "--datadir=/share/postgresql" + "--host=wasm32-wasix" + "--with-template=wasix-core" + "--without-readline" + "--without-icu" + "--without-zlib" + "--without-llvm" + "--without-pam" + "--with-openssl=no" + ) + if [ ! -f config.status ]; then + WASIXCC_RUN_WASM_OPT=no \ + CC=wasixcc \ + AR=wasixar \ + RANLIB=wasixranlib \ + NM=wasixnm \ + CPPFLAGS="-D_GNU_SOURCE" \ + CFLAGS="$WASIX_CORE_CFLAGS" \ + LDFLAGS="$WASIX_CORE_LDFLAGS" \ + "/work/$PGSRC/configure" "${configure_args[@]}" + fi + if ! grep -Fxq "#define HAVE_SYNC_FILE_RANGE 1" src/include/pg_config.h; then + printf "configured PostgreSQL does not define HAVE_SYNC_FILE_RANGE=1; refuse the fallback build\n" >&2 + exit 2 + fi + if [ "$MODE" = "configure-only" ]; then + exit 0 + fi + core_dirs=( + src/port + src/common + src/include + src/interfaces/libpq + src/backend + src/backend/snowball + src/backend/utils/mb/conversion_procs + src/pl/plpgsql/src + src/bin/initdb + src/bin/pg_ctl + src/bin/psql + src/bin/pg_dump + src/bin/pg_config + src/timezone + ) + make -C src/backend -j "$JOBS" generated-headers + rm -f \ + src/backend/postgres \ + src/bin/initdb/initdb \ + src/bin/pg_ctl/pg_ctl \ + src/bin/psql/psql \ + src/bin/pg_dump/pg_dump \ + src/bin/pg_dump/pg_restore \ + src/bin/pg_dump/pg_dumpall \ + src/bin/pg_config/pg_config + for dir in "${core_dirs[@]}"; do + make -C "$dir" -j "$JOBS" all + done + rm -rf "/work/$INSTALL_DIR" + mkdir -p "/work/$INSTALL_DIR" + for dir in "${core_dirs[@]}"; do + make -C "$dir" -j "$JOBS" install DESTDIR="/work/$INSTALL_DIR" + done + python3 \ + /work/src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-postmaster-wasm-import.py \ + "/work/$INSTALL_DIR/bin/postgres" + concurrency_args=() + if [ -n "$EXPECTED_ATOMIC_FENCE_TOTAL" ]; then + concurrency_args+=(--expected-total "$EXPECTED_ATOMIC_FENCE_TOTAL") + fi + if [ "$WASIX_CORE_LATCH_STATE_CONTRACT" = packed-atomic-v1 ]; then + concurrency_args+=( + --latch-state-contract packed-atomic-v1 + --wasm-dis /opt/wasixcc-home/.wasixcc/binaryen/bin/wasm-dis + ) + fi + python3 \ + /work/src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-postmaster-concurrency-contract.py \ + "${concurrency_args[@]}" \ + "/work/$INSTALL_DIR/bin/postgres" + ' >>"$log" 2>&1 +status=$? +set -e + +if [ "$status" -eq 0 ] && [ "$mode" = build ] && \ + [ "$wasix_core_latch_state_contract" = packed-atomic-v1 ] +then + if [ -z "$expected_atomic_fence_total" ] || \ + [ -z "$expected_final_atomic_fence_total" ]; then + printf 'sealed export closure requires profile-locked pre-seal and final atomic fence totals\n' >>"$log" + status=2 + else + set +e + ( + set -euo pipefail + + "$FRESH_ROOT/bin/seal-wasix-core-exports.sh" \ + --install-dir "$WASIX_INSTALL_DIR" \ + --expected-total "$expected_final_atomic_fence_total" + + sealed_export_receipt="$WASIX_INSTALL_DIR/share/postgresql/wasix-postmaster.sealed-export.structure.receipt" + "$FRESH_ROOT/bin/seal-wasix-linear-memory.sh" \ + --install-dir "$WASIX_INSTALL_DIR" \ + --predecessor-receipt "$sealed_export_receipt" + + python3 "$FRESH_ROOT/runtime/bin/verify-postmaster-wasm-import.py" \ + "$WASIX_INSTALL_DIR/bin/postgres" + fresh_require_start_proof_tool \ + "$FRESH_START_PROOF_BIN" \ + "$FRESH_POSTMASTER_EXECUTOR_BUILD_RECEIPT" + + proof_dir="$WASIX_INSTALL_DIR/share/postgresql" + final_start_proof="$proof_dir/wasix-postmaster.start-proof.json" + final_start_proof_pending="$proof_dir/.wasix-postmaster.start-proof.pending" + final_concurrency_receipt="$proof_dir/wasix-postmaster.final-wasm-concurrency.receipt" + final_concurrency_receipt_pending="$proof_dir/.wasix-postmaster.final-wasm-concurrency.pending" + [ -d "$proof_dir" ] && [ ! -L "$proof_dir" ] || { + printf 'unsafe final-proof directory: %s\n' "$proof_dir" >&2 + exit 2 + } + python3 "$durable_publication" remove-private "$final_start_proof_pending" + python3 "$durable_publication" remove-private "$final_concurrency_receipt_pending" + cleanup_final_proof_stage() { + status=$? + trap - EXIT + python3 "$durable_publication" remove-private \ + "$final_start_proof_pending" || status=2 + python3 "$durable_publication" remove-private \ + "$final_concurrency_receipt_pending" || status=2 + exit "$status" + } + trap cleanup_final_proof_stage EXIT + + docker_install_dir="$(fresh_docker_path_for "$WASIX_INSTALL_DIR")" + + validate_final_proof_generation() { + "$FRESH_START_PROOF_BIN" "$WASIX_INSTALL_DIR/bin/postgres" \ + | python3 "$durable_publication" write-stdin \ + "$final_start_proof_pending" + [ -s "$final_start_proof_pending" ] && \ + [ ! -L "$final_start_proof_pending" ] || { + printf 'deterministic-start analyzer did not produce a regular proof\n' >&2 + return 2 + } + python3 "$durable_publication" require-equal \ + "$final_start_proof_pending" "$final_start_proof" + python3 "$durable_publication" remove-private "$final_start_proof_pending" + python3 \ + "$FRESH_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" \ + --expected-total "$expected_final_atomic_fence_total" \ + --latch-state-contract packed-atomic-v1 \ + --verified-receipt "$final_concurrency_receipt" \ + --receipt-only \ + "$WASIX_INSTALL_DIR/bin/postgres" + } + + if [ -e "$final_concurrency_receipt" ] || \ + [ -L "$final_concurrency_receipt" ] + then + [ -f "$final_concurrency_receipt" ] && \ + [ ! -L "$final_concurrency_receipt" ] || { + printf 'final concurrency admission is not regular: %s\n' \ + "$final_concurrency_receipt" >&2 + exit 2 + } + [ -f "$final_start_proof" ] && [ ! -L "$final_start_proof" ] || { + printf 'admitted final generation has no regular start proof: %s\n' \ + "$final_start_proof" >&2 + exit 2 + } + validate_final_proof_generation + else + "$FRESH_START_PROOF_BIN" "$WASIX_INSTALL_DIR/bin/postgres" \ + | python3 "$durable_publication" write-stdin \ + "$final_start_proof_pending" + [ -s "$final_start_proof_pending" ] && \ + [ ! -L "$final_start_proof_pending" ] || { + printf 'deterministic-start analyzer did not produce a regular proof\n' >&2 + exit 2 + } + if [ -e "$final_start_proof" ] || [ -L "$final_start_proof" ]; then + [ -f "$final_start_proof" ] && [ ! -L "$final_start_proof" ] || { + printf 'partial final start proof is not regular: %s\n' \ + "$final_start_proof" >&2 + exit 2 + } + python3 "$durable_publication" require-equal \ + "$final_start_proof_pending" "$final_start_proof" + python3 "$durable_publication" remove-private "$final_start_proof_pending" + else + python3 "$durable_publication" publish \ + "$final_start_proof_pending" "$final_start_proof" + fi + + "$docker_bin" run --rm \ + --user "$(id -u):$(id -g)" \ + -v "$REPO_ROOT:/work" \ + -w /work \ + "$FRESH_WASIX_DOCKER_IMAGE" \ + python3 \ + /work/src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-postmaster-concurrency-contract.py \ + --expected-total "$expected_final_atomic_fence_total" \ + --latch-state-contract packed-atomic-v1 \ + --wasm-dis /opt/wasixcc-home/.wasixcc/binaryen/bin/wasm-dis \ + --receipt "$docker_install_dir/share/postgresql/$(basename "$final_concurrency_receipt_pending")" \ + "$docker_install_dir/bin/postgres" + [ -f "$final_concurrency_receipt_pending" ] && \ + [ ! -L "$final_concurrency_receipt_pending" ] || { + printf 'concurrency analyzer did not produce a regular receipt\n' >&2 + exit 2 + } + python3 \ + "$FRESH_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" \ + --expected-total "$expected_final_atomic_fence_total" \ + --latch-state-contract packed-atomic-v1 \ + --verified-receipt "$final_concurrency_receipt_pending" \ + --receipt-only \ + "$WASIX_INSTALL_DIR/bin/postgres" + # This receipt is the admission record for the pair and is therefore + # published last, without replacement, only after the start proof is + # durable at its public name. + python3 "$durable_publication" publish \ + "$final_concurrency_receipt_pending" "$final_concurrency_receipt" + validate_final_proof_generation + fi + trap - EXIT + ) >>"$log" 2>&1 + status=$? + set -e + fi +fi + +if [ "$status" -eq 0 ]; then + if [ "$mode" = build ]; then + guest_build_receipt="$WASIX_INSTALL_DIR/guest-build.receipt" + guest_build_receipt_pending="$WASIX_INSTALL_DIR/.guest-build.receipt.pending" + concurrency_args=() + if [ -n "$expected_final_atomic_fence_total" ]; then + concurrency_args+=(--expected-total "$expected_final_atomic_fence_total") + fi + final_wasm_concurrency_receipt_sha256="none" + if [ "$wasix_core_latch_state_contract" = packed-atomic-v1 ]; then + final_wasm_concurrency_receipt="$WASIX_INSTALL_DIR/share/postgresql/wasix-postmaster.final-wasm-concurrency.receipt" + [ -f "$final_wasm_concurrency_receipt" ] && [ ! -L "$final_wasm_concurrency_receipt" ] || { + echo 'missing final Wasm concurrency receipt' >&2 + exit 125 + } + concurrency_args+=( + --latch-state-contract packed-atomic-v1 + --verified-receipt "$final_wasm_concurrency_receipt" + ) + final_wasm_concurrency_receipt_sha256="$( + fresh_wasmer_bin_hash "$final_wasm_concurrency_receipt" + )" || exit + fresh_is_sha256 "$final_wasm_concurrency_receipt_sha256" || { + echo 'final Wasm concurrency receipt identity is not a SHA-256' >&2 + exit 125 + } + fi + concurrency_contract_output="$( + python3 "$FRESH_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" \ + "${concurrency_args[@]}" "$WASIX_INSTALL_DIR/bin/postgres" + )" || exit + atomic_fence_total="$( + printf '%s\n' "$concurrency_contract_output" | + sed -n 's/^verified PostgreSQL Wasm concurrency contract: total=\([0-9][0-9]*\) .*/\1/p' + )" + case "$atomic_fence_total" in + ''|*[!0-9]*) echo 'could not parse verified atomic fence total' >&2; exit 125 ;; + esac + linear_memory_profile_id="$FRESH_LINEAR_MEMORY_PROFILE_ID" + linear_memory_install_receipt="$WASIX_INSTALL_DIR/share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json" + [ -f "$linear_memory_install_receipt" ] && \ + [ ! -L "$linear_memory_install_receipt" ] || { + echo 'missing regular linear-memory install receipt' >&2 + exit 125 + } + python3 - "$linear_memory_install_receipt" "$linear_memory_profile_id" <<'PY' +import json +import sys + +path, expected_profile = sys.argv[1:] +with open(path, encoding="utf-8") as stream: + receipt = json.load(stream) +if receipt.get("schema") != "oliphaunt.wasix-postmaster.linear-memory-install.v1": + raise SystemExit("linear-memory install receipt schema differs") +if receipt.get("profile-id") != expected_profile: + raise SystemExit("linear-memory install receipt profile differs") +PY + linear_memory_install_receipt_sha256="$( + fresh_wasmer_bin_hash "$linear_memory_install_receipt" + )" || exit + fresh_is_sha256 "$linear_memory_install_receipt_sha256" || { + echo 'linear-memory install receipt identity is not a SHA-256' >&2 + exit 125 + } + [ -f "$build_signature_file" ] && [ ! -L "$build_signature_file" ] && + [ "$(cat "$build_signature_file")" = "$source_signature" ] || { + echo 'WASIX core build signature changed before guest receipt publication' >&2 + exit 125 + } + installed_closure_sha256="$( + python3 "$FRESH_ROOT/lib/guest_build_provenance.py" \ + seal-identity "$WASIX_INSTALL_DIR" + )" || exit + fresh_is_sha256 "$installed_closure_sha256" || { + echo 'WASIX core installed closure identity is not a SHA-256' >&2 + exit 125 + } + [ -f "$build_signature_file" ] && [ ! -L "$build_signature_file" ] && + [ "$(cat "$build_signature_file")" = "$source_signature" ] || { + echo 'WASIX core build signature changed while installed outputs were hashed' >&2 + exit 125 + } + case "$wasix_core_cflags$wasix_core_ldflags$wasixcc_run_wasm_opt$wasixcc_wasm_opt_flags$wasixcc_wasm_opt_suppress_default" in + *$'\n'*|*$'\r'*) + echo 'WASIX core effective build flags contain a line break' >&2 + exit 2 + ;; + esac + python3 "$durable_publication" remove-private "$guest_build_receipt_pending" + { + printf 'schema=oliphaunt.wasix-postmaster.guest-build.v4\n' + printf 'core_profile=%s\n' "$WASIX_CORE_PROFILE" + printf 'guest_source_signature_sha256=%s\n' "$source_signature" + printf 'installed_closure_sha256=%s\n' "$installed_closure_sha256" + printf 'child_backend=%s\n' "$wasix_core_child_backend" + printf 'effective_cflags=%s\n' "$wasix_core_cflags" + printf 'effective_ldflags=%s\n' "$wasix_core_ldflags" + printf 'effective_wasm_opt=%s\n' "$wasixcc_run_wasm_opt" + printf 'effective_wasm_opt_flags=%s\n' "${wasixcc_wasm_opt_flags:-none}" + printf 'effective_wasm_opt_suppress_default=%s\n' "$wasixcc_wasm_opt_suppress_default" + printf 'atomic_fence_total=%s\n' "$atomic_fence_total" + printf 'atomic_fence_set_latch=2\n' + printf 'atomic_fence_reset_latch=1\n' + printf 'atomic_fence_wait_event_set_wait=1\n' + printf 'latch_state_contract=%s\n' "$wasix_core_latch_state_contract" + printf 'final_wasm_concurrency_receipt_sha256=%s\n' \ + "$final_wasm_concurrency_receipt_sha256" + printf 'linear_memory_profile_id=%s\n' "$linear_memory_profile_id" + printf 'linear_memory_install_receipt_sha256=%s\n' \ + "$linear_memory_install_receipt_sha256" + printf 'postgres_tag=%s\n' "$POSTGRES_TAG" + printf 'postgres_version=%s\n' "$POSTGRES_VERSION" + printf 'sysroot_variant=%s\n' "$WASIXCC_SYSROOT_VARIANT" + } | python3 "$durable_publication" write-stdin \ + "$guest_build_receipt_pending" + if [ -e "$guest_build_receipt" ] || [ -L "$guest_build_receipt" ]; then + [ -f "$guest_build_receipt" ] && [ ! -L "$guest_build_receipt" ] || { + printf 'guest build admission is not regular: %s\n' \ + "$guest_build_receipt" >&2 + exit 125 + } + python3 "$durable_publication" require-equal \ + "$guest_build_receipt_pending" "$guest_build_receipt" || exit 125 + python3 "$durable_publication" remove-private \ + "$guest_build_receipt_pending" || exit 125 + else + # The guest receipt admits the complete installed closure. It is + # synchronized and published without replacement only after every + # predecessor proof above has been replayed against that closure. + python3 "$durable_publication" publish \ + "$guest_build_receipt_pending" "$guest_build_receipt" || exit 125 + fi + fi + { + printf '\n## Result\n\n' + printf -- '- Status: `pass`\n' + printf -- '- Mode: `%s`\n' "$mode" + printf -- '- Build directory: `%s`\n' "$WASIX_BUILD_DIR" + printf -- '- Install directory: `%s`\n' "$WASIX_INSTALL_DIR" + } >>"$report" + printf 'built WASIX core PostgreSQL lane at %s\n' "$WASIX_INSTALL_DIR" +else + { + printf '\n## Result\n\n' + printf -- '- Status: `fail`\n' + printf -- '- Mode: `%s`\n' "$mode" + printf -- '- Exit code: `%s`\n\n' "$status" + printf '## Blocker Policy\n\n' + printf 'Treat this as a PostgreSQL/WASIX/toolchain compatibility blocker. Do not add fake PostgreSQL success shims to make this pass.\n' + } >>"$report" + printf 'WASIX core build failed; see %s\n' "$log" >&2 + exit "$status" +fi diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-wasix-regress-support.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-wasix-regress-support.sh new file mode 100755 index 00000000..7b3beb0d --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-wasix-regress-support.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +force=0 +while [ "$#" -gt 0 ]; do + case "$1" in + --force) + force=1 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + exit 2 + ;; + esac + shift +done + +fresh_ensure_dirs +fresh_require_patched_wasixcc_sysroot + +jobs="${JOBS:-$(fresh_jobs)}" +docker_bin="$(fresh_docker_bin)" +fresh_resolve_wasix_core_profile + +if [ ! -f "$WASIX_BUILD_DIR/config.status" ] || [ ! -x "$WASIX_INSTALL_DIR/bin/postgres" ]; then + "$FRESH_ROOT/bin/build-wasix-core.sh" >/dev/null +fi + +report="$REPORT_DIR/wasix-regress-support-build.md" +log="$REPORT_DIR/wasix-regress-support-build.log" +signature_file="$WASIX_BUILD_DIR/.fresh-wasix-regress-support-signature" +regress_so="$WASIX_INSTALL_DIR/lib/postgresql/regress.so" +regress_host_dlsuffix_alias="$WASIX_INSTALL_DIR/lib/postgresql/regress.dylib" + +source_signature="$( + { + printf 'support-script=' + shasum -a 256 "$0" + printf 'wasix-make=' + shasum -a 256 "$FRESH_ROOT/bin/wasix-make.sh" + printf 'WASIX_CORE_PROFILE=%s\n' "$WASIX_CORE_PROFILE" + printf 'WASIXCC_RUN_WASM_OPT=%s\n' "$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT" + printf 'WASIXCC_WASM_OPT_FLAGS=%s\n' "$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT_FLAGS" + printf 'WASIXCC_WASM_OPT_SUPPRESS_DEFAULT=%s\n' "$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT_SUPPRESS_DEFAULT" + printf 'WASIXCC_SYSROOT_PREFIX=%s\n' "${WASIXCC_SYSROOT_PREFIX:-}" + printf 'WASIXCC_SYSROOT=%s\n' "${WASIXCC_SYSROOT:-}" + if [ -n "${WASIXCC_SYSROOT_PREFIX:-}" ] && [ -f "$WASIXCC_SYSROOT_PREFIX/.fresh-sysroot-signature" ]; then + printf 'WASIXCC_SYSROOT_PREFIX_SIGNATURE=' + cat "$WASIXCC_SYSROOT_PREFIX/.fresh-sysroot-signature" + fi + if [ -n "${WASIXCC_SYSROOT:-}" ] && [ -f "$WASIXCC_SYSROOT/.fresh-sysroot-signature" ]; then + printf 'WASIXCC_SYSROOT_SIGNATURE=' + cat "$WASIXCC_SYSROOT/.fresh-sysroot-signature" + fi + git -C "$WASIX_SRC_DIR" rev-parse HEAD 2>/dev/null || true + git -C "$WASIX_SRC_DIR" diff -- \ + src/test/regress \ + src/include \ + src/Makefile.shlib \ + src/makefiles/Makefile.wasix-core \ + src/template/wasix-core + } | shasum -a 256 | awk '{print $1}' +)" + +fresh_write_report_header "$report" "WASIX Regression Support Build" +{ + printf '## Scope\n\n' + printf -- '- Target: `src/test/regress` loadable support module `regress.so`.\n' + printf -- '- WASIX core profile: `%s`.\n' "$WASIX_CORE_PROFILE" + printf -- '- Install path: `%s`.\n' "$regress_so" + printf -- '- wasixcc sysroot prefix: `%s`.\n' "${WASIXCC_SYSROOT_PREFIX:-}" + printf -- '- wasixcc sysroot: `%s`.\n\n' "${WASIXCC_SYSROOT:-}" + printf '## Build Log\n\n' + printf 'See `%s`.\n' "$log" +} >>"$report" + +if [ "$force" -eq 0 ] && + [ -f "$signature_file" ] && + [ "$(cat "$signature_file")" = "$source_signature" ] && + [ -f "$regress_so" ] && + [ -f "$regress_host_dlsuffix_alias" ]; then + { + printf '\n## Result\n\n' + printf -- '- Status: `pass`\n' + printf -- '- Cached: `true`\n' + } >>"$report" + printf 'WASIX regress support is current at %s\n' "$regress_so" + exit 0 +fi + +: >"$log" +if ! "$docker_bin" info >>"$log" 2>&1; then + { + printf '\n## Result\n\n' + printf -- '- Status: `blocked`\n' + printf -- '- Blocker: Docker daemon is not reachable.\n' + } >>"$report" + printf 'blocked: Docker daemon is not reachable; see %s\n' "$log" >&2 + exit 2 +fi + +set +e +fresh_ensure_docker_image >>"$log" 2>&1 +image_status=$? +set -e +if [ "$image_status" -ne 0 ]; then + { + printf '\n## Result\n\n' + printf -- '- Status: `fail`\n' + printf -- '- Exit code: `%s`\n' "$image_status" + printf -- '- Failure: could not prepare Docker image `%s`.\n' "$FRESH_WASIX_DOCKER_IMAGE" + } >>"$report" + printf 'WASIX Docker image preparation failed; see %s\n' "$log" >&2 + exit "$image_status" +fi + +docker_env=() +if [ -n "${WASIXCC_SYSROOT_PREFIX:-}" ]; then + docker_env+=(-e "WASIXCC_SYSROOT_PREFIX=$(fresh_docker_path_for "$WASIXCC_SYSROOT_PREFIX")") +fi +if [ -n "${WASIXCC_SYSROOT:-}" ]; then + docker_env+=(-e "WASIXCC_SYSROOT=$(fresh_docker_path_for "$WASIXCC_SYSROOT")") +fi + +set +e +"$docker_bin" run --rm \ + -v "$REPO_ROOT:/work" \ + -w /work \ + -e JOBS="$jobs" \ + -e BUILD_DIR="${WASIX_BUILD_DIR#$REPO_ROOT/}" \ + -e INSTALL_DIR="${WASIX_INSTALL_DIR#$REPO_ROOT/}" \ + -e WASIXCC_RUN_WASM_OPT="$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT" \ + -e WASIXCC_WASM_OPT_FLAGS="$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT_FLAGS" \ + -e WASIXCC_WASM_OPT_SUPPRESS_DEFAULT="$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT_SUPPRESS_DEFAULT" \ + -e "HOST_UID=$(id -u)" \ + -e "HOST_GID=$(id -g)" \ + "${docker_env[@]}" \ + "$FRESH_WASIX_DOCKER_IMAGE" \ + bash -lc ' + set -euo pipefail + source ./src/runtimes/liboliphaunt/wasix/assets/build/docker_wasix_env.sh + restore_host_ownership() { + local command_status="$?" + local ownership_failed=0 + local output_path + + trap - EXIT + for output_path in \ + "/work/$BUILD_DIR/src/test/regress" \ + "/work/$INSTALL_DIR/lib/postgresql"; do + if [ -e "$output_path" ] && ! chown -R "$HOST_UID:$HOST_GID" "$output_path"; then + printf "failed to restore host ownership for %s\n" "$output_path" >&2 + ownership_failed=1 + fi + done + if [ "$command_status" -eq 0 ] && [ "$ownership_failed" -ne 0 ]; then + command_status="$ownership_failed" + fi + exit "$command_status" + } + trap restore_host_ownership EXIT + + if [ ! -e "$BUILD_DIR/src/include/utils/errcodes.h" ]; then + make -C "$BUILD_DIR/src/backend" generated-headers + fi + make -C "$BUILD_DIR/src/test/regress" clean-lib + make -C "$BUILD_DIR/src/test/regress" -j "$JOBS" all-lib + make -C "$BUILD_DIR/src/test/regress" install-lib DESTDIR="/work/$INSTALL_DIR" + test -f "/work/$INSTALL_DIR/lib/postgresql/regress.so" + cp -f \ + "/work/$INSTALL_DIR/lib/postgresql/regress.so" \ + "/work/$INSTALL_DIR/lib/postgresql/regress.dylib" + ' >>"$log" 2>&1 +status=$? +set -e + +if [ "$status" -eq 0 ]; then + printf '%s' "$source_signature" >"$signature_file" + { + printf '\n## Result\n\n' + printf -- '- Status: `pass`\n' + printf -- '- Cached: `false`\n' + printf -- '- Installed module: `%s`\n' "$regress_so" + printf -- '- Host pg_regress dlsuffix alias: `%s`\n' "$regress_host_dlsuffix_alias" + } >>"$report" + printf 'built WASIX regress support at %s\n' "$regress_so" +else + { + printf '\n## Result\n\n' + printf -- '- Status: `fail`\n' + printf -- '- Exit code: `%s`\n\n' "$status" + printf 'Treat this as an extension-loading/build blocker, not a PostgreSQL test skip.\n' + } >>"$report" + printf 'WASIX regress support build failed; see %s\n' "$log" >&2 + exit "$status" +fi diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-wasmer-perf-stats.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-wasmer-perf-stats.sh new file mode 100755 index 00000000..3087c0e6 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/build-wasmer-perf-stats.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +fresh_ensure_dirs + +wasmer_root="$FRESH_WORK_ROOT/runtime/wasmer" +target_dir="${WASIX_PERF_STATS_TARGET_DIR:-$wasmer_root/target/perf-stats}" +report="$REPORT_DIR/wasmer-perf-stats-build.md" +features="${WASIX_PERF_STATS_WASMER_FEATURES:-llvm,wat,perf-stats}" + +if [ ! -d "$wasmer_root/lib/cli" ]; then + printf 'missing upstream Wasmer checkout: %s\n' "$wasmer_root" >&2 + exit 2 +fi + +fresh_write_report_header "$report" "Wasmer Perf Stats Build" +{ + printf -- '- Wasmer root: `%s`\n' "$wasmer_root" + printf -- '- Cargo target dir: `%s`\n' "$target_dir" + printf -- '- CLI features: `%s`\n\n' "$features" +} >>"$report" + +( + cd "$wasmer_root" + CARGO_TARGET_DIR="$target_dir" \ + cargo build \ + --manifest-path lib/cli/Cargo.toml \ + --bin wasmer \ + --release \ + --no-default-features \ + --features "$features" +) >"$REPORT_DIR/wasmer-perf-stats-build.log" 2>&1 + +wasmer_bin="$target_dir/release/wasmer" +if [ ! -x "$wasmer_bin" ]; then + printf 'perf-stats Wasmer build did not produce %s\n' "$wasmer_bin" >&2 + exit 1 +fi + +{ + printf '## Result\n\n' + printf -- '- Status: `pass`\n' + printf -- '- Wasmer: `%s`\n' "$(fresh_wasmer_version "$wasmer_bin" 2>/dev/null || true)" + printf -- '- Binary: `%s`\n' "$wasmer_bin" + printf -- '- Binary hash: `%s`\n' "$(fresh_wasmer_bin_hash "$wasmer_bin")" + printf -- '- Build log: `%s`\n' "$REPORT_DIR/wasmer-perf-stats-build.log" +} >>"$report" + +printf '%s\n' "$wasmer_bin" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/capture-linux-cgroup-v2.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/capture-linux-cgroup-v2.py new file mode 100755 index 00000000..a87c4497 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/capture-linux-cgroup-v2.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +"""Capture one strict, hashable cgroup-v2 resource-accounting snapshot.""" + +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +import os +import platform +import re +import stat +import sys +import time +from pathlib import Path +from typing import NoReturn + + +SCHEMA = "oliphaunt.wasix-postmaster.cgroup-v2-snapshot.v1" +UNSIGNED = re.compile(r"^[0-9]+$") +DEVICE = re.compile(r"^[0-9]+:[0-9]+$") +REQUIRED_IO_METRICS = {"rbytes", "wbytes", "rios", "wios"} +FILE_CACHE_MEMORY_STAT_FIELDS = { + "active_file": ("gauge", "bytes"), + "inactive_file": ("gauge", "bytes"), + "file_mapped": ("gauge", "bytes"), + "workingset_refault_file": ("cumulative_counter", "pages"), + "workingset_activate_file": ("cumulative_counter", "pages"), + "workingset_restore_file": ("cumulative_counter", "pages"), + "pgscan": ("cumulative_counter", "pages"), + "pgsteal": ("cumulative_counter", "pages"), +} + + +class SnapshotError(RuntimeError): + pass + + +def fail(message: str) -> NoReturn: + raise SnapshotError(message) + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def read_text(root: Path, name: str) -> str: + path = root / name + info = os.lstat(path) + if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode): + fail(f"cgroup field is not a regular non-symlink file: {path}") + value = path.read_text(encoding="ascii") + if "\0" in value: + fail(f"cgroup field contains NUL: {path}") + return value.strip() + + +def parse_unsigned(value: str, label: str) -> int: + if not UNSIGNED.fullmatch(value): + fail(f"{label} must be an unsigned integer: {value!r}") + return int(value) + + +def parse_limit(value: str, label: str) -> int | str: + if value == "max": + return value + return parse_unsigned(value, label) + + +def parse_flat_map(value: str, label: str) -> dict[str, int]: + result: dict[str, int] = {} + for line in value.splitlines(): + fields = line.split() + if len(fields) != 2 or not fields[0] or fields[0] in result: + fail(f"malformed or duplicate {label} row: {line!r}") + result[fields[0]] = parse_unsigned(fields[1], f"{label}.{fields[0]}") + if not result: + fail(f"{label} is empty") + return result + + +def select_memory_events_file(root: Path) -> tuple[str, str]: + """Select local events only when the captured cgroup is currently a leaf.""" + with os.scandir(root) as entries: + has_child_cgroup = any(entry.is_dir(follow_symlinks=False) for entry in entries) + local_name = "memory.events.local" + if not has_child_cgroup and os.path.lexists(root / local_name): + return local_name, "local-leaf" + if has_child_cgroup: + return "memory.events", "hierarchical-descendants" + return "memory.events", "hierarchical-local-unavailable" + + +def select_file_cache_memory_stat(memory_stat: dict[str, int]) -> dict[str, object]: + fields: dict[str, dict[str, object]] = {} + missing: list[str] = [] + for name, (kind, unit) in FILE_CACHE_MEMORY_STAT_FIELDS.items(): + value = memory_stat.get(name) + if value is None: + missing.append(name) + fields[name] = { + "kind": kind, + "unit": unit, + "status": "available" if value is not None else "missing", + "value": value, + } + return { + "status": "complete" if not missing else "partial", + "missing_keys": missing, + "fields": fields, + } + + +def parse_pressure(value: str) -> dict[str, dict[str, int | float]]: + result: dict[str, dict[str, int | float]] = {} + for line in value.splitlines(): + fields = line.split() + if len(fields) != 5 or fields[0] not in {"some", "full"} or fields[0] in result: + fail(f"malformed or duplicate memory.pressure row: {line!r}") + metrics: dict[str, int | float] = {} + for field in fields[1:]: + if "=" not in field: + fail(f"malformed memory.pressure metric: {field!r}") + name, assigned = field.split("=", 1) + if name == "total": + metrics[name] = parse_unsigned(assigned, f"memory.pressure.{fields[0]}.total") + elif name in {"avg10", "avg60", "avg300"}: + try: + number = float(assigned) + except ValueError as error: + raise SnapshotError(f"invalid memory.pressure value: {field!r}") from error + if number < 0: + fail(f"negative memory.pressure value: {field!r}") + metrics[name] = number + else: + fail(f"unknown memory.pressure metric: {name}") + if set(metrics) != {"avg10", "avg60", "avg300", "total"}: + fail(f"incomplete memory.pressure row: {line!r}") + result[fields[0]] = metrics + if set(result) != {"some", "full"}: + fail("memory.pressure must contain exactly some and full rows") + return result + + +def parse_io_stat(value: str) -> tuple[list[dict[str, object]], dict[str, int]]: + devices: list[dict[str, object]] = [] + totals: dict[str, int] = {} + seen: set[str] = set() + for line in value.splitlines(): + fields = line.split() + if len(fields) < 2 or not DEVICE.fullmatch(fields[0]) or fields[0] in seen: + fail(f"malformed or duplicate io.stat row: {line!r}") + seen.add(fields[0]) + metrics: dict[str, int] = {} + for field in fields[1:]: + if "=" not in field: + fail(f"malformed io.stat metric: {field!r}") + name, assigned = field.split("=", 1) + if not name or name in metrics: + fail(f"duplicate or empty io.stat metric: {field!r}") + metrics[name] = parse_unsigned(assigned, f"io.stat.{fields[0]}.{name}") + totals[name] = totals.get(name, 0) + metrics[name] + missing = REQUIRED_IO_METRICS - set(metrics) + if missing: + fail(f"io.stat.{fields[0]} lacks required metrics: {sorted(missing)}") + devices.append({"device": fields[0], "metrics": dict(sorted(metrics.items()))}) + for name in REQUIRED_IO_METRICS: + totals.setdefault(name, 0) + return devices, dict(sorted(totals.items())) + + +def capture_io_stat(root: Path, controllers: list[str]) -> dict[str, object]: + controller_status = "available" if "io" in controllers else "missing" + path = root / "io.stat" + if not os.path.lexists(path): + return { + "status": "unavailable", + "controller_status": controller_status, + "missing_reason": ( + "io-controller-missing" + if controller_status == "missing" + else "io-stat-missing" + ), + "source": None, + "devices": None, + "totals": None, + } + devices, totals = parse_io_stat(read_text(root, "io.stat")) + return { + "status": "available", + "controller_status": controller_status, + "missing_reason": None, + "source": "io.stat", + "devices": devices, + "totals": totals, + } + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cgroup-dir", required=True, type=Path) + parser.add_argument("--cgroup-identity", required=True) + parser.add_argument("--output", required=True, type=Path) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + if platform.system() != "Linux": + fail("cgroup-v2 resource capture requires Linux") + root = args.cgroup_dir.absolute() + info = os.lstat(root) + if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode): + fail(f"cgroup root must be a non-symlink directory: {root}") + try: + relative = root.relative_to(Path("/sys/fs/cgroup")) + except ValueError as error: + raise SnapshotError(f"cgroup must be below /sys/fs/cgroup: {root}") from error + cgroup_path = "/" + str(relative) + if any(character in args.cgroup_identity for character in "\t\r\n\0"): + fail("cgroup identity contains control separators") + + memory_stat = parse_flat_map(read_text(root, "memory.stat"), "memory.stat") + memory_events_name, memory_events_scope = select_memory_events_file(root) + memory_events = parse_flat_map( + read_text(root, memory_events_name), memory_events_name + ) + pressure = parse_pressure(read_text(root, "memory.pressure")) + controllers = sorted(read_text(root, "cgroup.controllers").split()) + io = capture_io_stat(root, controllers) + required_stat = { + "anon", + "file", + "kernel", + "pagetables", + "file_dirty", + "file_writeback", + } + required_events = {"high", "max", "oom", "oom_kill"} + if required_stat - set(memory_stat): + fail(f"memory.stat lacks required fields: {sorted(required_stat - set(memory_stat))}") + if required_events - set(memory_events): + fail(f"memory.events lacks required fields: {sorted(required_events - set(memory_events))}") + + output = args.output.absolute() + if output == root or root in output.parents: + fail(f"snapshot output must be outside the cgroup filesystem: {output}") + captured_monotonic_ns = time.monotonic_ns() + snapshot = { + "schema_version": SCHEMA, + "status": "passed", + "captured_utc": dt.datetime.now(dt.timezone.utc).isoformat(timespec="microseconds"), + "captured_monotonic_ns": captured_monotonic_ns, + "tool_sha256": sha256_file(Path(__file__)), + "host": { + "system": platform.system(), + "release": platform.release(), + "machine": platform.machine(), + }, + "cgroup": { + "path": cgroup_path, + "identity": args.cgroup_identity, + "type": read_text(root, "cgroup.type"), + "controllers": controllers, + "pids_current": parse_unsigned(read_text(root, "pids.current"), "pids.current"), + }, + "memory": { + "current": parse_unsigned(read_text(root, "memory.current"), "memory.current"), + "peak": parse_unsigned(read_text(root, "memory.peak"), "memory.peak"), + "max": parse_limit(read_text(root, "memory.max"), "memory.max"), + "high": parse_limit(read_text(root, "memory.high"), "memory.high"), + "swap_current": parse_unsigned( + read_text(root, "memory.swap.current"), "memory.swap.current" + ), + "swap_peak": parse_unsigned(read_text(root, "memory.swap.peak"), "memory.swap.peak"), + "swap_max": parse_limit(read_text(root, "memory.swap.max"), "memory.swap.max"), + "stat": dict(sorted(memory_stat.items())), + "file_cache": select_file_cache_memory_stat(memory_stat), + "events": dict(sorted(memory_events.items())), + "events_source": { + "file": memory_events_name, + "scope": memory_events_scope, + }, + "pressure": pressure, + }, + "io": io, + } + output.parent.mkdir(parents=True, exist_ok=True) + pending = output.with_name(f".{output.name}.pending.{os.getpid()}") + with pending.open("x", encoding="utf-8", newline="\n") as stream: + json.dump(snapshot, stream, indent=2, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + os.replace(pending, output) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (SnapshotError, OSError, ValueError) as error: + print(f"cgroup-v2 snapshot failed: {error}", file=sys.stderr) + raise SystemExit(1) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/check-prior-art.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/check-prior-art.sh new file mode 100755 index 00000000..b9b00f3c --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/check-prior-art.sh @@ -0,0 +1,826 @@ +#!/usr/bin/env bash + +set -euo pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO_ROOT="$(cd "$PROJECT_ROOT/../../../.." && pwd)" +PATCH_ROOT="$PROJECT_ROOT/postgres/patches" +SERIES_FILE="$PATCH_ROOT/series" +CHECK_TEMP="$(mktemp -d)" +trap 'rm -rf -- "$CHECK_TEMP"' EXIT HUP INT TERM + +fail() { + printf 'wasix-postmaster check: %s\n' "$*" >&2 + exit 1 +} + +require_text() { + local path="$1" + local expected="$2" + grep -Fq -- "$expected" "$path" || fail "$path must contain: $expected" +} + +forbid_text() { + local path="$1" + local forbidden="$2" + if grep -Fq -- "$forbidden" "$path"; then + fail "$path must not contain: $forbidden" + fi +} + +require_moon_task_text() { + local task="$1" + local expected="$2" + + awk -v task=" $task:" -v expected="$expected" ' + /^ [[:alnum:]][[:alnum:]-]*:$/ { in_task = ($0 == task) } + in_task && index($0, expected) { found = 1 } + END { exit(found ? 0 : 1) } + ' "$PROJECT_ROOT/moon.yml" || + fail "moon task $task must contain: $expected" +} + +for required in \ + "$PROJECT_ROOT/README.md" \ + "$PROJECT_ROOT/docs/architecture.md" \ + "$PROJECT_ROOT/docs/libpq-latency-qualification.md" \ + "$PROJECT_ROOT/docs/checkpoint-recycle-qualification.md" \ + "$PROJECT_ROOT/docs/cold-ownership-qualification.md" \ + "$PROJECT_ROOT/docs/shared-memory-backing-experiment.md" \ + "$PROJECT_ROOT/docs/sealed-export-closure.md" \ + "$PROJECT_ROOT/docs/semantic-cache-offers.md" \ + "$PROJECT_ROOT/docs/semantic-wal-cache-offers.md" \ + "$PROJECT_ROOT/docs/replay-status.md" \ + "$PROJECT_ROOT/postgres/product-patch-provenance.toml" \ + "$PROJECT_ROOT/sources.lock.toml" \ + "$PROJECT_ROOT/moon.yml" \ + "$SERIES_FILE" \ + "$PATCH_ROOT/0006-wasix-retry-proc-join-on-eintr.patch" \ + "$PATCH_ROOT/0007-wasix-semantic-relation-cache-offers.patch" \ + "$PATCH_ROOT/0007-wasix-semantic-relation-cache-offers.test.py" \ + "$PATCH_ROOT/0008-wasix-packed-atomic-latch-state.patch" \ + "$PATCH_ROOT/0008-wasix-packed-atomic-latch-state.test.py" \ + "$PATCH_ROOT/0009-wasix-inactive-durable-wal-cache-offer.patch" \ + "$PATCH_ROOT/0009-wasix-inactive-durable-wal-cache-offer.test.py" \ + "$PROJECT_ROOT/runtime/capabilities.tsv" \ + "$PROJECT_ROOT/runtime/bin/build-runtime.sh" \ + "$PROJECT_ROOT/runtime/patches/wasmer/0001-postgres-wasix-blockers.patch" \ + "$PROJECT_ROOT/runtime/bin/prepare-upstream-checkouts.sh" \ + "$PROJECT_ROOT/runtime/bin/verify-source-lock.py" \ + "$PROJECT_ROOT/runtime/bin/verify-source-lock.test.py" \ + "$PROJECT_ROOT/runtime/bin/verify-runtime-state-ownership.py" \ + "$PROJECT_ROOT/runtime/bin/verify-runtime-state-ownership.test.py" \ + "$PROJECT_ROOT/runtime/bin/verify-runtime-execution-ownership.py" \ + "$PROJECT_ROOT/runtime/bin/verify-runtime-execution-ownership.test.py" \ + "$PROJECT_ROOT/runtime/bin/verify-postmaster-wasm-import.py" \ + "$PROJECT_ROOT/runtime/bin/verify-postmaster-wasm-import.test.py" \ + "$PROJECT_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" \ + "$PROJECT_ROOT/runtime/bin/verify-postmaster-concurrency-contract.test.py" \ + "$PROJECT_ROOT/runtime/policies/sealed-main-runtime-exports.v1.txt" \ + "$PROJECT_ROOT/runtime/policies/sealed-main-dlsym-exports.v1.txt" \ + "$PROJECT_ROOT/runtime/policies/sealed-side-modules.v1.tsv" \ + "$PROJECT_ROOT/tools/sealed-export-closure/Cargo.toml" \ + "$PROJECT_ROOT/tools/sealed-export-closure/Cargo.lock" \ + "$PROJECT_ROOT/tools/sealed-export-closure/src/main.rs" \ + "$PROJECT_ROOT/bin/build-wasix-core.sh" \ + "$PROJECT_ROOT/bin/seal-wasix-core-exports.sh" \ + "$PROJECT_ROOT/bin/seal-wasix-core-exports.test.sh" \ + "$PROJECT_ROOT/bin/seal-wasix-core-exports.transaction.test.sh" \ + "$PROJECT_ROOT/bin/build-sealed-headless-carrier.sh" \ + "$PROJECT_ROOT/bin/build-sealed-headless-carrier.test.sh" \ + "$PROJECT_ROOT/bin/precompile-wasix-core.sh" \ + "$PROJECT_ROOT/bin/seal-wasix-linear-memory.sh" \ + "$PROJECT_ROOT/bin/seal-wasix-linear-memory.test.sh" \ + "$PROJECT_ROOT/lib/verify-sealed-carrier.test.py" \ + "$PROJECT_ROOT/lib/durable_publication.py" \ + "$PROJECT_ROOT/lib/durable_publication.test.py" \ + "$PROJECT_ROOT/lib/durable_publication_crash.test.py" \ + "$PROJECT_ROOT/lib/guest_build_provenance.py" \ + "$PROJECT_ROOT/lib/guest_build_provenance.test.py" \ + "$PROJECT_ROOT/lib/linear_memory_transaction.py" \ + "$PROJECT_ROOT/lib/linear_memory_transaction.test.py" \ + "$PROJECT_ROOT/lib/sealed_export_chain.py" \ + "$PROJECT_ROOT/lib/sealed_export_chain.test.py" \ + "$PROJECT_ROOT/bin/current-evidence-manifest.py" \ + "$PROJECT_ROOT/bin/run-frozen-measurement.py" \ + "$PROJECT_ROOT/bin/run-frozen-measurement.test.py" \ + "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" \ + "$PROJECT_ROOT/bin/bench-shared-memory-provider-options.test.sh" \ + "$PROJECT_ROOT/bin/bench-memory-budget-options.test.sh" \ + "$PROJECT_ROOT/bin/prove-linux-cold-residency.py" \ + "$PROJECT_ROOT/bin/capture-linux-cgroup-v2.py" \ + "$PROJECT_ROOT/bin/classify-linux-file-residency.py" \ + "$PROJECT_ROOT/bin/classify-linux-file-residency.test.py" \ + "$PROJECT_ROOT/bin/validate-sealed-loader-audit.py" \ + "$PROJECT_ROOT/bin/validate-sealed-loader-audit.test.py" \ + "$PROJECT_ROOT/bin/validate-file-cache-telemetry.py" \ + "$PROJECT_ROOT/bin/validate-file-cache-telemetry.test.py" \ + "$PROJECT_ROOT/bin/validate-adaptive-file-cache-telemetry.py" \ + "$PROJECT_ROOT/bin/validate-adaptive-file-cache-telemetry.test.py" \ + "$PROJECT_ROOT/bin/validate-wasix-cold-ownership.py" \ + "$PROJECT_ROOT/bin/summarize-wasix-cold-ownership.py" \ + "$PROJECT_ROOT/bin/cold-ownership.test.py" \ + "$PROJECT_ROOT/bin/qualify-wasix-cold-ownership.sh" \ + "$PROJECT_ROOT/bin/qualify-wasix-cold-ownership.test.sh" \ + "$PROJECT_ROOT/bin/compare-postgres-settings.py" \ + "$PROJECT_ROOT/bin/compare-postgres-settings.test.py" \ + "$PROJECT_ROOT/bin/delta-pg-stat-io.py" \ + "$PROJECT_ROOT/bin/delta-pg-stat-io.test.py" \ + "$PROJECT_ROOT/bin/resource-evidence.test.sh" \ + "$PROJECT_ROOT/bin/resource-monitor-race-retry.test.sh" \ + "$PROJECT_ROOT/bin/process-tree-collector.test.sh" \ + "$PROJECT_ROOT/bin/connected-client-gate.test.sh" \ + "$PROJECT_ROOT/bin/summarize-libpq-latency.sh" \ + "$PROJECT_ROOT/bin/summarize-libpq-latency.test.sh" \ + "$PROJECT_ROOT/bin/compare-libpq-latency.py" \ + "$PROJECT_ROOT/bin/compare-libpq-latency.test.py" \ + "$PROJECT_ROOT/bin/validate-host-fd-churn.sh" \ + "$PROJECT_ROOT/bin/validate-host-fd-churn.test.sh" \ + "$PROJECT_ROOT/bin/validate-memory-evidence.sh" \ + "$PROJECT_ROOT/bin/validate-memory-evidence.test.sh" \ + "$PROJECT_ROOT/bin/validate-wasix-lifecycle-plateau.py" \ + "$PROJECT_ROOT/bin/validate-wasix-lifecycle-plateau.test.py" \ + "$PROJECT_ROOT/bin/validate-wasix-lifecycle-memory-plateau.py" \ + "$PROJECT_ROOT/bin/validate-wasix-lifecycle-memory-plateau.test.py" \ + "$PROJECT_ROOT/bin/lifecycle-memory-checkpoint.test.sh" \ + "$PROJECT_ROOT/bin/freeze-wasix-lifecycle-evidence.py" \ + "$PROJECT_ROOT/bin/freeze-wasix-lifecycle-evidence.test.py" \ + "$PROJECT_ROOT/bin/freeze-wasix-lifecycle-policy.py" \ + "$PROJECT_ROOT/bin/freeze-wasix-lifecycle-policy.test.py" \ + "$PROJECT_ROOT/bin/compose-wasix-embedded-performance-viability.py" \ + "$PROJECT_ROOT/bin/compose-wasix-embedded-performance-viability.test.py" \ + "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" \ + "$PROJECT_ROOT/bin/qualify-wasix-single-backend.test.sh" \ + "$PROJECT_ROOT/bin/qualify-wasix-libpq-latency.sh" \ + "$PROJECT_ROOT/bin/qualify-wasix-libpq-latency.test.sh" \ + "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.sh" \ + "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.test.sh" \ + "$PROJECT_ROOT/bin/qualify-wasix-checkpoint-recycle.sh" \ + "$PROJECT_ROOT/bin/qualify-wasix-checkpoint-recycle.test.sh" \ + "$PROJECT_ROOT/bin/validate-checkpoint-recycle.py" \ + "$PROJECT_ROOT/bin/validate-checkpoint-recycle.test.py" \ + "$PROJECT_ROOT/bin/validate-checkpoint-memory.py" \ + "$PROJECT_ROOT/bin/validate-checkpoint-memory.test.py" \ + "$PROJECT_ROOT/bin/validate-wal-recycle.py" \ + "$PROJECT_ROOT/bin/validate-wal-recycle.test.py" \ + "$PROJECT_ROOT/bin/extract-checkpoint-summary.py" \ + "$PROJECT_ROOT/bin/extract-checkpoint-summary.test.py" \ + "$PROJECT_ROOT/bin/summarize-checkpoint-qualification.py" \ + "$PROJECT_ROOT/bin/summarize-checkpoint-qualification.test.py" \ + "$PROJECT_ROOT/bin/verify-sealed-headless-carrier.sh" \ + "$PROJECT_ROOT/lib/postgres-profiles.sh" \ + "$PROJECT_ROOT/lib/postgres-profiles.test.sh" \ + "$PROJECT_ROOT/lib/shared_memory_provider.py" \ + "$PROJECT_ROOT/lib/shared_memory_provider.test.py" \ + "$PROJECT_ROOT/lib/qualification-identities.sh" \ + "$PROJECT_ROOT/lib/sealed-carrier.sh" \ + "$PROJECT_ROOT/lib/host-fd-telemetry.sh" \ + "$PROJECT_ROOT/lib/host-fd-telemetry.test.sh" \ + "$PROJECT_ROOT/lib/process-supervision.sh" \ + "$PROJECT_ROOT/lib/process-supervision.test.sh" \ + "$PROJECT_ROOT/lib/signal-owned-pid.py" \ + "$PROJECT_ROOT/lib/signal-owned-pid.test.py" \ + "$PROJECT_ROOT/lib/server-lifecycle.sh" \ + "$PROJECT_ROOT/lib/server-lifecycle.test.sh" \ + "$PROJECT_ROOT/lib/verify-sealed-carrier.py" \ + "$PROJECT_ROOT/testdata/fake-postmaster-compiler.py" \ + "$PROJECT_ROOT/testdata/make-sealed-export-fixture.py" \ + "$PROJECT_ROOT/testdata/fake-sealed-wasmer.py" \ + "$PROJECT_ROOT/probes/libpq_latency_probe.c" \ + "$PROJECT_ROOT/probes/libpq_checkpoint_probe.c" \ + "$PROJECT_ROOT/bench/sql/checkpoint-workload-setup.sql" \ + "$PROJECT_ROOT/bench/sql/checkpoint-volume.sql" \ + "$PROJECT_ROOT/bench/sql/checkpoint-database-state.sql" \ + "$PROJECT_ROOT/runtime/bin/run-exec-backend-probes.sh" \ + "$PROJECT_ROOT/runtime/probes/directory_fsync_probe.c" \ + "$PROJECT_ROOT/runtime/probes/epoll_ofd_lifecycle_probe.c" \ + "$PROJECT_ROOT/runtime/probes/exec_shared_latch_sigurg_probe.c" \ + "$PROJECT_ROOT/runtime/probes/sync_file_range_probe.c" \ + "$PROJECT_ROOT/profiles/runtime-footprints/embedded-concurrent-v1.gucs" \ + "$PROJECT_ROOT/profiles/durability/safe-v1.gucs" \ + "$PROJECT_ROOT/profiles/checkpoint-policies/embedded-steady-v1.gucs" \ + "$PROJECT_ROOT/profiles/checkpoint-policies/embedded-steady-v1.tsv" \ + "$PROJECT_ROOT/profiles/memory-budgets/embedded-c4-lower-pressure-v1.tsv" \ + "$PROJECT_ROOT/profiles/lifecycle-baselines/relative-stabilized-idle-postmaster-candidate-v1.tsv" \ + "$PROJECT_ROOT/profiles/qualification/embedded-performance-viability-candidate-v1.tsv" \ + "$PROJECT_ROOT/bin/stress-wasix-initdb.sh" \ + "$PROJECT_ROOT/bin/stress-wasix-backend-waves.sh" \ + "$PROJECT_ROOT/bin/summarize-linux-smaps.sh" \ + "$PROJECT_ROOT/bin/summarize-linux-smaps.test.sh" \ + "$REPO_ROOT/src/sources/third-party/wasix-postmaster/wasmer.toml" \ + "$REPO_ROOT/src/sources/third-party/wasix-postmaster/wasmer-napi.toml" \ + "$REPO_ROOT/src/sources/third-party/wasix-postmaster/wasmer-test-files.toml" \ + "$REPO_ROOT/src/sources/third-party/wasix-postmaster/webassembly-testsuite.toml" \ + "$REPO_ROOT/src/sources/third-party/wasix-postmaster/wasix-libc.toml" \ + "$REPO_ROOT/src/sources/toolchains/wasix.toml" \ + "$REPO_ROOT/src/postgres/versions/18/source.toml" \ + "$REPO_ROOT/src/runtimes/liboliphaunt/wasix/assets/build/docker/Dockerfile" \ + "$REPO_ROOT/src/runtimes/liboliphaunt/wasix/assets/build/docker_wasix_env.sh" +do + [ -f "$required" ] || fail "missing required file: $required" +done + +require_text "$REPO_ROOT/src/postgres/versions/18/source.toml" 'version = "18.4"' +require_text "$PROJECT_ROOT/sources.lock.toml" 'version = "18.4"' +require_text "$PROJECT_ROOT/sources.lock.toml" 'tag = "REL_18_4"' +require_text "$PROJECT_ROOT/sources.lock.toml" 'commit = "1d1b3420beef28550afbb4692b664bd7f6bc2581"' +require_text "$PROJECT_ROOT/sources.lock.toml" 'napi_commit = "706383f42391cb4e4e82e5fd5e63a0ebf81ae19d"' +require_text "$PROJECT_ROOT/sources.lock.toml" 'commit = "34178a6272804f90448b5bd08dc7bcf0d85438e3"' +require_text "$REPO_ROOT/src/sources/third-party/wasix-postmaster/wasmer.toml" 'commit = "1d1b3420beef28550afbb4692b664bd7f6bc2581"' +require_text "$REPO_ROOT/src/sources/third-party/wasix-postmaster/wasmer-napi.toml" 'commit = "706383f42391cb4e4e82e5fd5e63a0ebf81ae19d"' +require_text "$REPO_ROOT/src/sources/third-party/wasix-postmaster/wasmer-test-files.toml" 'commit = "7f27e84c69af3b772f751d6c4a733d9f448b2c70"' +require_text "$REPO_ROOT/src/sources/third-party/wasix-postmaster/webassembly-testsuite.toml" 'commit = "7e0b83aba9dbbb6e0623c9334b0f73b3bb584b90"' +require_text "$REPO_ROOT/src/sources/third-party/wasix-postmaster/wasix-libc.toml" 'commit = "34178a6272804f90448b5bd08dc7bcf0d85438e3"' +require_text "$PROJECT_ROOT/lib/common.sh" 'target/oliphaunt-wasix-postmaster' +require_text "$PROJECT_ROOT/lib/common.sh" 'fresh_project_source_identity_path' +require_text "$PROJECT_ROOT/lib/common.sh" 'src/runtimes/liboliphaunt/wasix-postmaster' +require_text "$PROJECT_ROOT/lib/common.sh" 'REL_18_4' +require_text "$PROJECT_ROOT/lib/common.sh" 'WASIXCC_SYSROOT_VARIANT:-sysroot-exnref-ehpic' +require_text "$PROJECT_ROOT/lib/common.sh" 'oliphaunt.wasix-postmaster.wasmer-build.v2' +require_text "$PROJECT_ROOT/lib/common.sh" 'fresh_require_local_wasmer_build_state' +require_text "$PROJECT_ROOT/lib/common.sh" 'fresh_require_patched_wasmer_headless' +require_text "$PROJECT_ROOT/lib/common.sh" 'fresh_require_patched_postmaster_executor' +require_text "$PROJECT_ROOT/lib/common.sh" 'oliphaunt.wasix-postmaster.postmaster-executor-build.v3' +require_text "$PROJECT_ROOT/lib/common.sh" 'fresh_require_start_proof_tool' +require_text "$PROJECT_ROOT/lib/common.sh" 'fresh_require_memory_profile_tool' +require_text "$PROJECT_ROOT/lib/common.sh" 'fresh_require_patched_postmaster_compiler' +require_text "$PROJECT_ROOT/lib/common.sh" 'wasm32-max256m-u64-static4g-guard2g.v1' +require_text "$PROJECT_ROOT/lib/common.sh" 'fresh_validate_postmaster_task_budget_profile' +require_text "$PROJECT_ROOT/lib/common.sh" 'embedded-postmaster-v1-budget96.v2' +require_text "$PROJECT_ROOT/profiles/runtime-task-budgets/embedded-postmaster-v1.tsv" $'\t90\t1\t5\t96\t1\t1000' +require_text "$PROJECT_ROOT/lib/common.sh" 'FRESH_WASMER_WASIX_VERSION:-0.702.0-alpha.2' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'WASMER_BUILD_RECEIPT_OUT' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" '--locked' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" '--target-dir' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'fresh_require_local_wasmer_build_state' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'OLIPHAUNT_WASIX_RUNTIME_ABI_ID' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'file_advice' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'syscalls::wasi::fd_advise::tests' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'host_file_range_writeback' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'syscalls::wasix::fd_sync_range::tests' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'required_import_tests' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'require_listed_test' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'verify-source-lock.py' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'postmaster_compiler_binary=' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'generic-baseline' +require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" 'verify-runtime-state-ownership.py' +require_text "$PROJECT_ROOT/runtime/bin/prepare-upstream-checkouts.sh" 'verify-source-lock.py' +require_text "$PROJECT_ROOT/bin/build-wasix-core.sh" 'seal-wasix-core-exports.sh' +require_text "$PROJECT_ROOT/bin/seal-wasix-core-exports.sh" '--remove-unused-module-elements' +require_text "$PROJECT_ROOT/bin/seal-wasix-core-exports.sh" 'attest-final' +require_text "$PROJECT_ROOT/tools/sealed-export-closure/src/main.rs" 'structural-estimate-not-measured-rss' +require_text "$PROJECT_ROOT/tools/sealed-export-closure/src/main.rs" 'required_global_value_type' +require_text "$PROJECT_ROOT/tools/sealed-export-closure/src/main.rs" 'final exports do not exactly equal the derived seed closure' +require_text "$PROJECT_ROOT/runtime/policies/sealed-main-runtime-exports.v1.txt" 'WaitEventSetWait' +require_text "$PROJECT_ROOT/runtime/policies/sealed-side-modules.v1.tsv" 'lib/postgresql/plpgsql.so' +closure_tool_source="src/runtimes/liboliphaunt/wasix-postmaster/tools/sealed-export-closure/src/main.rs" +closure_tool_target="src/runtimes/liboliphaunt/wasix-postmaster/tools/sealed-export-closure/target/debug/tool" +if git -C "$REPO_ROOT" check-ignore -q -- "$closure_tool_source"; then + fail 'sealed export closure source is ignored by product policy' +fi +git -C "$REPO_ROOT" check-ignore -q -- "$closure_tool_target" || + fail 'sealed export closure Cargo target is not ignored by product policy' +require_text "$PROJECT_ROOT/runtime/bin/run-upstream-checks.sh" 'NON-QUALIFYING' +require_text "$PROJECT_ROOT/runtime/bin/run-upstream-checks.sh" 'build-runtime.sh' +for exact_runtime_test in \ + 'fd_sync_range_maps_all_exact_flag_combinations' \ + 'fd_sync_range_rejects_negative_overflowing_and_unknown_ranges' \ + 'fd_sync_range_observability_classifies_the_complete_signed_range' \ + 'fd_sync_range_preserves_zero_length_and_maximum_finite_range' \ + 'fd_sync_range_maps_unsupported_backends_to_nosys' \ + 'fd_sync_range_read_only_advice_right_is_accepted' \ + 'fd_sync_range_directory_is_badf' \ + 'fd_sync_range_preserves_linux_writeback_errnos' \ + 'child_execution_admission_requires_exact_publication' \ + 'failed_launch_rollback_waits_for_real_execution_quiescence' \ + 'execution_guard_publishes_terminal_before_quiescence' \ + 'abandoned_host_execution_fails_closed_only_after_last_guard_clone' \ + 'supplemental_parent_guard_requires_an_accepted_successor' \ + 'vfork_parent_and_child_ownership_coexist_across_deep_sleep_handoffs' \ + 'panicking_monitor_manager_terminates_and_reaps_before_quiescence' \ + 'reinit_requires_terminal_status_and_execution_quiescence' \ + 'descendant_validation_failure_seals_no_process_in_tree' \ + 'repeated_child_construction_keeps_main_handle_owned_by_child' \ + 'canceled_task_wasm_terminalizes_exact_thread_before_quiescence' \ + 'closed_worker_queue_drops_pending_execution_fail_closed' \ + 'accepted_callback_conversion_is_the_only_successful_disarm' \ + 'accepted_callback_panic_terminalizes_before_quiescence' \ + 'deep_sleep_handoff_keeps_thread_live_until_successor_guard_finishes' \ + 'non_core_workers_retire_after_the_idle_timeout' \ + 'memory_construction_failure_terminalizes_before_releasing_lease' \ + 'instantiation_failure_terminalizes_before_releasing_lease' \ + 'panicking_custom_task_wasm_callback_terminalizes_before_quiescence' \ + 'module_start_observes_child_parent_after_exact_publication' \ + 'run_exec_panic_terminalizes_before_releasing_accepted_guard' \ + 'absent_pid_returns_srch_for_liveness_probe' \ + 'signal_zero_observes_existing_pid_without_delivery' \ + 'real_signal_delivery_is_unchanged' \ + 'shared_futex_registry_uses_containing_mapping_only' \ + 'runtime_state_snapshot_reports_compact_occupancy_without_pruning' \ + 'shared_futex_registry_reuses_same_live_file' \ + 'shared_futex_registry_pins_file_identity_until_last_live_reference' \ + 'shared_futex_registry_replaces_entry_after_last_live_drop' \ + 'shared_futex_registry_last_drop_racing_lookup_keeps_exact_replacement' \ + 'forked_states_share_mapping_registry_and_return_to_zero_plateau' \ + 'repeated_shared_futex_registry_churn_returns_to_slot_and_fd_plateau' \ + 'shared_futex_registry_old_generation_cannot_remove_replacement' \ + 'shared_futex_registry_prunes_stale_slots_in_bounded_order' \ + 'direct_signal_controller_is_platform_neutral_and_rejects_finished_tasks' \ + 'unix_supervisor_real_signals_restore_and_route_exclusively' \ + 'wasi_runner_never_owns_host_lifecycle_by_default' \ + 'lifecycle_bind_failure_kills_and_reaps_spawned_root' \ + 'bind_panic_terminates_and_reaps_spawned_root' \ + 'dropping_admitted_watcher_terminates_and_reaps_spawned_root' \ + 'direct_root_spawn_has_no_effect_before_watcher_admission' \ + 'runtime_policy_identity_requires_the_exact_postmaster_closure' \ + 'runtime_policy_identity_parser_rejects_unknown_manifest_fields' \ + 'runtime_policy_identity_selects_only_product_executables' \ + 'product_runner_applies_the_same_guest_and_host_task_budget' \ + 'blocking_worker_growth_is_bounded_by_the_host_task_budget' \ + 'sealed_postmaster_policy_id_and_worker_configuration_are_stable' \ + 'sealed_postmaster_runtime_has_exactly_two_tokio_workers' \ + 'generic_runtime_policy_retains_tokio_default_worker_selection' +do + require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" "$exact_runtime_test" +done +for exact_runtime_test in \ + 'cache_advice_generation_is_descriptor_relative_and_detects_mutation' \ + 'pinned_file_is_owned_path_free_and_fails_closed_after_mutation' \ + 'normal_sync_and_validation_fast_paths_do_not_wait_for_policy_mutex' \ + 'contended_action_gate_never_blocks_relation_and_only_queues_proven_wal' \ + 'wal_requires_canonical_complete_segment_without_alignment_trimming' \ + 'one_hundred_wal_segments_are_bounded_and_only_one_acts_per_fresh_sample' \ + 'deferred_wal_capacity_includes_inflight_budget_and_evicts_oldest' \ + 'deferred_test_lock_contention_drops_once_with_exact_conservation' \ + 'deferred_generation_invalidation_is_benign_and_advice_error_is_terminal' \ + 'sampler_clock_and_breaker_degradation_flush_deferred_ownership' \ + 'maintenance_expires_deferred_pin_without_an_advice_trigger' \ + 'finalization_flushes_queue_and_publishes_terminal_zero_receipt' \ + 'finalization_rejects_a_previously_published_live_snapshot' \ + 'threaded_finalization_waits_for_action_then_publishes_terminal_zero' \ + 'admission_word_and_seqcst_wait_protocol_cover_adversarial_interleavings' \ + 'runtime_policy_identity_approved_config_hash_matches_compiled_default_telemetry' \ + 'process_tree_join_precedes_one_shot_product_evidence_finalization' +do + require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" "$exact_runtime_test" +done +for exact_runtime_test in \ + 'wait_dump_config_is_disabled_without_both_interval_and_file' \ + 'wait_dump_zero_limit_means_unlimited_and_verbose_is_explicit' \ + 'wait_dump_order_is_strictly_monotonic' \ + 'runtime_state_record_schema_and_field_order_are_exact' +do + require_text "$PROJECT_ROOT/runtime/bin/build-runtime.sh" "$exact_runtime_test" +done +require_text "$PROJECT_ROOT/bin/build-wasix-core.sh" '#define HAVE_SYNC_FILE_RANGE 1' +require_text "$PROJECT_ROOT/bin/build-wasix-core.sh" 'verify-postmaster-wasm-import.py' +require_text "$PROJECT_ROOT/bin/build-wasix-core.sh" 'verify-postmaster-concurrency-contract.py' +require_text "$PROJECT_ROOT/bin/build-wasix-core.sh" 'WASIXCC_WASM_OPT_SUPPRESS_DEFAULT' +require_text "$PROJECT_ROOT/bin/build-wasix-core.sh" 'EXPECTED_ATOMIC_FENCE_TOTAL' +require_text "$PROJECT_ROOT/lib/common.sh" 'FRESH_WASIX_CORE_PROFILE_EXPECTED_ATOMIC_FENCE_TOTAL="1111"' +require_text "$PROJECT_ROOT/lib/common.sh" 'FRESH_WASIX_CORE_PROFILE_EXPECTED_FINAL_ATOMIC_FENCE_TOTAL="995"' +require_text "$PROJECT_ROOT/bin/build-sealed-headless-carrier.sh" 'oliphaunt.wasix-postmaster.sealed-aot.v5' +require_text "$PROJECT_ROOT/bin/build-sealed-headless-carrier.sh" 'postmaster-executor.receipt' +require_text "$PROJECT_ROOT/bin/build-sealed-headless-carrier.sh" '--executor-role' +require_text "$PROJECT_ROOT/bin/build-sealed-headless-carrier.sh" 'guest-build-recipe-sha256' +require_text "$PROJECT_ROOT/bin/build-wasix-core.sh" 'oliphaunt.wasix-postmaster.guest-build.v4' +require_text "$PROJECT_ROOT/bin/build-wasix-core.sh" 'wasix-postmaster.final-wasm-concurrency.receipt' +require_text "$PROJECT_ROOT/bin/build-wasix-core.sh" 'seal-wasix-linear-memory.sh' +require_text "$PROJECT_ROOT/bin/build-wasix-core.sh" 'seal-identity "$WASIX_INSTALL_DIR"' +require_text "$PROJECT_ROOT/bin/build-wasix-core.sh" 'durable_publication.py' +require_text "$PROJECT_ROOT/lib/durable_publication.py" 'os.O_EXCL' +require_text "$PROJECT_ROOT/lib/durable_publication.py" 'directory.fsync()' +require_text "$PROJECT_ROOT/lib/guest_build_provenance.py" 'synchronize_root_entry' +require_text "$PROJECT_ROOT/bin/seal-wasix-linear-memory.sh" 'linear_memory_transaction.py' +require_text "$PROJECT_ROOT/bin/seal-wasix-linear-memory.sh" '--allow-linear-memory-descendant' +require_text "$PROJECT_ROOT/bin/seal-wasix-linear-memory.sh" 'flock -n' +require_text "$PROJECT_ROOT/lib/linear_memory_transaction.py" 'linear-memory-transaction.v1' +require_text "$PROJECT_ROOT/lib/linear_memory_transaction.py" 'os.link(aggregate_source, aggregate_destination' +require_text "$PROJECT_ROOT/lib/sealed_export_chain.py" 'O_NOFOLLOW' +require_text "$PROJECT_ROOT/lib/sealed_export_chain.py" 'analyzer version differs from structural receipt' +require_text "$PROJECT_ROOT/bin/precompile-wasix-core.sh" 'verify-aot' +require_text "$PROJECT_ROOT/bin/precompile-wasix-core.sh" 'refuses pinned or foreign cache roots' +require_text "$PROJECT_ROOT/bin/build-sealed-headless-carrier.sh" 'fresh_atomic_publish_directory_noreplace' +require_text "$PROJECT_ROOT/bin/build-sealed-headless-carrier.sh" 'refuse pinned or foreign AOT cache roots' +require_text "$PROJECT_ROOT/runtime/patches/wasmer/0001-postgres-wasix-blockers.patch" 'pub fn native_cpu' +require_text "$PROJECT_ROOT/runtime/patches/wasmer/0001-postgres-wasix-blockers.patch" 'product_compiler_cpu_policy.rs' +require_text "$PROJECT_ROOT/runtime/patches/wasmer/0001-postgres-wasix-blockers.patch" 'memory-profile-core' +require_text "$PROJECT_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" 'i32.atomic.rmw.or' +require_text "$PROJECT_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" 'i32.atomic.rmw.and' +require_text "$PROJECT_ROOT/lib/guest_build_provenance.py" 'installed-closure.v1' +require_text "$PROJECT_ROOT/bin/build-sealed-headless-carrier.sh" '--emit-preinitialized-memory-image' +require_text "$PROJECT_ROOT/bin/build-sealed-headless-carrier.sh" 'independent memory image captures differ' +require_text "$PROJECT_ROOT/bin/build-sealed-headless-carrier.sh" 'payload.files' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'bulk_batch_wall_ms' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'whole_run_observed_cgroup_memory_peak_bytes' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'host_open_fd_count_total' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'assert_no_client_process_residue' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'libpq-latency-summary.tsv' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'effective-postgres-settings.tsv' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'host-fd-checkpoints.tsv' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'memory-evidence.tsv' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" '--runtime-footprint' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" '--durability' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'fresh_validate_postgres_profile_settings' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" '--cold-ownership' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" '--shared-memory-provider' +require_text "$PROJECT_ROOT/lib/shared_memory_provider.py" 'oliphaunt.wasix-postmaster.shared-memory-provider.v2' +require_text "$PROJECT_ROOT/lib/shared_memory_provider.py" 'oliphaunt.wasix-postmaster.shared-memory-release.v2' +require_text "$PROJECT_ROOT/lib/shared_memory_provider.py" 'anchored-parent-exact-inode-empty-rmdir-v2' +require_text "$PROJECT_ROOT/lib/shared_memory_provider.py" 'clean-postgresql-shutdown-v1' +require_text "$PROJECT_ROOT/lib/shared_memory_provider.py" 'post-process-drain-v1' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" '--cold-ownership-workloads' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" '--immutable-carrier-verification-scope' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'oliphaunt.wasix-postmaster.cold-ownership-mode.v1' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" '# FINAL COLD BOUNDARY:' +require_text "$PROJECT_ROOT/bin/prove-linux-cold-residency.py" 'POSIX_FADV_DONTNEED' +require_text "$PROJECT_ROOT/bin/prove-linux-cold-residency.py" 'mincore' +require_text "$PROJECT_ROOT/bin/validate-sealed-loader-audit.py" 'sealed-loader-receipt.v2' +require_text "$PROJECT_ROOT/bin/validate-sealed-loader-audit.py" 'residency_after_archive_release' +require_text "$PROJECT_ROOT/bin/validate-sealed-loader-audit.py" 'source_cache_eviction_errno' +require_text "$PROJECT_ROOT/bin/validate-wasix-cold-ownership.py" 'spawn_to_first_query_ms' +require_text "$PROJECT_ROOT/bin/qualify-wasix-cold-ownership.sh" 'candidate-unqualified-non-release' +require_text "$PROJECT_ROOT/bin/qualify-wasix-cold-ownership.sh" 'global_drop_caches' +require_moon_task_text cold-ownership-check 'cold-ownership.test.py' +require_moon_task_text cold-ownership-check 'validate-sealed-loader-audit.test.py' +for memory_budget_option in \ + '--max-peak-pss-kib' \ + '--max-peak-pss-anon-kib' \ + '--max-peak-page-table-kib' \ + '--max-cgroup-high-events-delta' \ + '--max-psi-some-stall-fraction' \ + '--max-psi-full-stall-fraction' +do + require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" \ + "$memory_budget_option" +done +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" \ + 'oliphaunt.wasix-postmaster.memory-budget.v1' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" \ + 'assert_frozen_memory_budget' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" \ + 'validate-wasix-lifecycle-plateau.py' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" \ + 'validate-wasix-lifecycle-memory-plateau.py' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" \ + '--wasix-lifecycle-memory-checkpoint-every' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" \ + '--max-lifecycle-late-pss-slope-kib-per-1000' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" \ + 'SELECT pg_log_standby_snapshot()' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" \ + 'validate-adaptive-file-cache-telemetry.py' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" \ + '--adaptive-cache-evidence-policy' +require_text "$PROJECT_ROOT/bin/validate-adaptive-file-cache-telemetry.py" \ + 'file-cache-admission-fallback.v1' +require_text "$PROJECT_ROOT/bin/validate-adaptive-file-cache-telemetry.py" \ + 'adaptive terminal offer dispositions do not conserve offers' +require_text "$PROJECT_ROOT/bin/validate-adaptive-file-cache-telemetry.py" \ + 'constrained-linux-wal-action-v1' +require_text "$PROJECT_ROOT/bin/validate-wasix-lifecycle-plateau.py" \ + 'relative-to-stabilized-baseline' +require_text "$PROJECT_ROOT/profiles/memory-budgets/embedded-c4-lower-pressure-v1.tsv" \ + 'candidate-unqualified' +require_text "$PROJECT_ROOT/profiles/qualification/embedded-performance-viability-candidate-v1.tsv" \ + 'awaiting-composite-environment-and-memory-contracts' +require_text "$PROJECT_ROOT/profiles/qualification/embedded-performance-viability-candidate-v1.tsv" \ + $'unresolved\tunresolved\tunresolved' +require_text "$PROJECT_ROOT/bin/compose-wasix-embedded-performance-viability.py" \ + 'positive-composition-disabled:same-host-session' +require_text "$PROJECT_ROOT/bin/compose-wasix-embedded-performance-viability.py" \ + 'failed-embedded-performance-viability-composition-non-release' +[ -x "$PROJECT_ROOT/bin/freeze-wasix-lifecycle-policy.py" ] || + fail "lifecycle policy freezer must be executable" +[ -x "$PROJECT_ROOT/bin/validate-wasix-lifecycle-memory-plateau.py" ] || + fail "lifecycle memory plateau validator must be executable" +[ -x "$PROJECT_ROOT/bin/lifecycle-memory-checkpoint.test.sh" ] || + fail "lifecycle memory checkpoint test must be executable" +[ -x "$PROJECT_ROOT/bin/compose-wasix-embedded-performance-viability.py" ] || + fail "performance composite blocker must be executable" +require_text "$PROJECT_ROOT/bin/summarize-libpq-latency.sh" 'nearest-rank p50/p95/p99' +require_text "$PROJECT_ROOT/probes/libpq_latency_probe.c" 'CLOCK_MONOTONIC' +require_text "$PROJECT_ROOT/probes/libpq_latency_probe.c" 'PQconnectdb(conninfo)' +require_text "$PROJECT_ROOT/probes/libpq_latency_probe.c" 'PQconnectdb(options.conninfo)' +require_text "$PROJECT_ROOT/probes/libpq_latency_probe.c" 'PQfinish(connection)' +require_text "$PROJECT_ROOT/lib/process-supervision.sh" 'kill -TERM -- "-$pgid"' +require_text "$PROJECT_ROOT/lib/process-supervision.sh" 'kill -KILL -- "-$pgid"' +require_text "$PROJECT_ROOT/lib/process-supervision.sh" 'signal-owned-pid.py' +require_text "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" 'derived_metrics_valid' +require_text "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" 'bulk-batch residual' +require_text "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" 'assert_frozen_carrier' +require_text "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" 'compare-postgres-settings.py' +require_text "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" '--runtime-footprint' +require_text "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" '--durability' +require_text "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" 'postgres-profile-resolution.tsv' +require_text "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" 'assert_frozen_native_oracle' +require_text "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" 'native-oracle-identity.tsv' +require_text "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" \ + 'adaptive-cache-verification.tsv' +require_text "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" \ + 'constrained-linux-wal-action-v1' +require_text "$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" \ + '--shared-memory-provider' +require_text "$PROJECT_ROOT/bin/qualify-wasix-libpq-latency.sh" 'ABBA/BAAB' +require_text "$PROJECT_ROOT/bin/qualify-wasix-libpq-latency.sh" '--resource-detail off' +require_text "$PROJECT_ROOT/bin/qualify-wasix-libpq-latency.sh" 'qualification-plan.tsv' +require_text "$PROJECT_ROOT/bin/qualify-wasix-libpq-latency.sh" 'WASIX_WAIT_DUMP_FENCE_REQUEST_FILE' +require_text "$PROJECT_ROOT/bin/compare-libpq-latency.py" 'raw latency evidence' +require_text "$PROJECT_ROOT/bin/compare-libpq-latency.py" 'paired_p95_ratio_p95' +require_text "$PROJECT_ROOT/docs/libpq-latency-qualification.md" 'true-libpq-safe-o2-c1-n1000-192m-v1' +require_text "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.sh" 'received immediate shutdown request' +require_text "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.sh" 'automatic recovery in progress' +require_text "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.sh" 'wait_for_unassisted_exit' +require_text "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.sh" 'cleanup_escalation' +require_text "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.sh" 'fresh_validate_postgres_profile_settings' +require_text "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.sh" '--immutable-carrier-receipt' +require_text "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.sh" 'OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1' +require_text "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.sh" '--expected-postgres-executions 3' +require_text "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.sh" 'MemorySwapMax=' +require_text "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.sh" 'expected_outer_postgres_invocations' +require_text "$PROJECT_ROOT/README.md" 'bootstrap postgres as a fourth outer postmaster' +require_text "$PROJECT_ROOT/bin/qualify-wasix-checkpoint-recycle.sh" 'ABBA' +require_text "$PROJECT_ROOT/bin/qualify-wasix-checkpoint-recycle.sh" \ + 'WASIX_CHECKPOINT_CGROUP_MEMORY_MAX:-256M' +require_text "$PROJECT_ROOT/bin/qualify-wasix-checkpoint-recycle.sh" \ + '"--property=MemoryMax=$cgroup_memory_max"' +require_text "$PROJECT_ROOT/bin/qualify-wasix-checkpoint-recycle.sh" 'candidate-unqualified' +require_text "$PROJECT_ROOT/bin/qualify-wasix-checkpoint-recycle.sh" '--data-checksums' +require_text "$PROJECT_ROOT/bin/qualify-wasix-checkpoint-recycle.sh" 'pg_checksums_bin' +require_text "$PROJECT_ROOT/bin/qualify-wasix-checkpoint-recycle.sh" 'extract-checkpoint-summary.py' +require_text "$PROJECT_ROOT/bench/sql/checkpoint-workload-setup.sql" 'oliphaunt_checkpoint_transaction' +require_text "$PROJECT_ROOT/probes/libpq_checkpoint_probe.c" 'CLOCK_MONOTONIC' +require_text "$PROJECT_ROOT/probes/libpq_checkpoint_probe.c" 'scheduled_ns + interval_ns' +require_text "$PROJECT_ROOT/bin/validate-checkpoint-recycle.py" 'monotonic_schedule_grid' +require_text "$PROJECT_ROOT/bin/validate-checkpoint-memory.py" 'restart_high_events_per_second' +require_text "$PROJECT_ROOT/bin/validate-wal-recycle.py" 'cross_snapshot_reuse_transitions' +require_text "$PROJECT_ROOT/profiles/checkpoint-policies/embedded-steady-v1.tsv" 'candidate-unqualified' +require_text "$PROJECT_ROOT/docs/checkpoint-recycle-qualification.md" 'fresh execution context' +require_text "$PROJECT_ROOT/lib/postgres-profiles.sh" 'embedded-concurrent-v1.gucs' +require_text "$PROJECT_ROOT/lib/postgres-profiles.sh" 'safe-v1.gucs' +require_text "$PROJECT_ROOT/lib/postgres-profiles.sh" 'oliphaunt.wasix-postmaster.postgres-profile-resolution.v1' +require_text "$PROJECT_ROOT/lib/sealed-carrier.sh" 'oliphaunt.wasix-postmaster.aot-producer.v2' +require_text "$PROJECT_ROOT/lib/sealed-carrier.sh" 'oliphaunt.wasix-postmaster.memory-image.v2' +require_text "$PROJECT_ROOT/lib/sealed-carrier.sh" 'oliphaunt.wasix-postmaster.deterministic-start-proof.v1' +require_text "$PROJECT_ROOT/lib/sealed-carrier.sh" 'carrier-verifier-sha256' +require_text "$PROJECT_ROOT/lib/sealed-carrier.sh" 'fresh_verify_sealed_headless_carrier' +require_text "$PROJECT_ROOT/lib/sealed-carrier.sh" 'fresh_sealed_executor_selection' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'fresh_verify_sealed_headless_carrier' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'oliphaunt.wasix-postmaster.sealed-executor-selection.v1' +require_text "$PROJECT_ROOT/lib/verify-sealed-carrier.py" 'oliphaunt.wasix-postmaster.payload-files.v1' +require_text "$PROJECT_ROOT/lib/verify-sealed-carrier.py" 'llvm-shared-memory-init-restricted-effects.v1' +require_text "$PROJECT_ROOT/lib/verify-sealed-carrier.py" 'deterministic-start-proof-output-sha256' +require_text "$PROJECT_ROOT/lib/immutable-carrier.py" 'oliphaunt.wasix-postmaster.immutable-carrier-deployment.v2' +require_text "$PROJECT_ROOT/bin/verify-immutable-sealed-carrier.sh" '--fast' +require_text "$PROJECT_ROOT/lib/qualification-identities.sh" 'FRESH_QUALIFICATION_GUEST_BUILD_RECIPE_SHA256' +require_text "$PROJECT_ROOT/bin/current-evidence-manifest.py" 'candidate-unqualified-non-release' +require_text "$PROJECT_ROOT/bin/current-evidence-manifest.py" 'artifact-identity-only' +require_text "$PROJECT_ROOT/bin/current-evidence-manifest.py" 'oliphaunt.wasix-postmaster.postmaster-executor-build.v3' +require_text "$PROJECT_ROOT/bin/current-evidence-manifest.py" 'run_carrier_verifier' +require_text "$PROJECT_ROOT/runtime/bin/run-exec-backend-probes.sh" '--probe posix-spawn-sigchld-default' +require_text "$PROJECT_ROOT/runtime/bin/run-exec-backend-probes.sh" '--probe waitpid-wnohang-any' +require_text "$PROJECT_ROOT/runtime/bin/run-exec-backend-probes.sh" '--probe dynamic-vfork-exec' +require_text "$PROJECT_ROOT/runtime/bin/run-exec-backend-probes.sh" '--probe wasm-eh-sjlj' +require_text "$PROJECT_ROOT/runtime/bin/run-exec-backend-probes.sh" '--probe epoll-ofd-lifecycle' +require_text "$PROJECT_ROOT/runtime/bin/run-exec-backend-probes.sh" '--probe sync-file-range' +require_text "$PROJECT_ROOT/runtime/bin/run-exec-backend-probes.sh" '--probe directory-fsync' +require_text "$PROJECT_ROOT/runtime/bin/run-exec-backend-probes.sh" '--probe exec-shared-latch-sigurg' +require_text "$PROJECT_ROOT/runtime/bin/run-blocker-probes.sh" 'epoll_ofd_lifecycle.pic.wasm' +require_text "$PROJECT_ROOT/runtime/bin/run-blocker-probes.sh" 'sync_file_range.pic.wasm' +require_text "$PROJECT_ROOT/runtime/bin/run-blocker-probes.sh" 'directory_fsync.pic.wasm' +require_text "$PROJECT_ROOT/runtime/capabilities.tsv" 'epoll-open-file-description-lifecycle' +require_text "$PROJECT_ROOT/runtime/capabilities.tsv" 'host-file-advice' +require_text "$PROJECT_ROOT/runtime/capabilities.tsv" 'semantic-relation-cache-offers' +require_text "$PROJECT_ROOT/runtime/capabilities.tsv" 'sealed-loader-residency-audit' +require_text "$PROJECT_ROOT/runtime/capabilities.tsv" 'postmaster-product-executor' +require_text "$PROJECT_ROOT/runtime/capabilities.tsv" 'semantic-inactive-durable-wal-cache-offers' +require_text "$PROJECT_ROOT/runtime/capabilities.tsv" 'host-range-writeback' +require_text "$PROJECT_ROOT/runtime/capabilities.tsv" 'host-directory-durability' +require_text "$PROJECT_ROOT/runtime/probes/directory_fsync_probe.c" 'rename(ORIGINAL, RENAMED)' +require_text "$PROJECT_ROOT/runtime/probes/directory_fsync_probe.c" 'fdatasync(duplicate_fd)' +require_text "$PROJECT_ROOT/runtime/probes/directory_fsync_probe.c" 'fsync(duplicate_fd)' +forbid_text "$PATCH_ROOT/0004-wasix-core-execbackend-initdb-runtime.patch" 'errno == EISDIR' +require_text "$PROJECT_ROOT/runtime/probes/sync_file_range_probe.c" 'O_RDONLY' +require_text "$PROJECT_ROOT/runtime/probes/sync_file_range_probe.c" 'SYNC_FILE_RANGE_WAIT_BEFORE == 1' +require_text "$PROJECT_ROOT/runtime/probes/sync_file_range_probe.c" 'oliphaunt_postmaster_v1' +require_text "$PROJECT_ROOT/runtime/bin/verify-postmaster-wasm-import.py" 'EXPECTED_MODULE = "oliphaunt_postmaster_v1"' +require_text "$PROJECT_ROOT/runtime/bin/verify-postmaster-wasm-import.py" '"fd_sync_range": (' +require_text "$PROJECT_ROOT/runtime/bin/verify-postmaster-wasm-import.py" '"fd_cache_offer": (' +require_text "$PROJECT_ROOT/runtime/bin/verify-postmaster-wasm-import.py" '"fd_cache_revoke": (' +require_text "$PROJECT_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" 'expected_total: int | None = None' +require_text "$PROJECT_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" '"SetLatch": 2' +require_text "$PROJECT_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" '"ResetLatch": 1' +require_text "$PROJECT_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" '"WaitEventSetWait": 1' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'content-addressed-read-only' +require_text "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" 'assert_frozen_measurement_tool_closure' +require_text "$PROJECT_ROOT/runtime/probes/epoll_ofd_lifecycle_probe.c" 'PAYLOAD_OLD_ALIAS' +require_text "$PROJECT_ROOT/runtime/probes/epoll_ofd_lifecycle_probe.c" 'reused_read_fd != old_read_fd' +require_text "$PROJECT_ROOT/runtime/probes/epoll_ofd_lifecycle_probe.c" 'close(old_alias)' +require_text "$PROJECT_ROOT/runtime/probes/exec_shared_latch_sigurg_probe.c" 'kill(getpid(), 0)' +require_text "$PROJECT_ROOT/runtime/probes/exec_shared_latch_sigurg_probe.c" 'kill(child_pid, 0)' +require_text "$PROJECT_ROOT/runtime/probes/exec_shared_latch_sigurg_probe.c" 'errno != ESRCH' +require_text "$PROJECT_ROOT/moon.yml" 'liboliphaunt-wasix-postmaster:runtime-build' +require_text "$PROJECT_ROOT/moon.yml" 'liboliphaunt-wasix-postmaster:postgres-build' +require_text "$PROJECT_ROOT/moon.yml" 'liboliphaunt-wasix-postmaster:initdb-stress' +require_text "$PROJECT_ROOT/moon.yml" 'liboliphaunt-wasix-postmaster:smoke' +require_text "$PROJECT_ROOT/moon.yml" 'checkpoint-qualification-check' +require_text "$PROJECT_ROOT/README.md" '## Build toolchain boundary' +require_text "$PROJECT_ROOT/docs/architecture.md" 'EXEC_BACKEND' +require_text "$PROJECT_ROOT/docs/concurrent-root-cause.md" 'PG_WASIX_ATOMIC_LATCH_STATE' +require_text "$PROJECT_ROOT/runtime/capabilities.tsv" 'cross-instance-packed-latch-state' +require_text "$PROJECT_ROOT/sources.lock.toml" '[current_postgresql_patches.packed_atomic_latch_state]' + +awk ' + $0 == " - id: \"liboliphaunt-wasix\"" { + if ((getline next_line) > 0 && next_line == " scope: \"build\"") found = 1 + } + END { exit(found ? 0 : 1) } +' "$PROJECT_ROOT/moon.yml" || + fail "moon project must declare liboliphaunt-wasix as its build-scope recipe owner" + +require_moon_task_text check '"liboliphaunt-wasix:check"' +require_moon_task_text postgres-build '"/src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-postmaster-wasm-import.py"' +require_moon_task_text checkpoint-qualification-check 'validate-checkpoint-memory.test.py' +require_moon_task_text checkpoint-qualification-check 'extract-checkpoint-summary.test.py' +for toolchain_task in check runtime-build configure postgres-build blocker-probes regression; do + require_moon_task_text "$toolchain_task" "/src/sources/toolchains/wasix.toml" + require_moon_task_text "$toolchain_task" "/src/runtimes/liboliphaunt/wasix/assets/build/docker/**/*" + require_moon_task_text "$toolchain_task" "/src/runtimes/liboliphaunt/wasix/assets/build/docker_wasix_env.sh" +done + +[ ! -e "$PROJECT_ROOT/bin/install-wasmer.sh" ] || + fail "stock Wasmer downloader is forbidden; build the exact pinned patched runtime" +if find "$PROJECT_ROOT/bin" "$PROJECT_ROOT/lib" "$PROJECT_ROOT/runtime/bin" \ + -type f ! -name 'check-prior-art.sh' -print0 \ + | xargs -0 grep -nF 'github.com/wasmerio/wasmer/releases/download' \ + >/dev/null 2>&1 +then + fail "live scripts must not download an unverified stock Wasmer runtime" +fi + +if [ -e "$PROJECT_ROOT/release.toml" ]; then + fail "research project must not define release.toml before promotion gates pass" +fi +if grep -Fq 'release-product' "$PROJECT_ROOT/moon.yml"; then + fail "research project must not carry the release-product tag" +fi + +while IFS= read -r script; do + [ -x "$script" ] || fail "shebang script is not executable: $script" + bash -n "$script" || fail "shell syntax failed: $script" +done < <(find "$PROJECT_ROOT" -type f -name '*.sh' -print | sort) + +while IFS= read -r script; do + [ -x "$script" ] || fail "shebang script is not executable: $script" +done < <(grep -rl '^#!' "$PROJECT_ROOT" | sort) + +python3 "$PROJECT_ROOT/runtime/bin/verify-source-lock.py" || + fail "source lock reconciliation failed" +python3 "$PROJECT_ROOT/runtime/bin/verify-source-lock.test.py" || + fail "source lock verifier tests failed" +python3 "$PATCH_ROOT/0007-wasix-semantic-relation-cache-offers.test.py" || + fail "semantic relation cache-offer patch tests failed" +python3 "$PATCH_ROOT/0008-wasix-packed-atomic-latch-state.test.py" || + fail "packed atomic latch-state patch tests failed" +python3 "$PATCH_ROOT/0009-wasix-inactive-durable-wal-cache-offer.test.py" || + fail "inactive durable WAL cache-offer patch tests failed" +python3 "$PROJECT_ROOT/runtime/bin/verify-runtime-state-ownership.test.py" || + fail "runtime-state ownership verifier tests failed" +python3 "$PROJECT_ROOT/runtime/bin/verify-runtime-execution-ownership.test.py" || + fail "runtime execution ownership verifier tests failed" +python3 "$PROJECT_ROOT/bin/run-frozen-measurement.test.py" || + fail "frozen measurement-tool closure tests failed" +bash "$PROJECT_ROOT/lib/common.test.sh" || fail "patched Wasmer receipt selection tests failed" +bash "$PROJECT_ROOT/lib/host-fd-telemetry.test.sh" || fail "host FD telemetry tests failed" +bash "$PROJECT_ROOT/lib/process-supervision.test.sh" || fail "process supervision tests failed" +python3 "$PROJECT_ROOT/lib/signal-owned-pid.test.py" || fail "pidfd ownership tests failed" +bash "$PROJECT_ROOT/lib/server-lifecycle.test.sh" || fail "server lifecycle tests failed" +bash "$PROJECT_ROOT/lib/postgres-profiles.test.sh" || fail "PostgreSQL profile resolution tests failed" +python3 "$PROJECT_ROOT/lib/shared_memory_provider.test.py" || + fail "shared-memory provider lifecycle tests failed" +python3 "$PROJECT_ROOT/lib/verify-sealed-carrier.test.py" || + fail "sealed carrier schema verifier tests failed" +python3 "$PROJECT_ROOT/lib/sealed_export_chain.test.py" || + fail "sealed-export predecessor-chain tests failed" +python3 "$PROJECT_ROOT/lib/linear_memory_transaction.test.py" || + fail "linear-memory durable transaction tests failed" +python3 "$PROJECT_ROOT/lib/durable_publication.test.py" || + fail "durable receipt publication tests failed" +python3 "$PROJECT_ROOT/lib/durable_publication_crash.test.py" || + fail "durable receipt crash-boundary tests failed" +python3 "$PROJECT_ROOT/lib/guest_build_provenance.test.py" || + fail "durable guest closure provenance tests failed" +bash "$PROJECT_ROOT/bin/seal-wasix-core-exports.transaction.test.sh" || + fail "sealed export crash-recovery transaction tests failed" +bash "$PROJECT_ROOT/bin/build-sealed-headless-carrier.test.sh" || fail "sealed headless carrier packaging tests failed" +bash "$PROJECT_ROOT/bin/qualify-wasix-single-backend.test.sh" || fail "single-backend qualifier evidence tests failed" +bash "$PROJECT_ROOT/bin/qualify-wasix-libpq-latency.test.sh" || + fail "true-libpq latency qualifier tests failed" +bash "$PROJECT_ROOT/bin/qualify-wasix-immediate-recovery.test.sh" || + fail "immediate-recovery exit tests failed" +python3 "$PROJECT_ROOT/bin/compare-libpq-latency.test.py" || + fail "true-libpq latency comparator tests failed" +python3 "$PROJECT_ROOT/bin/classify-linux-file-residency.test.py" || + fail "Linux file-residency classifier tests failed" +python3 "$PROJECT_ROOT/bin/validate-sealed-loader-audit.test.py" || + fail "sealed-loader residency audit tests failed" +python3 "$PROJECT_ROOT/bin/validate-file-cache-telemetry.test.py" || + fail "observe-only file-cache telemetry validation tests failed" +python3 "$PROJECT_ROOT/bin/validate-adaptive-file-cache-telemetry.test.py" || + fail "adaptive file-cache telemetry validation tests failed" +bash "$PROJECT_ROOT/bin/summarize-linux-smaps.test.sh" || fail "Linux smaps summary tests failed" +bash "$PROJECT_ROOT/bin/summarize-libpq-latency.test.sh" || fail "libpq latency evidence tests failed" +bash "$PROJECT_ROOT/bin/validate-host-fd-churn.test.sh" || fail "host FD churn validation tests failed" +bash "$PROJECT_ROOT/bin/validate-memory-evidence.test.sh" || fail "memory evidence validation tests failed" +bash "$PROJECT_ROOT/bin/bench-memory-budget-options.test.sh" || + fail "benchmark memory budget option tests failed" +bash "$PROJECT_ROOT/bin/bench-shared-memory-provider-options.test.sh" || + fail "benchmark shared-memory provider option tests failed" +bash "$PROJECT_ROOT/bin/bench-instrumentation-policy.test.sh" || + fail "benchmark instrumentation policy tests failed" +bash -n "$PROJECT_ROOT/bin/bench-wasix-concurrent-query-suite.sh" || + fail "concurrent benchmark syntax failed" +python3 "$PROJECT_ROOT/bin/freeze-wasix-lifecycle-evidence.test.py" || + fail "WASIX lifecycle evidence freezer tests failed" +python3 "$PROJECT_ROOT/bin/validate-wasix-lifecycle-plateau.test.py" || + fail "WASIX lifecycle plateau validation tests failed" +python3 "$PROJECT_ROOT/bin/validate-wasix-lifecycle-memory-plateau.test.py" || + fail "WASIX lifecycle memory plateau validation tests failed" +bash "$PROJECT_ROOT/bin/lifecycle-memory-checkpoint.test.sh" || + fail "WASIX lifecycle memory checkpoint integration tests failed" +python3 "$PROJECT_ROOT/bin/freeze-wasix-lifecycle-policy.test.py" || + fail "WASIX exact lifecycle policy freezer tests failed" +PYTHONPYCACHEPREFIX="$CHECK_TEMP/pycache" python3 -m py_compile \ + "$PROJECT_ROOT/bin/compose-wasix-embedded-performance-viability.py" \ + "$PROJECT_ROOT/bin/compose-wasix-embedded-performance-viability.test.py" || + fail "performance composite blocker Python compilation failed" +python3 "$PROJECT_ROOT/bin/compose-wasix-embedded-performance-viability.test.py" || + fail "performance composite blocker tests failed" +bash "$PROJECT_ROOT/bin/resource-evidence.test.sh" || fail "resource evidence tests failed" +bash "$PROJECT_ROOT/bin/resource-monitor-race-retry.test.sh" || + fail "resource monitor process-tree retry tests failed" +bash "$PROJECT_ROOT/bin/process-tree-collector.test.sh" || + fail "bounded process-tree collector tests failed" +bash "$PROJECT_ROOT/bin/connected-client-gate.test.sh" || + fail "connected client population-gate tests failed" +python3 "$PROJECT_ROOT/bin/delta-pg-stat-io.test.py" || fail "pg_stat_io delta tests failed" +python3 "$PROJECT_ROOT/bin/compare-postgres-settings.test.py" || fail "PostgreSQL settings comparison tests failed" +python3 "$PROJECT_ROOT/runtime/bin/verify-postmaster-wasm-import.test.py" || + fail "postmaster Wasm import verifier tests failed" +python3 "$PROJECT_ROOT/runtime/bin/verify-postmaster-concurrency-contract.test.py" || + fail "postmaster Wasm concurrency contract verifier tests failed" +bash "$PROJECT_ROOT/bin/seal-wasix-core-exports.test.sh" || + fail "sealed export closure policy and analyzer tests failed" +bash "$PROJECT_ROOT/bin/qualify-wasix-checkpoint-recycle.test.sh" || + fail "checkpoint/recycle qualifier plan tests failed" +python3 "$PROJECT_ROOT/bin/validate-checkpoint-recycle.test.py" || + fail "checkpoint/recycle fixed-offer tests failed" +python3 "$PROJECT_ROOT/bin/validate-checkpoint-memory.test.py" || + fail "checkpoint/recycle memory tests failed" +python3 "$PROJECT_ROOT/bin/validate-wal-recycle.test.py" || + fail "WAL recycle evidence tests failed" +python3 "$PROJECT_ROOT/bin/extract-checkpoint-summary.test.py" || + fail "checkpoint summary extraction tests failed" +python3 "$PROJECT_ROOT/bin/summarize-checkpoint-qualification.test.py" || + fail "checkpoint/recycle paired summary tests failed" + +listed="$({ + while IFS= read -r patch_name || [ -n "$patch_name" ]; do + case "$patch_name" in + ''|'#'*) continue ;; + /*|*../*) fail "unsafe PostgreSQL patch entry: $patch_name" ;; + esac + [ -f "$PATCH_ROOT/$patch_name" ] || fail "series names missing patch: $patch_name" + printf '%s\n' "$patch_name" + done <"$SERIES_FILE" +} | sort)" +actual="$(find "$PATCH_ROOT" -maxdepth 1 -type f -name '*.patch' -exec basename {} \; | sort)" +[ "$listed" = "$actual" ] || { + printf 'listed patches:\n%s\nactual patches:\n%s\n' "$listed" "$actual" >&2 + fail "PostgreSQL patch series and directory differ" +} + +while IFS= read -r patch; do + git apply --numstat <"$patch" >/dev/null || fail "malformed patch: $patch" +done < <(find "$PATCH_ROOT" "$PROJECT_ROOT/runtime/patches" -type f -name '*.patch' -print | sort) + +if find "$PROJECT_ROOT/bin" "$PROJECT_ROOT/lib" "$PROJECT_ROOT/runtime/bin" \ + -type f ! -name 'check-prior-art.sh' -print0 \ + | xargs -0 grep -nE 'assets/wasix-build|work/experiments/fresh-wasix-postgres|REL_18_3' \ + >/dev/null 2>&1 +then + fail "live scripts still reference the historical experiment layout" +fi + +if grep -Fq 'Arc-style registry' "$PROJECT_ROOT/runtime/capabilities.tsv" || + grep -Fq 'not-implemented:registry-reuse-cleanup' "$PROJECT_ROOT/runtime/capabilities.tsv" +then + fail "capability ledger contains the superseded shared-futex registry model" +fi + +while IFS= read -r ref; do + rel="${ref#project:}" + [ -e "$PROJECT_ROOT/$rel" ] || fail "capability ledger references missing project path: $rel" +done < <(grep -oE 'project:[^;[:space:]]+' "$PROJECT_ROOT/runtime/capabilities.tsv" | sort -u) + +printf 'wasix-postmaster prior-art checks passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/classify-linux-file-residency.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/classify-linux-file-residency.py new file mode 100755 index 00000000..bda08b39 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/classify-linux-file-residency.py @@ -0,0 +1,971 @@ +#!/usr/bin/env python3 +"""Classify Linux page-cache residency for one carrier and one PGDATA tree. + +This is an observation-only tool. It opens exact, canonical roots without +following symlinks, maps regular files read-only, and queries mincore(2). It +does not read file payloads, issue cache advice, sync files, or reclaim pages. +""" + +from __future__ import annotations + +import argparse +import csv +import ctypes +import mmap +import os +import platform +import re +import resource +import stat +import sys +import time +from dataclasses import dataclass +from pathlib import Path, PurePosixPath +from typing import NoReturn, Sequence + + +SCHEMA = "oliphaunt.wasix-postmaster.linux-file-residency.v1" +PAGE_SIZE = os.sysconf("SC_PAGE_SIZE") +PROBE_CHUNK_PAGES = 65_536 +MAP_FAILED = ctypes.c_void_p(-1).value +UNSAFE_NAME_CHARACTERS = "\t\r\n\0" +TEMP_RELATION = re.compile(r"^t[0-9]+_[0-9]+(?:_(?:fsm|vm|init))?(?:\.[0-9]+)?$") + + +@dataclass(frozen=True) +class Category: + root: str + name: str + scope: str = "-" + + +CATEGORIES = ( + Category("carrier", "bin"), + Category("carrier", "aot"), + Category("carrier", "memory"), + Category("carrier", "share"), + Category("carrier", "metadata"), + Category("pgdata", "relation-index-aggregate", "base"), + Category("pgdata", "relation-index-aggregate", "global"), + Category("pgdata", "pg_wal"), + Category("pgdata", "temp"), + Category("pgdata", "other"), +) +CATEGORY_RANK = {category: rank for rank, category in enumerate(CATEGORIES)} + + +class ClassificationError(RuntimeError): + pass + + +def fail(message: str) -> NoReturn: + raise ClassificationError(message) + + +@dataclass(frozen=True) +class Identity: + device: int + inode: int + size: int + file_type: int + permissions: int + mtime_ns: int + ctime_ns: int + + @classmethod + def from_stat(cls, info: os.stat_result) -> "Identity": + return cls( + device=info.st_dev, + inode=info.st_ino, + size=info.st_size, + file_type=stat.S_IFMT(info.st_mode), + permissions=stat.S_IMODE(info.st_mode), + mtime_ns=info.st_mtime_ns, + ctime_ns=info.st_ctime_ns, + ) + + +@dataclass +class RootHandle: + name: str + path: Path + fd: int + identity: Identity + + +@dataclass +class OutputHandle: + path: Path + parent: Path + name: str + fd: int + identity: Identity + + +@dataclass(frozen=True) +class FileCandidate: + root: str + components: tuple[str, ...] + identity: Identity + category: Category + + @property + def relative_path(self) -> str: + return PurePosixPath(*self.components).as_posix() + + +@dataclass(frozen=True) +class TreeSnapshot: + files: tuple[FileCandidate, ...] + directories: tuple[tuple[tuple[str, ...], Identity], ...] + + @property + def directory_map(self) -> dict[tuple[str, ...], Identity]: + return dict(self.directories) + + +@dataclass(frozen=True) +class FileMeasurement: + candidate: FileCandidate + page_count: int + resident_pages: int + resident_bytes: int + nonresident_pages: int + nonresident_bytes: int + + +@dataclass(frozen=True) +class FaultMetrics: + minor_before: int + minor_after: int + major_before: int + major_after: int + scan_started_monotonic_ns: int + scan_completed_monotonic_ns: int + + @property + def minor_delta(self) -> int: + return self.minor_after - self.minor_before + + @property + def major_delta(self) -> int: + return self.major_after - self.major_before + + @property + def scan_duration_ms(self) -> str: + duration_ns = self.scan_completed_monotonic_ns - self.scan_started_monotonic_ns + return f"{duration_ns / 1_000_000:.3f}" + + +FILES_HEADER = ( + "schema_version", + "status", + "root", + "category", + "scope", + "relative_path", + "device", + "inode", + "logical_bytes", + "page_count", + "resident_logical_bytes", + "resident_page_bytes", + "resident_pages", + "nonresident_logical_bytes", + "nonresident_page_bytes", + "nonresident_pages", + "error", +) + +SUMMARY_HEADER = ( + "schema_version", + "status", + "root", + "category", + "scope", + "file_count", + "logical_bytes", + "page_count", + "resident_logical_bytes", + "resident_page_bytes", + "resident_pages", + "nonresident_logical_bytes", + "nonresident_page_bytes", + "nonresident_pages", + "error_count", + "errors", + "page_size", + "probe_minor_faults_before", + "probe_minor_faults_after", + "probe_minor_faults_delta", + "probe_major_faults_before", + "probe_major_faults_after", + "probe_major_faults_delta", + "probe_snapshot_scope", + "probe_consecutive_vectors_stable", + "probe_scan_started_monotonic_ns", + "probe_scan_completed_monotonic_ns", + "probe_scan_duration_ms", + "probe_payload_bytes_read", +) + + +libc = ctypes.CDLL(None, use_errno=True) +libc.mmap.argtypes = ( + ctypes.c_void_p, + ctypes.c_size_t, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ctypes.c_longlong, +) +libc.mmap.restype = ctypes.c_void_p +libc.mincore.argtypes = ( + ctypes.c_void_p, + ctypes.c_size_t, + ctypes.POINTER(ctypes.c_ubyte), +) +libc.mincore.restype = ctypes.c_int +libc.munmap.argtypes = (ctypes.c_void_p, ctypes.c_size_t) +libc.munmap.restype = ctypes.c_int + + +def safe_error(error: BaseException) -> str: + return " ".join(str(error).replace("\0", "\\0").splitlines()) + + +def require_safe_name(name: str, display_path: str) -> None: + if not name or name in (".", ".."): + fail(f"unsafe empty or relative entry name below {display_path}") + if any(character in name for character in UNSAFE_NAME_CHARACTERS): + fail(f"unsafe TSV/control character in path below {display_path}") + if any(0xD800 <= ord(character) <= 0xDFFF for character in name): + fail(f"non-UTF-8 path entry below {display_path}") + + +def require_identity(actual: os.stat_result, expected: Identity, display_path: str) -> None: + observed = Identity.from_stat(actual) + if observed != expected: + fail( + "path identity changed while classifying residency: " + f"{display_path}; expected={expected} observed={observed}" + ) + + +def require_output_parent_identity( + actual: os.stat_result, expected: Identity, display_path: str +) -> None: + observed = Identity.from_stat(actual) + expected_stable = ( + expected.device, + expected.inode, + expected.file_type, + expected.permissions, + ) + observed_stable = ( + observed.device, + observed.inode, + observed.file_type, + observed.permissions, + ) + if observed_stable != expected_stable: + fail( + "report output parent identity changed while publishing: " + f"{display_path}; expected={expected_stable} observed={observed_stable}" + ) + + +def classify_path(root: str, components: tuple[str, ...]) -> Category: + first = components[0] + if root == "carrier": + if first in {"bin", "aot", "memory", "share"}: + return Category("carrier", first) + return Category("carrier", "metadata") + + basename = components[-1] + if ( + first == "pg_stat_tmp" + or "pgsql_tmp" in components + or basename.startswith("pgsql_tmp") + or TEMP_RELATION.fullmatch(basename) is not None + ): + return Category("pgdata", "temp") + if first in {"base", "global"}: + return Category("pgdata", "relation-index-aggregate", first) + if first == "pg_wal": + return Category("pgdata", "pg_wal") + return Category("pgdata", "other") + + +def open_root(name: str, supplied: Path) -> RootHandle: + if not supplied.is_absolute(): + fail(f"--{name}-root must be absolute: {supplied}") + before = os.lstat(supplied) + if stat.S_ISLNK(before.st_mode) or not stat.S_ISDIR(before.st_mode): + fail(f"--{name}-root must be a non-symlink directory: {supplied}") + canonical = Path(os.path.realpath(supplied)) + if canonical != supplied: + fail(f"--{name}-root must already be canonical: supplied={supplied} canonical={canonical}") + flags = ( + os.O_RDONLY + | getattr(os, "O_CLOEXEC", 0) + | getattr(os, "O_DIRECTORY", 0) + | getattr(os, "O_NOFOLLOW", 0) + ) + fd = os.open(supplied, flags) + identity = Identity.from_stat(before) + try: + require_identity(os.fstat(fd), identity, str(supplied)) + except BaseException: + os.close(fd) + raise + return RootHandle(name=name, path=supplied, fd=fd, identity=identity) + + +def display(root: RootHandle, components: Sequence[str]) -> str: + if not components: + return str(root.path) + return f"{root.path}/{PurePosixPath(*components).as_posix()}" + + +def snapshot_tree(root: RootHandle) -> TreeSnapshot: + files: list[FileCandidate] = [] + directories: dict[tuple[str, ...], Identity] = {} + + def visit(directory_fd: int, components: tuple[str, ...], expected: Identity) -> None: + path_display = display(root, components) + require_identity(os.fstat(directory_fd), expected, path_display) + directories[components] = expected + try: + names = sorted(os.listdir(directory_fd)) + except OSError as error: + fail(f"unable to list exact directory {path_display}: {error}") + for name in names: + require_safe_name(name, path_display) + + for name in names: + child_components = (*components, name) + child_display = display(root, child_components) + try: + before = os.stat(name, dir_fd=directory_fd, follow_symlinks=False) + except OSError as error: + fail(f"entry changed while scanning {child_display}: {error}") + if stat.S_ISLNK(before.st_mode): + fail(f"tree contains a symlink: {child_display}") + child_identity = Identity.from_stat(before) + if stat.S_ISDIR(before.st_mode): + flags = ( + os.O_RDONLY + | getattr(os, "O_CLOEXEC", 0) + | getattr(os, "O_DIRECTORY", 0) + | getattr(os, "O_NOFOLLOW", 0) + ) + try: + child_fd = os.open(name, flags, dir_fd=directory_fd) + except OSError as error: + fail(f"directory changed while opening {child_display}: {error}") + try: + require_identity(os.fstat(child_fd), child_identity, child_display) + visit(child_fd, child_components, child_identity) + require_identity(os.fstat(child_fd), child_identity, child_display) + require_identity( + os.stat(name, dir_fd=directory_fd, follow_symlinks=False), + child_identity, + child_display, + ) + finally: + os.close(child_fd) + elif stat.S_ISREG(before.st_mode): + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) + try: + file_fd = os.open(name, flags, dir_fd=directory_fd) + except OSError as error: + fail(f"file changed while opening {child_display}: {error}") + try: + require_identity(os.fstat(file_fd), child_identity, child_display) + require_identity( + os.stat(name, dir_fd=directory_fd, follow_symlinks=False), + child_identity, + child_display, + ) + finally: + os.close(file_fd) + files.append( + FileCandidate( + root=root.name, + components=child_components, + identity=child_identity, + category=classify_path(root.name, child_components), + ) + ) + else: + fail(f"tree contains a non-regular, non-directory entry: {child_display}") + + try: + names_after = sorted(os.listdir(directory_fd)) + except OSError as error: + fail(f"unable to re-list exact directory {path_display}: {error}") + if names_after != names: + fail(f"directory entries changed while scanning: {path_display}") + require_identity(os.fstat(directory_fd), expected, path_display) + + visit(root.fd, (), root.identity) + files.sort(key=lambda item: (CATEGORY_RANK[item.category], item.relative_path)) + return TreeSnapshot(files=tuple(files), directories=tuple(sorted(directories.items()))) + + +def verify_root_path(root: RootHandle) -> None: + require_identity(os.fstat(root.fd), root.identity, str(root.path)) + require_identity(os.lstat(root.path), root.identity, str(root.path)) + + +def open_candidate( + root: RootHandle, + candidate: FileCandidate, + directories: dict[tuple[str, ...], Identity], +) -> tuple[int, int]: + parent_fd = os.dup(root.fd) + prefix: tuple[str, ...] = () + try: + for component in candidate.components[:-1]: + next_prefix = (*prefix, component) + expected = directories.get(next_prefix) + if expected is None: + fail(f"missing directory identity for {display(root, next_prefix)}") + before = os.stat(component, dir_fd=parent_fd, follow_symlinks=False) + require_identity(before, expected, display(root, next_prefix)) + flags = ( + os.O_RDONLY + | getattr(os, "O_CLOEXEC", 0) + | getattr(os, "O_DIRECTORY", 0) + | getattr(os, "O_NOFOLLOW", 0) + ) + child_fd = os.open(component, flags, dir_fd=parent_fd) + try: + require_identity(os.fstat(child_fd), expected, display(root, next_prefix)) + except BaseException: + os.close(child_fd) + raise + os.close(parent_fd) + parent_fd = child_fd + prefix = next_prefix + + name = candidate.components[-1] + path_display = display(root, candidate.components) + require_identity( + os.stat(name, dir_fd=parent_fd, follow_symlinks=False), + candidate.identity, + path_display, + ) + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) + file_fd = os.open(name, flags, dir_fd=parent_fd) + try: + require_identity(os.fstat(file_fd), candidate.identity, path_display) + except BaseException: + os.close(file_fd) + raise + return parent_fd, file_fd + except BaseException: + os.close(parent_fd) + raise + + +def verify_open_candidate( + root: RootHandle, candidate: FileCandidate, parent_fd: int, file_fd: int +) -> None: + path_display = display(root, candidate.components) + require_identity(os.fstat(file_fd), candidate.identity, path_display) + require_identity( + os.stat(candidate.components[-1], dir_fd=parent_fd, follow_symlinks=False), + candidate.identity, + path_display, + ) + + +def mincore_vector( + address: int, length: int, vector: ctypes.Array[ctypes.c_ubyte] +) -> None: + if libc.mincore(address, length, vector) != 0: + error = ctypes.get_errno() + raise OSError(error, os.strerror(error)) + + +def probe_chunk( + fd: int, + file_size: int, + offset: int, + length: int, + first: ctypes.Array[ctypes.c_ubyte], + second: ctypes.Array[ctypes.c_ubyte], + display_path: str, +) -> tuple[int, int, int, int]: + address = libc.mmap(None, length, mmap.PROT_READ, mmap.MAP_SHARED, fd, offset) + if address == MAP_FAILED: + error = ctypes.get_errno() + raise OSError(error, os.strerror(error)) + pages = (length + PAGE_SIZE - 1) // PAGE_SIZE + try: + mincore_vector(address, length, first) + mincore_vector(address, length, second) + resident_pages = 0 + resident_bytes = 0 + nonresident_pages = 0 + nonresident_bytes = 0 + for page in range(pages): + before = first[page] & 1 + after = second[page] & 1 + if before != after: + fail( + "residency changed between consecutive mincore probes: " + f"{display_path} offset={offset} page={page} before={before} after={after}" + ) + logical_bytes = min(PAGE_SIZE, file_size - offset - page * PAGE_SIZE) + if after: + resident_pages += 1 + resident_bytes += logical_bytes + else: + nonresident_pages += 1 + nonresident_bytes += logical_bytes + return resident_pages, resident_bytes, nonresident_pages, nonresident_bytes + finally: + if libc.munmap(address, length) != 0: + error = ctypes.get_errno() + raise OSError(error, os.strerror(error)) + + +def probe_file( + fd: int, + size: int, + first: ctypes.Array[ctypes.c_ubyte], + second: ctypes.Array[ctypes.c_ubyte], + display_path: str, + first_chunk_only: bool = False, +) -> tuple[int, int, int, int]: + resident_pages = resident_bytes = nonresident_pages = nonresident_bytes = 0 + chunk_bytes = PROBE_CHUNK_PAGES * PAGE_SIZE + offset = 0 + while offset < size: + length = min(chunk_bytes, size - offset) + values = probe_chunk(fd, size, offset, length, first, second, display_path) + resident_pages += values[0] + resident_bytes += values[1] + nonresident_pages += values[2] + nonresident_bytes += values[3] + offset += length + if first_chunk_only: + break + return resident_pages, resident_bytes, nonresident_pages, nonresident_bytes + + +def prepare_vectors() -> tuple[ctypes.Array[ctypes.c_ubyte], ctypes.Array[ctypes.c_ubyte]]: + vector_type = ctypes.c_ubyte * PROBE_CHUNK_PAGES + first = vector_type() + second = vector_type() + # Pre-fault the small result buffers outside the observed probe window. + for index in range(0, PROBE_CHUNK_PAGES, PAGE_SIZE): + first[index] = 0 + second[index] = 0 + first[-1] = 0 + second[-1] = 0 + return first, second + + +def measure_files( + roots: dict[str, RootHandle], snapshots: dict[str, TreeSnapshot] +) -> tuple[list[FileMeasurement], FaultMetrics]: + candidates = [candidate for root in ("carrier", "pgdata") for candidate in snapshots[root].files] + first, second = prepare_vectors() + + # Resolve libc/mmap/mincore code and ctypes call paths before fault accounting. + warm = next((candidate for candidate in candidates if candidate.identity.size > 0), None) + if warm is not None: + root = roots[warm.root] + parent_fd, file_fd = open_candidate(root, warm, snapshots[warm.root].directory_map) + try: + probe_file( + file_fd, + warm.identity.size, + first, + second, + display(root, warm.components), + first_chunk_only=True, + ) + verify_open_candidate(root, warm, parent_fd, file_fd) + finally: + os.close(file_fd) + os.close(parent_fd) + + scan_started_monotonic_ns = time.monotonic_ns() + before = resource.getrusage(resource.RUSAGE_SELF) + measurements: list[FileMeasurement] = [] + for candidate in candidates: + root = roots[candidate.root] + parent_fd, file_fd = open_candidate(root, candidate, snapshots[candidate.root].directory_map) + try: + resident_pages, resident_bytes, nonresident_pages, nonresident_bytes = probe_file( + file_fd, + candidate.identity.size, + first, + second, + display(root, candidate.components), + ) + verify_open_candidate(root, candidate, parent_fd, file_fd) + finally: + os.close(file_fd) + os.close(parent_fd) + page_count = (candidate.identity.size + PAGE_SIZE - 1) // PAGE_SIZE + if resident_pages + nonresident_pages != page_count: + fail(f"internal page accounting mismatch for {display(root, candidate.components)}") + if resident_bytes + nonresident_bytes != candidate.identity.size: + fail(f"internal byte accounting mismatch for {display(root, candidate.components)}") + measurements.append( + FileMeasurement( + candidate=candidate, + page_count=page_count, + resident_pages=resident_pages, + resident_bytes=resident_bytes, + nonresident_pages=nonresident_pages, + nonresident_bytes=nonresident_bytes, + ) + ) + after = resource.getrusage(resource.RUSAGE_SELF) + scan_completed_monotonic_ns = time.monotonic_ns() + faults = FaultMetrics( + minor_before=before.ru_minflt, + minor_after=after.ru_minflt, + major_before=before.ru_majflt, + major_after=after.ru_majflt, + scan_started_monotonic_ns=scan_started_monotonic_ns, + scan_completed_monotonic_ns=scan_completed_monotonic_ns, + ) + return measurements, faults + + +def open_output(path: Path) -> OutputHandle: + parent = path.parent + name = path.name + require_safe_name(name, str(parent)) + try: + before = os.lstat(parent) + except OSError as error: + fail(f"report output parent must already exist: {parent}: {error}") + if stat.S_ISLNK(before.st_mode) or not stat.S_ISDIR(before.st_mode): + fail(f"report output parent must be a non-symlink directory: {parent}") + canonical_parent = Path(os.path.realpath(parent)) + if canonical_parent != parent: + fail( + "report output parent must already be canonical: " + f"supplied={parent} canonical={canonical_parent}" + ) + flags = ( + os.O_RDONLY + | getattr(os, "O_CLOEXEC", 0) + | getattr(os, "O_DIRECTORY", 0) + | getattr(os, "O_NOFOLLOW", 0) + ) + fd = os.open(parent, flags) + identity = Identity.from_stat(before) + try: + require_output_parent_identity(os.fstat(fd), identity, str(parent)) + try: + existing = os.stat(name, dir_fd=fd, follow_symlinks=False) + except FileNotFoundError: + existing = None + if existing is not None and not stat.S_ISREG(existing.st_mode): + fail(f"report output must be absent or a regular non-symlink file: {path}") + except BaseException: + os.close(fd) + raise + return OutputHandle(path=path, parent=parent, name=name, fd=fd, identity=identity) + + +def verify_output_parent(output: OutputHandle) -> None: + require_output_parent_identity(os.fstat(output.fd), output.identity, str(output.parent)) + require_output_parent_identity(os.lstat(output.parent), output.identity, str(output.parent)) + + +def validate_outputs( + files_output: Path, summary_output: Path, roots: Sequence[Path] +) -> tuple[OutputHandle, OutputHandle]: + outputs = (files_output, summary_output) + resolved_outputs = tuple(Path(os.path.realpath(output)) for output in outputs) + if files_output == summary_output or resolved_outputs[0] == resolved_outputs[1]: + fail("--files-output and --summary-output must be different paths") + for output, resolved in zip(outputs, resolved_outputs, strict=True): + for root in roots: + if resolved == root or root in resolved.parents: + fail(f"report output must be outside measured roots: {output}") + opened: list[OutputHandle] = [] + try: + for output in outputs: + opened.append(open_output(output)) + except BaseException: + for handle in opened: + os.close(handle.fd) + raise + return opened[0], opened[1] + + +def rows_for_files(measurements: Sequence[FileMeasurement], status: str) -> list[dict[str, object]]: + rows: list[dict[str, object]] = [] + for measurement in sorted( + measurements, + key=lambda item: ( + CATEGORY_RANK[item.candidate.category], + item.candidate.relative_path, + ), + ): + candidate = measurement.candidate + rows.append( + { + "schema_version": SCHEMA, + "status": status, + "root": candidate.root, + "category": candidate.category.name, + "scope": candidate.category.scope, + "relative_path": candidate.relative_path, + "device": candidate.identity.device, + "inode": candidate.identity.inode, + "logical_bytes": candidate.identity.size, + "page_count": measurement.page_count, + "resident_logical_bytes": measurement.resident_bytes, + "resident_page_bytes": measurement.resident_pages * PAGE_SIZE, + "resident_pages": measurement.resident_pages, + "nonresident_logical_bytes": measurement.nonresident_bytes, + "nonresident_page_bytes": measurement.nonresident_pages * PAGE_SIZE, + "nonresident_pages": measurement.nonresident_pages, + "error": "", + } + ) + return rows + + +def rows_for_summary( + measurements: Sequence[FileMeasurement], + status: str, + faults: FaultMetrics | None, + errors: Sequence[str], +) -> list[dict[str, object]]: + grouped: dict[Category, list[FileMeasurement]] = {category: [] for category in CATEGORIES} + for measurement in measurements: + grouped[measurement.candidate.category].append(measurement) + + def make_row( + root: str, category: str, scope: str, values: Sequence[FileMeasurement], total: bool = False + ) -> dict[str, object]: + return { + "schema_version": SCHEMA, + "status": status, + "root": root, + "category": category, + "scope": scope, + "file_count": len(values), + "logical_bytes": sum(item.candidate.identity.size for item in values), + "page_count": sum(item.page_count for item in values), + "resident_logical_bytes": sum(item.resident_bytes for item in values), + "resident_page_bytes": sum(item.resident_pages for item in values) * PAGE_SIZE, + "resident_pages": sum(item.resident_pages for item in values), + "nonresident_logical_bytes": sum(item.nonresident_bytes for item in values), + "nonresident_page_bytes": sum(item.nonresident_pages for item in values) * PAGE_SIZE, + "nonresident_pages": sum(item.nonresident_pages for item in values), + "error_count": len(errors) if total else 0, + "errors": " | ".join(errors) if total else "", + "page_size": PAGE_SIZE, + "probe_minor_faults_before": "" if faults is None else faults.minor_before, + "probe_minor_faults_after": "" if faults is None else faults.minor_after, + "probe_minor_faults_delta": "" if faults is None else faults.minor_delta, + "probe_major_faults_before": "" if faults is None else faults.major_before, + "probe_major_faults_after": "" if faults is None else faults.major_after, + "probe_major_faults_delta": "" if faults is None else faults.major_delta, + "probe_snapshot_scope": "sequential-point-in-time", + "probe_consecutive_vectors_stable": "yes" if status == "passed" else "no", + "probe_scan_started_monotonic_ns": ( + "" if faults is None else faults.scan_started_monotonic_ns + ), + "probe_scan_completed_monotonic_ns": ( + "" if faults is None else faults.scan_completed_monotonic_ns + ), + "probe_scan_duration_ms": "" if faults is None else faults.scan_duration_ms, + "probe_payload_bytes_read": 0, + } + + rows = [ + make_row(category.root, category.name, category.scope, grouped[category]) + for category in CATEGORIES + ] + rows.append(make_row("all", "total", "-", measurements, total=True)) + return rows + + +def write_tsv( + output: OutputHandle, header: Sequence[str], rows: Sequence[dict[str, object]] +) -> None: + pending = f".{output.name}.pending.{os.getpid()}" + descriptor = -1 + published = False + try: + verify_output_parent(output) + flags = ( + os.O_WRONLY + | os.O_CREAT + | os.O_EXCL + | getattr(os, "O_CLOEXEC", 0) + | getattr(os, "O_NOFOLLOW", 0) + ) + descriptor = os.open(pending, flags, 0o600, dir_fd=output.fd) + with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as stream: + descriptor = -1 + writer = csv.DictWriter( + stream, + fieldnames=header, + delimiter="\t", + lineterminator="\n", + extrasaction="raise", + ) + writer.writeheader() + writer.writerows(rows) + stream.flush() + os.fsync(stream.fileno()) + verify_output_parent(output) + try: + existing = os.stat(output.name, dir_fd=output.fd, follow_symlinks=False) + except FileNotFoundError: + existing = None + if existing is not None and not stat.S_ISREG(existing.st_mode): + fail( + "report output changed to a non-regular or symlink entry before publish: " + f"{output.path}" + ) + os.replace( + pending, + output.name, + src_dir_fd=output.fd, + dst_dir_fd=output.fd, + ) + published = True + os.fsync(output.fd) + verify_output_parent(output) + finally: + if descriptor >= 0: + os.close(descriptor) + if not published: + try: + os.unlink(pending, dir_fd=output.fd) + except FileNotFoundError: + pass + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--carrier-root", required=True, type=Path) + parser.add_argument("--pgdata-root", required=True, type=Path) + parser.add_argument("--files-output", "--output", dest="files_output", required=True, type=Path) + parser.add_argument("--summary-output", "--summary", dest="summary_output", required=True, type=Path) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + files_output = args.files_output.absolute() + summary_output = args.summary_output.absolute() + measurements: list[FileMeasurement] = [] + faults: FaultMetrics | None = None + outputs_valid = False + output_handles: tuple[OutputHandle, OutputHandle] | None = None + roots: dict[str, RootHandle] = {} + try: + if platform.system() != "Linux": + fail("file residency classification requires Linux mincore(2)") + if PAGE_SIZE <= 0: + fail(f"invalid host page size: {PAGE_SIZE}") + if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_DIRECTORY"): + fail("safe traversal requires O_NOFOLLOW and O_DIRECTORY") + + supplied_roots = (args.carrier_root, args.pgdata_root) + output_boundaries = tuple(Path(os.path.realpath(path)) for path in supplied_roots) + output_handles = validate_outputs(files_output, summary_output, output_boundaries) + outputs_valid = True + roots["carrier"] = open_root("carrier", args.carrier_root) + roots["pgdata"] = open_root("pgdata", args.pgdata_root) + carrier_path = roots["carrier"].path + pgdata_path = roots["pgdata"].path + if ( + carrier_path == pgdata_path + or carrier_path in pgdata_path.parents + or pgdata_path in carrier_path.parents + or ( + roots["carrier"].identity.device == roots["pgdata"].identity.device + and roots["carrier"].identity.inode == roots["pgdata"].identity.inode + ) + ): + fail(f"carrier and PGDATA roots must be disjoint: {carrier_path} and {pgdata_path}") + + snapshots = {name: snapshot_tree(roots[name]) for name in ("carrier", "pgdata")} + inode_paths: dict[tuple[int, int], str] = {} + for name in ("carrier", "pgdata"): + for candidate in snapshots[name].files: + key = (candidate.identity.device, candidate.identity.inode) + previous = inode_paths.get(key) + if previous is not None: + fail( + "hard-linked paths make physical residency attribution ambiguous: " + f"{previous} and {display(roots[name], candidate.components)}" + ) + inode_paths[key] = display(roots[name], candidate.components) + + measurements, faults = measure_files(roots, snapshots) + if faults.major_delta != 0: + fail( + "probe incurred major faults and cannot prove payload-neutral observation: " + f"before={faults.major_before} after={faults.major_after} " + f"delta={faults.major_delta}" + ) + + for name in ("carrier", "pgdata"): + verify_root_path(roots[name]) + final_snapshot = snapshot_tree(roots[name]) + if final_snapshot != snapshots[name]: + fail(f"{name} tree changed between residency snapshot boundaries: {roots[name].path}") + + write_tsv(output_handles[0], FILES_HEADER, rows_for_files(measurements, "passed")) + write_tsv( + output_handles[1], + SUMMARY_HEADER, + rows_for_summary(measurements, "passed", faults, ()), + ) + return 0 + except (ClassificationError, OSError, ValueError) as error: + message = safe_error(error) + if outputs_valid: + try: + assert output_handles is not None + write_tsv( + output_handles[0], + FILES_HEADER, + rows_for_files(measurements, "failed"), + ) + write_tsv( + output_handles[1], + SUMMARY_HEADER, + rows_for_summary(measurements, "failed", faults, (message,)), + ) + except (ClassificationError, OSError, ValueError) as output_error: + message += f"; unable to write failure evidence: {safe_error(output_error)}" + print(f"file residency classification failed: {message}", file=sys.stderr) + return 1 + finally: + for root in roots.values(): + try: + os.close(root.fd) + except OSError: + pass + if output_handles is not None: + for output in output_handles: + try: + os.close(output.fd) + except OSError: + pass + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/classify-linux-file-residency.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/classify-linux-file-residency.test.py new file mode 100755 index 00000000..f061505d --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/classify-linux-file-residency.test.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import mmap +import os +import platform +import subprocess +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).with_name("classify-linux-file-residency.py") +PAGE_SIZE = os.sysconf("SC_PAGE_SIZE") + + +@unittest.skipUnless(platform.system() == "Linux", "Linux mincore fixture") +class LinuxFileResidencyClassifierTests(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory(prefix="file-residency-test-") + self.root = Path(self.temp.name) + self.carrier = self.root / "carrier" + self.pgdata = self.root / "pgdata" + for name in ("bin", "aot", "memory", "share"): + (self.carrier / name).mkdir(parents=True, exist_ok=True) + (self.pgdata / "base" / "12345").mkdir(parents=True) + (self.pgdata / "global").mkdir() + (self.pgdata / "pg_wal").mkdir() + (self.pgdata / "base" / "pgsql_tmp").mkdir() + (self.pgdata / "pg_stat_tmp").mkdir() + + self.resident = self.carrier / "bin" / "wasmer-headless" + self.resident.write_bytes(b"R" * (2 * PAGE_SIZE)) + self.resident_fd = os.open(self.resident, os.O_RDONLY) + self.resident_map = mmap.mmap(self.resident_fd, 0, access=mmap.ACCESS_READ) + for offset in range(0, len(self.resident_map), PAGE_SIZE): + self.assertEqual(self.resident_map[offset], ord("R")) + + self.sparse = self.carrier / "aot" / "sparse-module.bin" + with self.sparse.open("wb") as stream: + stream.truncate(16 * PAGE_SIZE) + + (self.carrier / "memory" / "postgres.bin").write_bytes(b"memory-image") + (self.carrier / "share" / "extension.control").write_bytes(b"share") + (self.carrier / "manifest.json").write_bytes(b"{}\n") + (self.pgdata / "base" / "12345" / "16384").write_bytes(b"relation") + (self.pgdata / "global" / "1262").write_bytes(b"global") + (self.pgdata / "pg_wal" / "000000010000000000000001").write_bytes(b"wal") + (self.pgdata / "base" / "pgsql_tmp" / "pgsql_tmp42.0").write_bytes(b"temp") + (self.pgdata / "base" / "12345" / "t7_9001_vm.1").write_bytes(b"temp-relation") + (self.pgdata / "pg_stat_tmp" / "global.stat").write_bytes(b"temp-stat") + (self.pgdata / "PG_VERSION").write_bytes(b"18\n") + + self.files_output = self.root / "files.tsv" + self.summary_output = self.root / "summary.tsv" + + def tearDown(self) -> None: + self.resident_map.close() + os.close(self.resident_fd) + self.temp.cleanup() + + def run_classifier( + self, carrier: Path | None = None, suffix: str = "" + ) -> subprocess.CompletedProcess[str]: + files_output = self.root / f"files{suffix}.tsv" + summary_output = self.root / f"summary{suffix}.tsv" + result = subprocess.run( + [ + "python3", + str(SCRIPT), + "--carrier-root", + str(carrier or self.carrier), + "--pgdata-root", + str(self.pgdata), + "--files-output", + str(files_output), + "--summary-output", + str(summary_output), + ], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return result + + def read_rows(self, path: Path) -> list[dict[str, str]]: + with path.open("r", encoding="utf-8", newline="") as stream: + return list(csv.DictReader(stream, delimiter="\t")) + + def test_sparse_resident_temp_and_summary_accounting(self) -> None: + result = self.run_classifier() + self.assertEqual(result.returncode, 0, result.stderr) + files = self.read_rows(self.files_output) + by_path = {(row["root"], row["relative_path"]): row for row in files} + + sparse = by_path[("carrier", "aot/sparse-module.bin")] + self.assertEqual(sparse["resident_pages"], "0") + self.assertEqual(sparse["nonresident_pages"], "16") + self.assertEqual(sparse["nonresident_logical_bytes"], str(16 * PAGE_SIZE)) + self.assertEqual(sparse["nonresident_page_bytes"], str(16 * PAGE_SIZE)) + + resident = by_path[("carrier", "bin/wasmer-headless")] + self.assertEqual(resident["resident_pages"], "2") + self.assertEqual(resident["resident_logical_bytes"], str(2 * PAGE_SIZE)) + self.assertEqual(resident["resident_page_bytes"], str(2 * PAGE_SIZE)) + + self.assertEqual( + by_path[("pgdata", "base/pgsql_tmp/pgsql_tmp42.0")]["category"], "temp" + ) + self.assertEqual(by_path[("pgdata", "base/12345/t7_9001_vm.1")]["category"], "temp") + self.assertEqual(by_path[("pgdata", "pg_stat_tmp/global.stat")]["category"], "temp") + self.assertEqual( + by_path[("pgdata", "base/12345/16384")]["category"], + "relation-index-aggregate", + ) + self.assertEqual(by_path[("pgdata", "base/12345/16384")]["scope"], "base") + + summary = self.read_rows(self.summary_output) + expected_order = [ + ("carrier", "bin", "-"), + ("carrier", "aot", "-"), + ("carrier", "memory", "-"), + ("carrier", "share", "-"), + ("carrier", "metadata", "-"), + ("pgdata", "relation-index-aggregate", "base"), + ("pgdata", "relation-index-aggregate", "global"), + ("pgdata", "pg_wal", "-"), + ("pgdata", "temp", "-"), + ("pgdata", "other", "-"), + ("all", "total", "-"), + ] + self.assertEqual( + [(row["root"], row["category"], row["scope"]) for row in summary], + expected_order, + ) + total = summary[-1] + self.assertEqual(total["status"], "passed") + self.assertEqual(total["error_count"], "0") + self.assertEqual(total["probe_major_faults_delta"], "0") + self.assertEqual(total["probe_snapshot_scope"], "sequential-point-in-time") + self.assertEqual(total["probe_consecutive_vectors_stable"], "yes") + self.assertGreater(int(total["probe_scan_started_monotonic_ns"]), 0) + self.assertGreaterEqual( + int(total["probe_scan_completed_monotonic_ns"]), + int(total["probe_scan_started_monotonic_ns"]), + ) + self.assertGreaterEqual(float(total["probe_scan_duration_ms"]), 0.0) + self.assertEqual(total["probe_payload_bytes_read"], "0") + self.assertEqual( + int(total["resident_pages"]) + int(total["nonresident_pages"]), + int(total["page_count"]), + ) + self.assertEqual( + int(total["resident_logical_bytes"]) + + int(total["nonresident_logical_bytes"]), + int(total["logical_bytes"]), + ) + self.assertEqual( + int(total["resident_page_bytes"]) + + int(total["nonresident_page_bytes"]), + int(total["page_count"]) * PAGE_SIZE, + ) + self.assertGreaterEqual(int(total["probe_minor_faults_delta"]), 0) + + repeat = self.run_classifier(suffix="-repeat") + self.assertEqual(repeat.returncode, 0, repeat.stderr) + self.assertEqual( + self.files_output.read_bytes(), + (self.root / "files-repeat.tsv").read_bytes(), + ) + repeat_summary = self.read_rows(self.root / "summary-repeat.tsv") + varying_fault_fields = { + "probe_minor_faults_before", + "probe_minor_faults_after", + "probe_minor_faults_delta", + "probe_major_faults_before", + "probe_major_faults_after", + "probe_major_faults_delta", + "probe_scan_started_monotonic_ns", + "probe_scan_completed_monotonic_ns", + "probe_scan_duration_ms", + } + for first, second in zip(summary, repeat_summary, strict=True): + for field in varying_fault_fields: + first[field] = "" + second[field] = "" + self.assertEqual(summary, repeat_summary) + + def test_symlink_is_rejected_with_failure_summary(self) -> None: + (self.pgdata / "unsafe-link").symlink_to("PG_VERSION") + result = self.run_classifier(suffix="-symlink") + self.assertEqual(result.returncode, 1) + self.assertIn("tree contains a symlink", result.stderr) + summary = self.read_rows(self.root / "summary-symlink.tsv") + total = summary[-1] + self.assertEqual(total["status"], "failed") + self.assertEqual(total["error_count"], "1") + self.assertIn("tree contains a symlink", total["errors"]) + + def test_noncanonical_root_is_rejected(self) -> None: + alias = self.root / "carrier-alias" + alias.symlink_to(self.carrier, target_is_directory=True) + result = self.run_classifier(carrier=alias, suffix="-root-link") + self.assertEqual(result.returncode, 1) + self.assertIn("must be a non-symlink directory", result.stderr) + summary = self.read_rows(self.root / "summary-root-link.tsv") + self.assertEqual(summary[-1]["status"], "failed") + + def test_output_aliases_cannot_overwrite_each_other(self) -> None: + reports = self.root / "reports" + reports.mkdir() + alias = self.root / "reports-alias" + alias.symlink_to(reports, target_is_directory=True) + output = reports / "evidence.tsv" + result = subprocess.run( + [ + "python3", + str(SCRIPT), + "--carrier-root", + str(self.carrier), + "--pgdata-root", + str(self.pgdata), + "--files-output", + str(output), + "--summary-output", + str(alias / "evidence.tsv"), + ], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + self.assertEqual(result.returncode, 1) + self.assertIn("must be different paths", result.stderr) + self.assertFalse(output.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/cold-ownership.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/cold-ownership.test.py new file mode 100755 index 00000000..7f95ea3c --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/cold-ownership.test.py @@ -0,0 +1,741 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import hashlib +import importlib.util +import json +import os +import resource +import shlex +import shutil +import stat +import subprocess +import sys +import tempfile +import unittest +from unittest import mock +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +PROVER = ROOT / "bin" / "prove-linux-cold-residency.py" +CAPTURE = ROOT / "bin" / "capture-linux-cgroup-v2.py" +VALIDATOR = ROOT / "bin" / "validate-wasix-cold-ownership.py" +SUMMARIZER = ROOT / "bin" / "summarize-wasix-cold-ownership.py" +BENCH = ROOT / "bin" / "bench-wasix-concurrent-query-suite.sh" +PROCESS_SUPERVISION = ROOT / "lib" / "process-supervision.sh" + + +def digest(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load_module(path: Path, name: str): + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + try: + spec.loader.exec_module(module) + except BaseException: + sys.modules.pop(name, None) + raise + return module + + +def shell_function(path: Path, name: str) -> str: + lines = path.read_text(encoding="utf-8").splitlines() + marker = f"{name}() {{" + start = lines.index(marker) + for end in range(start + 1, len(lines)): + if lines[end] == "}": + return "\n".join(lines[start : end + 1]) + raise AssertionError(f"unterminated shell function: {name}") + + +class ColdOwnershipTests(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory(prefix="cold-ownership-test-") + self.root = Path(self.temp.name) + self.carrier = self.root / "carrier" + self.pgdata = self.root / "pgdata" + (self.carrier / "bin").mkdir(parents=True) + (self.carrier / "aot").mkdir() + self.pgdata.mkdir() + (self.carrier / "bin" / "wasmer-headless").write_bytes(b"runtime\n" * 8192) + (self.carrier / "aot" / "module.bin").write_bytes(b"aot\n" * 4096) + (self.pgdata / "PG_VERSION").write_text("18\n", encoding="ascii") + (self.pgdata / "base").mkdir() + (self.pgdata / "base" / "relation").write_bytes(b"relation\n" * 4096) + for directory, _, files in os.walk(self.carrier, topdown=False): + for name in files: + path = Path(directory) / name + path.chmod(0o555 if path.name == "wasmer-headless" else 0o444) + Path(directory).chmod(0o555) + self.execution = self.root / "execution.tsv" + self.execution.write_text("fixture execution identity\n", encoding="utf-8") + self.receipt = self.root / "cold.json" + + def tearDown(self) -> None: + # Restore carrier ownership so TemporaryDirectory can clean it. + for directory, _, files in os.walk(self.carrier, topdown=False): + Path(directory).chmod(0o755) + for name in files: + (Path(directory) / name).chmod(0o644) + self.temp.cleanup() + + def prover_command( + self, + *, + output: Path | None = None, + roots: list[tuple[str, Path]] | None = None, + read_only_roles: tuple[str, ...] = ("carrier",), + ) -> list[str]: + command = ["python3", str(PROVER)] + for role, path in roots or [("carrier", self.carrier), ("pgdata", self.pgdata)]: + command.extend(["--root", f"{role}={path}"]) + for role in read_only_roles: + command.extend(["--read-only-root", role]) + command.extend( + [ + "--binding", + f"execution_identity_sha256={digest(self.execution)}", + "--output", + str(output or self.receipt), + ] + ) + return command + + def run_prover( + self, + *, + output: Path | None = None, + roots: list[tuple[str, Path]] | None = None, + read_only_roles: tuple[str, ...] = ("carrier",), + nofile_limit: int | None = None, + check: bool = False, + ) -> subprocess.CompletedProcess[str]: + def constrain_nofile() -> None: + assert nofile_limit is not None + resource.setrlimit(resource.RLIMIT_NOFILE, (nofile_limit, nofile_limit)) + + return subprocess.run( + self.prover_command( + output=output, + roots=roots, + read_only_roles=read_only_roles, + ), + check=check, + preexec_fn=constrain_nofile if nofile_limit is not None else None, + text=True, + capture_output=True, + ) + + def prove(self, nofile_limit: int | None = None) -> dict[str, object]: + result = self.run_prover(nofile_limit=nofile_limit) + self.assertEqual(result.returncode, 0, result.stderr) + return json.loads(self.receipt.read_text(encoding="utf-8")) + + def test_targeted_proof_is_zero_and_hash_bound(self) -> None: + receipt = self.prove() + self.assertEqual(receipt["status"], "passed") + self.assertEqual(receipt["totals"]["resident_after_pages"], 0) + self.assertGreater(receipt["totals"]["page_count"], 0) + self.assertEqual( + receipt["bindings"]["execution_identity_sha256"], digest(self.execution) + ) + roles = {entry["role"] for entry in receipt["roots"]} + self.assertEqual(roles, {"carrier", "pgdata"}) + scope = receipt["proof_scope"] + self.assertEqual( + scope["root_activity_requirement"], + "quiescent before initial inventory through measured spawn", + ) + self.assertEqual( + scope["inventory_revalidation"], + "exact-before-eviction-and-after-mincore", + ) + timestamps = receipt["timestamps"] + self.assertLessEqual( + scope["quiescence_required_from_monotonic_ns"], + timestamps["inventory_started_monotonic_ns"], + ) + self.assertLessEqual( + timestamps["pre_eviction_inventory_verified_monotonic_ns"], + timestamps["eviction_started_monotonic_ns"], + ) + self.assertLessEqual( + timestamps["post_proof_inventory_verified_monotonic_ns"], + timestamps["proof_completed_monotonic_ns"], + ) + for root in receipt["roots"]: + self.assertRegex(root["exact_inventory_sha256"], r"^[0-9a-f]{64}$") + + def test_streaming_proof_stays_bounded_below_file_count(self) -> None: + fixture = self.pgdata / "base" / "low-fd-fixture" + fixture.mkdir() + for index in range(192): + (fixture / f"relation-{index:03d}").write_bytes( + (f"relation {index:03d}\n".encode("ascii")) * 256 + ) + + receipt = self.prove(nofile_limit=32) + + self.assertGreater(receipt["totals"]["unique_file_count"], 100) + bounds = receipt["resource_bounds"] + self.assertEqual(bounds["cold_file_descriptor_strategy"], "bounded-streaming-reopen") + self.assertEqual(bounds["cold_file_descriptor_limit"], 1) + self.assertEqual(bounds["cold_file_descriptor_peak"], 1) + self.assertEqual(bounds["persistent_cold_file_descriptors"], 0) + self.assertEqual(bounds["final_mincore_sweeps"], 2) + self.assertEqual(bounds["final_mincore_resident_pages"], [0, 0]) + + def test_concurrent_addition_is_rejected_before_eviction(self) -> None: + prover = load_module(PROVER, "prove_linux_cold_residency_concurrent_addition") + original = prover.require_exact_inventories + injected = False + + def add_entry_then_validate(roots, descriptor_budget, phase): + nonlocal injected + if phase == "pre-eviction boundary" and not injected: + injected = True + (self.pgdata / "base" / "concurrent-addition").write_bytes(b"raced\n") + return original(roots, descriptor_budget, phase) + + argv = self.prover_command()[1:] + with mock.patch.object(prover, "require_exact_inventories", add_entry_then_validate): + with mock.patch.object(sys, "argv", argv): + with self.assertRaisesRegex( + prover.ProofError, + "(inventory changed|directory changed|descriptor changed identity)", + ): + prover.main() + self.assertTrue(injected) + self.assertFalse(self.receipt.exists()) + + def test_output_symlink_and_symlinked_parent_are_rejected(self) -> None: + target = self.root / "receipt-target" + target.write_text("must remain unchanged\n", encoding="utf-8") + self.receipt.symlink_to(target) + result = self.run_prover() + self.assertNotEqual(result.returncode, 0) + self.assertIn("output must not be a symlink", result.stderr) + self.assertEqual(target.read_text(encoding="utf-8"), "must remain unchanged\n") + + self.receipt.unlink() + aliased_parent = self.root / "pgdata-output-alias" + aliased_parent.symlink_to(self.pgdata, target_is_directory=True) + aliased_output = aliased_parent / "receipt.json" + result = self.run_prover(output=aliased_output) + self.assertNotEqual(result.returncode, 0) + self.assertIn("must already be canonical", result.stderr) + self.assertFalse((self.pgdata / "receipt.json").exists()) + + def test_internal_hardlinks_are_recorded_when_all_aliases_are_in_one_root(self) -> None: + relation = self.pgdata / "base" / "relation" + alias = self.pgdata / "base" / "relation-alias" + os.link(relation, alias) + + receipt = self.prove() + + hard_links = receipt["hard_links"] + self.assertEqual(hard_links["external_alias_count"], 0) + self.assertEqual(hard_links["internal_hard_link_object_count"], 1) + recorded = hard_links["internal_hard_links"][0] + self.assertEqual(recorded["link_count"], 2) + self.assertEqual( + recorded["paths"], + [["pgdata", "base/relation"], ["pgdata", "base/relation-alias"]], + ) + + def test_external_hardlink_is_rejected(self) -> None: + relation = self.pgdata / "base" / "relation" + os.link(relation, self.root / "external-relation-alias") + + result = self.run_prover() + + self.assertNotEqual(result.returncode, 0) + self.assertIn("external or otherwise unobserved hard-link aliases", result.stderr) + self.assertFalse(self.receipt.exists()) + + def test_cross_root_hardlink_is_rejected(self) -> None: + carrier_file = self.carrier / "aot" / "module.bin" + os.link(carrier_file, self.pgdata / "base" / "carrier-alias") + + result = self.run_prover() + + self.assertNotEqual(result.returncode, 0) + self.assertIn("cold roots share one hard-linked inode", result.stderr) + self.assertFalse(self.receipt.exists()) + + def test_validator_binds_boundary_cgroup_and_full_samples(self) -> None: + proof_end = 1_000_000_000 + self.receipt.write_text( + json.dumps( + { + "schema_version": "oliphaunt.wasix-postmaster.cold-residency.v1", + "status": "passed", + "bindings": { + "execution_identity_sha256": digest(self.execution), + }, + "roots": [ + { + "role": "carrier", + "path": str(self.carrier), + "require_read_only": True, + }, + { + "role": "pgdata", + "path": str(self.pgdata), + "require_read_only": False, + }, + ], + "totals": { + "regular_path_count": 4, + "unique_file_count": 4, + "logical_bytes": 65_536, + "page_count": 16, + "resident_before_pages": 8, + "resident_after_pages": 0, + }, + "timestamps": { + "proof_completed_monotonic_ns": proof_end, + }, + }, + sort_keys=True, + ) + + "\n", + encoding="utf-8", + ) + spawn = proof_end + 1_000_000 + first_query = spawn + 25_000_000 + first = self.root / "first.json" + final = self.root / "final.json" + + def snapshot(path: Path, captured: int, peak: int, rbytes: int | None) -> None: + if rbytes is None: + io = { + "status": "unavailable", + "controller_status": "missing", + "missing_reason": "io-controller-missing", + "source": None, + "devices": None, + "totals": None, + } + else: + io = { + "status": "available", + "controller_status": "available", + "missing_reason": None, + "source": "io.stat", + "devices": [ + { + "device": "8:0", + "metrics": { + "rbytes": rbytes, + "wbytes": 8192, + "rios": 4, + "wios": 2, + }, + } + ], + "totals": { + "rbytes": rbytes, + "wbytes": 8192, + "rios": 4, + "wios": 2, + }, + } + path.write_text( + json.dumps( + { + "schema_version": "oliphaunt.wasix-postmaster.cgroup-v2-snapshot.v1", + "status": "passed", + "captured_monotonic_ns": captured, + "cgroup": {"path": "/fixture.scope", "identity": "dev:ino"}, + "memory": { + "current": 80_000_000, + "peak": peak, + "max": 268_435_456, + "high": 234_881_024, + "swap_current": 0, + "swap_peak": 0, + "swap_max": 0, + "stat": {"file": 40_000_000, "file_dirty": 4096, "file_writeback": 0}, + }, + "io": io, + }, + sort_keys=True, + ) + + "\n", + encoding="utf-8", + ) + + snapshot(first, first_query + 1_000_000, 120_000_000, 1_048_576) + snapshot(final, first_query + 5_000_000, 125_000_000, 2_097_152) + samples = self.root / "resources.tsv" + fields = [ + "target", + "smaps_status", + "cgroup_status", + "cgroup_path", + "cgroup_scope_memory_peak_bytes", + "cgroup_scope_swap_peak_bytes", + "cgroup_memory_stat_file_dirty_bytes", + "cgroup_memory_stat_file_writeback_bytes", + ] + with samples.open("w", encoding="utf-8", newline="") as stream: + writer = csv.DictWriter(stream, fieldnames=fields, delimiter="\t") + writer.writeheader() + writer.writerow( + { + "target": "wasix", + "smaps_status": "ok", + "cgroup_status": "ok", + "cgroup_path": "/fixture.scope", + "cgroup_scope_memory_peak_bytes": "125000000", + "cgroup_scope_swap_peak_bytes": "0", + "cgroup_memory_stat_file_dirty_bytes": "4096", + "cgroup_memory_stat_file_writeback_bytes": "0", + } + ) + output = self.root / "sample.tsv" + command = [ + "python3", + str(VALIDATOR), + "--residency-receipt", + str(self.receipt), + "--first-query-snapshot", + str(first), + "--final-snapshot", + str(final), + "--resource-samples", + str(samples), + "--execution-identity", + str(self.execution), + "--carrier-root", + str(self.carrier), + "--pgdata-root", + str(self.pgdata), + "--spawn-monotonic-ns", + str(spawn), + "--first-query-monotonic-ns", + str(first_query), + "--readiness-attempts", + "2", + "--memory-max", + "256M", + "--memory-high", + "224M", + "--swap-max", + "0", + "--output", + str(output), + ] + result = subprocess.run( + command, + text=True, + capture_output=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + with output.open("r", encoding="utf-8", newline="") as stream: + row = next(csv.DictReader(stream, delimiter="\t")) + self.assertEqual(row["status"], "passed") + self.assertEqual(row["resident_after_pages"], "0") + self.assertEqual(row["spawn_to_first_query_ms"], "25.000000") + self.assertEqual(row["io_observation_status"], "available") + self.assertEqual(row["io_first_touch_status"], "attributable") + + snapshot(first, first_query + 1_000_000, 120_000_000, None) + snapshot(final, first_query + 5_000_000, 125_000_000, None) + missing_output = self.root / "sample-missing-io.tsv" + command[-1] = str(missing_output) + missing_result = subprocess.run(command, text=True, capture_output=True) + self.assertEqual(missing_result.returncode, 0, missing_result.stderr) + with missing_output.open("r", encoding="utf-8", newline="") as stream: + missing_row = next(csv.DictReader(stream, delimiter="\t")) + self.assertEqual(missing_row["status"], "passed") + self.assertEqual(missing_row["io_observation_status"], "unavailable") + self.assertEqual(missing_row["io_controller_status"], "missing") + self.assertEqual(missing_row["io_missing_reason"], "io-controller-missing") + self.assertEqual(missing_row["io_first_touch_status"], "unavailable") + self.assertEqual(missing_row["io_read_bytes"], "") + self.assertEqual(missing_row["io_read_ios"], "") + + malformed = json.loads(final.read_text(encoding="utf-8")) + malformed["io"]["totals"] = {} + final.write_text(json.dumps(malformed) + "\n", encoding="utf-8") + malformed_output = self.root / "sample-malformed-missing-io.tsv" + command[-1] = str(malformed_output) + malformed_result = subprocess.run(command, text=True, capture_output=True) + self.assertNotEqual(malformed_result.returncode, 0) + self.assertIn("unavailable io totals must be null", malformed_result.stderr) + + def test_capture_parsers_reject_malformed_and_total_io(self) -> None: + capture = load_module(CAPTURE, "capture_linux_cgroup_v2") + devices, totals = capture.parse_io_stat( + "8:0 rbytes=10 wbytes=20 rios=1 wios=2\n8:1 rbytes=5 wbytes=7 rios=3 wios=4" + ) + self.assertEqual(len(devices), 2) + self.assertEqual(totals["rbytes"], 15) + self.assertEqual(totals["wios"], 6) + with self.assertRaises(capture.SnapshotError): + capture.parse_io_stat("8:0 rbytes=10 wbytes=20 rios=1") + with self.assertRaises(capture.SnapshotError): + capture.parse_pressure("some avg10=0.00 total=1") + event_root = self.root / "cgroup-fixture" + event_root.mkdir() + (event_root / "memory.events").write_text("high 0\n", encoding="ascii") + (event_root / "memory.events.local").write_text("high 0\n", encoding="ascii") + self.assertEqual( + capture.select_memory_events_file(event_root), + ("memory.events.local", "local-leaf"), + ) + (event_root / "child.scope").mkdir() + self.assertEqual( + capture.select_memory_events_file(event_root), + ("memory.events", "hierarchical-descendants"), + ) + missing_io = capture.capture_io_stat(event_root, ["cpu", "memory", "pids"]) + self.assertEqual( + missing_io, + { + "status": "unavailable", + "controller_status": "missing", + "missing_reason": "io-controller-missing", + "source": None, + "devices": None, + "totals": None, + }, + ) + selected = capture.select_file_cache_memory_stat( + {"active_file": 4096, "pgscan": 10} + ) + self.assertEqual(selected["status"], "partial") + self.assertEqual( + selected["fields"]["active_file"], + { + "kind": "gauge", + "unit": "bytes", + "status": "available", + "value": 4096, + }, + ) + self.assertIn("workingset_refault_file", selected["missing_keys"]) + + def test_repeated_summary_reports_nearest_rank_p50_p95(self) -> None: + validator = load_module(VALIDATOR, "validate_wasix_cold_ownership") + inputs: list[Path] = [] + for block, latency in enumerate((10, 20, 30, 40, 50), start=1): + sample = self.root / f"sample-{block}.tsv" + row = {name: "1" for name in validator.HEADER} + row.update( + { + "schema_version": "oliphaunt.wasix-postmaster.cold-ownership-sample.v1", + "target": "wasix", + "status": "passed", + "execution_identity_sha256": "a" * 64, + "carrier_root": str(self.carrier), + "pgdata_root": str(self.root / f"pgdata-{block}"), + "resident_after_pages": "0", + "spawn_to_first_query_ms": str(latency), + "full_valid_sample_count": "1", + "memory_max_bytes": "268435456", + "memory_high_bytes": "234881024", + "swap_max_bytes": "0", + "whole_scope_memory_peak_bytes": str(100_000_000 + block), + "peak_file_dirty_bytes": "4096", + "peak_file_writeback_bytes": "0", + "io_observation_status": "available", + "io_controller_status": "available", + "io_missing_reason": "none", + "io_first_touch_status": "attributable", + "io_read_bytes": "1048576", + "io_write_bytes": "8192", + } + ) + with sample.open("w", encoding="utf-8", newline="") as stream: + writer = csv.DictWriter(stream, fieldnames=validator.HEADER, delimiter="\t") + writer.writeheader() + writer.writerow(row) + inputs.extend([Path("--input"), sample]) + output = self.root / "summary.tsv" + receipt = self.root / "summary.json" + command = ["python3", str(SUMMARIZER)] + for block in range(1, 6): + command.extend(["--input", str(self.root / f"sample-{block}.tsv")]) + command.extend( + [ + "--expected-blocks", + "5", + "--output", + str(output), + "--receipt", + str(receipt), + ] + ) + subprocess.run(command, check=True) + with output.open("r", encoding="utf-8", newline="") as stream: + row = next(csv.DictReader(stream, delimiter="\t")) + self.assertEqual(row["spawn_to_first_query_p50_ms"], "30.000000") + self.assertEqual(row["spawn_to_first_query_p95_ms"], "50.000000") + self.assertEqual(row["status"], "passed") + self.assertEqual(row["io_first_touch_status"], "attributable") + self.assertEqual(row["io_attributable_block_count"], "5") + self.assertEqual(row["io_unavailable_block_count"], "0") + self.assertEqual(row["total_io_read_bytes"], str(5 * 1_048_576)) + + for block in range(1, 6): + sample = self.root / f"sample-{block}.tsv" + with sample.open("r", encoding="utf-8", newline="") as stream: + unavailable_row = next(csv.DictReader(stream, delimiter="\t")) + unavailable_row.update( + { + "io_observation_status": "unavailable", + "io_controller_status": "missing", + "io_missing_reason": "io-controller-missing", + "io_first_touch_status": "unavailable", + "io_read_bytes": "", + "io_write_bytes": "", + "io_read_ios": "", + "io_write_ios": "", + } + ) + with sample.open("w", encoding="utf-8", newline="") as stream: + writer = csv.DictWriter(stream, fieldnames=validator.HEADER, delimiter="\t") + writer.writeheader() + writer.writerow(unavailable_row) + unavailable_output = self.root / "summary-unavailable-io.tsv" + unavailable_receipt = self.root / "summary-unavailable-io.json" + command[-3] = str(unavailable_output) + command[-1] = str(unavailable_receipt) + subprocess.run(command, check=True) + with unavailable_output.open("r", encoding="utf-8", newline="") as stream: + unavailable_summary = next(csv.DictReader(stream, delimiter="\t")) + self.assertEqual(unavailable_summary["status"], "passed") + self.assertEqual(unavailable_summary["io_first_touch_status"], "unavailable") + self.assertEqual(unavailable_summary["io_attributable_block_count"], "0") + self.assertEqual(unavailable_summary["io_unavailable_block_count"], "5") + self.assertEqual(unavailable_summary["total_io_read_bytes"], "") + self.assertEqual(unavailable_summary["total_io_write_bytes"], "") + + def test_final_boundary_has_no_root_read_before_spawn(self) -> None: + text = BENCH.read_text(encoding="utf-8") + marker = "# FINAL COLD BOUNDARY:" + start = text.index(marker) + end = text.index("fresh_spawn_process_group -- launch_measured_server", start) + boundary = text[start:end] + forbidden = ( + "fresh_verify_sealed_headless_carrier", + "fresh_capture_qualification_carrier_identity", + "fresh_wasmer_bin_hash", + "sha256sum", + "prove-linux-cold-residency.py", + "$sealed_carrier_root/", + "$pgdata/", + ) + for token in forbidden: + self.assertNotIn(token, boundary) + executable = [ + line.strip() + for line in boundary.splitlines() + if line.strip() and not line.lstrip().startswith("#") + ] + self.assertEqual(executable, ['cold_spawn_monotonic_ns="$(now_ns)"', "fi"]) + launcher = shell_function(BENCH, "launch_measured_server") + self.assertIn('exec "$@"', launcher) + self.assertNotIn("sha256", launcher) + self.assertNotIn("sealed_carrier", launcher) + self.assertNotIn("pgdata", launcher.lower()) + supervisor = shell_function(PROCESS_SUPERVISION, "fresh_spawn_process_group") + self.assertIn('"$@" None: + strace = shutil.which("strace") + if strace is None: + self.skipTest("strace is required for the launch-boundary syscall proof") + sleep = shutil.which("sleep") + if sleep is None: + self.skipTest("sleep executable is required for the launch-boundary proof") + + cold_root = self.root / "launch-cold-root" + executable = cold_root / "bin" / "wasmer-headless" + executable.parent.mkdir(parents=True) + shutil.copy2(sleep, executable) + executable.chmod(0o555) + limits = self.root / "launch-limits.txt" + trace = self.root / "launch.trace" + script = self.root / "launch-wrapper.sh" + script.write_text( + "\n".join( + [ + "#!/usr/bin/env bash", + "set -euo pipefail", + f"source {shlex.quote(str(PROCESS_SUPERVISION))}", + "libpq_latency_samples=0", + "libpq_latency_soft_nofile=1024", + shell_function(BENCH, "launch_measured_server"), + 'fresh_spawn_process_group -- launch_measured_server "$2" "$1" 2', + 'wait "$FRESH_PROCESS_GROUP_PID"', + "", + ] + ), + encoding="utf-8", + ) + result = subprocess.run( + [ + strace, + "-f", + "-qq", + "-s", + "4096", + "-e", + "trace=open,openat,newfstatat,statx,access,readlink,readlinkat,execve", + "-o", + str(trace), + "bash", + str(script), + str(executable), + str(limits), + ], + text=True, + capture_output=True, + ) + trace_text = trace.read_text(encoding="utf-8") if trace.exists() else "" + self.assertEqual( + result.returncode, + 0, + ( + f"stderr:\n{result.stderr}\nstdout:\n{result.stdout}\ntrace tail:\n" + + "\n".join(trace_text.splitlines()[-100:]) + ), + ) + lines = trace_text.splitlines() + target = str(executable) + exec_index = next( + index + for index, line in enumerate(lines) + if "execve(" in line and target in line and line.find(target) < line.find("[") + ) + observed_syscalls = ( + "open(", + "openat(", + "newfstatat(", + "statx(", + "access(", + "readlink(", + "readlinkat(", + ) + premature = [ + line + for line in lines[:exec_index] + if str(cold_root) in line + and any(syscall in line for syscall in observed_syscalls) + ] + self.assertEqual(premature, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-libpq-latency.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-libpq-latency.py new file mode 100755 index 00000000..734ee059 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-libpq-latency.py @@ -0,0 +1,1051 @@ +#!/usr/bin/env python3 +"""Validate and compare paired native/WASIX true-libpq latency evidence. + +The input is a run index emitted by qualify-wasix-libpq-latency.sh. This +program deliberately re-opens the benchmark's raw samples and recomputes its +nearest-rank percentiles. A benchmark summary is evidence, not authority. +""" + +from __future__ import annotations + +import argparse +import csv +import hashlib +import math +import os +import re +import stat +import sys +from dataclasses import dataclass +from decimal import Decimal, InvalidOperation, getcontext +from pathlib import Path +from typing import Iterable, Sequence + + +getcontext().prec = 40 + +RUN_HEADER = [ + "schema_version", + "block", + "order", + "pair", + "position", + "target", + "run_label", + "harness_status", + "report_dir", + "effective_settings", + "effective_settings_sha256", + "carrier_closure_identity", + "native_oracle_identity", + "postgres_profile_resolution_identity", + "qualification_plan_identity", +] +PROFILE_COMPARISON_HEADER = [ + "schema_version", + "block", + "order", + "pair", + "native_settings", + "wasix_settings", + "comparison", + "comparison_sha256", + "status", +] +EFFECTIVE_SETTINGS_HEADER = ["name", "setting", "unit", "source"] +PROFILE_INPUT_HEADER = ["kind", "id", "path", "sha256"] +PROFILE_RESOLUTION_HEADER = [ + "name", + "value", + "source", + "profile_id", + "profile_path", + "profile_sha256", + "precedence", +] +PROFILE_RESULT_HEADER = [ + "name", + "native_setting", + "native_unit", + "native_source", + "wasix_setting", + "wasix_unit", + "wasix_source", + "status", +] +REQUIRED_SETTINGS = ( + "autovacuum_worker_slots", + "backend_flush_after", + "bgwriter_flush_after", + "checkpoint_flush_after", + "checkpoint_timeout", + "fsync", + "full_page_writes", + "io_method", + "max_connections", + "max_wal_senders", + "max_worker_processes", + "max_wal_size", + "min_wal_size", + "shared_buffers", + "synchronous_commit", + "wal_segment_size", +) +LATENCY_HEADER = [ + "schema_version", + "target", + "mode", + "status", + "clock", + "warmup_count", + "sample_count", + "p50_ns", + "p95_ns", + "p99_ns", + "p50_ms", + "p95_ms", + "p99_ms", + "raw_tsv", + "libpq_path", + "libpq_sha256", + "probe_sha256", +] +RAW_HEADER = [ + "schema_version", + "mode", + "phase", + "sample_index", + "duration_ns", + "status", +] +HOST_FD_HEADER = [ + "target", + "mode", + "before_open_fds", + "after_open_fds", + "quiescent_open_fds", + "quiescent_growth", + "allowance", + "status", +] +SERVER_LIMITS_HEADER = [ + "target", + "requested_soft_nofile", + "pre_soft_nofile", + "pre_hard_nofile", + "actual_soft_nofile", + "actual_hard_nofile", + "status", + "launch_record", +] +SERVER_LIFECYCLE_HEADER = [ + "target", + "server_pid", + "server_pgid", + "server_birth_identity", + "cgroup_path", + "cgroup_identity", + "orderly_int", + "forced", + "wait_status", + "clean_shutdown_marker", + "process_group_residue", + "cgroup_residue", + "port_residue", + "status", + "report", +] +INSTRUMENTATION_HEADER = [ + "schema_version", + "lane", + "wasix_perf_stats", + "wait_dump_policy", + "wait_dump_interval_ms", + "wait_dump_max_per_wait", + "wait_dump_verbose", + "fence_protocol", + "sanitized_environment", +] +WAIT_DUMP_ENVIRONMENT = ( + "WASIX_PERF_WAIT_DUMP_INTERVAL_MS", + "WASIX_PERF_WAIT_DUMP_FILE", + "WASIX_PERF_WAIT_DUMP_MAX_PER_WAIT", + "WASIX_PERF_WAIT_DUMP_VERBOSE", + "WASIX_WAIT_DUMP_INTERVAL_MS", + "WASIX_WAIT_DUMP_FILE", + "WASIX_WAIT_DUMP_MAX_PER_WAIT", + "WASIX_WAIT_DUMP_VERBOSE", + "WASIX_WAIT_DUMP_FENCE_REQUEST_FILE", + "WASIX_WAIT_DUMP_FENCE_ACK_FILE", +) +NATIVE_MANIFEST_HEADER = ["schema", "kind", "path", "bytes", "sha256_or_target"] +RECEIPT_HEADER = [ + "schema_version", + "block", + "order", + "pair", + "position", + "target", + "mode", + "run_label", + "status", + "clock", + "warmup_count", + "sample_count", + "p50_ns", + "p95_ns", + "p99_ns", + "raw_tsv", + "raw_sha256", + "latency_summary", + "latency_summary_sha256", + "host_fd_churn_sha256", + "server_limits_sha256", + "server_lifecycle_sha256", + "instrumentation_policy_sha256", + "effective_settings_sha256", + "carrier_closure_identity", + "native_oracle_identity", + "probe_source_sha256", + "probe_path", + "probe_sha256", + "libpq_path", + "libpq_sha256", + "postgres_profile_resolution_identity", + "qualification_plan_identity", + "report_dir", +] +PAIR_HEADER = [ + "schema_version", + "block", + "order", + "pair", + "first_target", + "second_target", + "mode", + "native_p95_ns", + "wasix_p95_ns", + "paired_p95_ratio", + "native_p99_ns", + "wasix_p99_ns", + "paired_p99_ratio", +] +SUMMARY_HEADER = [ + "schema_version", + "mode", + "status", + "server_pairs", + "native_server_runs", + "wasix_server_runs", + "max_p95_ratio", + "max_p99_ratio", + "max_wasix_p95_ms", + "max_wasix_p99_ms", + "paired_p95_ratio_p50", + "paired_p95_ratio_p95", + "paired_p99_ratio_p50", + "paired_p99_ratio_p95", + "wasix_p95_ms_p50", + "wasix_p95_ms_p95", + "wasix_p99_ms_p50", + "wasix_p99_ms_p95", + "detail", +] +IDENTITY_HEADER = [ + "schema_version", + "carrier_closure_identity", + "native_oracle_identity", + "probe_source_sha256", + "representative_probe_path", + "probe_sha256", + "libpq_path", + "libpq_sha256", + "postgres_profile_resolution_identity", + "qualification_plan_identity", +] + +SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +LABEL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") +MODES = ("persistent", "reconnect") + + +class EvidenceError(RuntimeError): + pass + + +def die(message: str) -> "None": + raise EvidenceError(message) + + +def canonical_positive(value: str, name: str, maximum: int = 10_000_000) -> int: + if not re.fullmatch(r"[1-9][0-9]*", value): + die(f"{name} must be a canonical positive integer: {value!r}") + number = int(value) + if number > maximum: + die(f"{name} exceeds {maximum}: {number}") + return number + + +def canonical_nonnegative(value: str, name: str, maximum: int = 10_000_000) -> int: + if not re.fullmatch(r"0|[1-9][0-9]*", value): + die(f"{name} must be a canonical nonnegative integer: {value!r}") + number = int(value) + if number > maximum: + die(f"{name} exceeds {maximum}: {number}") + return number + + +def sha256_bytes(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def require_sha256(value: str, name: str) -> str: + if not SHA256_RE.fullmatch(value): + die(f"{name} is not a lowercase SHA-256: {value!r}") + return value + + +def read_regular_bytes(path: Path, name: str) -> bytes: + try: + before = path.lstat() + except OSError as exc: + die(f"cannot stat {name} {path}: {exc}") + if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode): + die(f"{name} is not a regular non-symlink file: {path}") + try: + data = path.read_bytes() + after = path.lstat() + except OSError as exc: + die(f"cannot read {name} {path}: {exc}") + identity_before = (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) + identity_after = (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns) + if identity_before != identity_after or len(data) != after.st_size: + die(f"{name} changed while it was read: {path}") + return data + + +def read_tsv(path: Path, header: Sequence[str], name: str) -> tuple[list[dict[str, str]], bytes]: + data = read_regular_bytes(path, name) + try: + text = data.decode("utf-8") + except UnicodeDecodeError as exc: + die(f"{name} is not UTF-8: {path}: {exc}") + if "\r" in text or "\0" in text: + die(f"{name} contains forbidden control characters: {path}") + rows = list(csv.reader(text.splitlines(), delimiter="\t", strict=True)) + if not rows or rows[0] != list(header): + actual = "" if not rows else "\t".join(rows[0]) + die(f"{name} has an unexpected header: {actual}") + if any(len(row) != len(header) for row in rows[1:]): + die(f"{name} has a malformed row: {path}") + return [dict(zip(header, row, strict=True)) for row in rows[1:]], data + + +def nearest_rank(values: Sequence[Decimal | int], percentile: int): + if not values: + die("cannot calculate a percentile over an empty sample") + index = math.ceil(len(values) * percentile / 100) - 1 + return sorted(values)[index] + + +def decimal_arg(value: str, name: str) -> Decimal: + try: + parsed = Decimal(value) + except InvalidOperation: + die(f"{name} must be a decimal: {value!r}") + if not parsed.is_finite() or parsed <= 0: + die(f"{name} must be finite and greater than zero: {value!r}") + return parsed + + +def format_decimal(value: Decimal) -> str: + result = format(value.quantize(Decimal("0.000000001")), "f") + return result.rstrip("0").rstrip(".") if "." in result else result + + +def ns_to_ms(value: int) -> Decimal: + return Decimal(value) / Decimal(1_000_000) + + +def validate_raw(path: Path, mode: str, warmup: int, samples: int) -> tuple[dict[str, int], str]: + rows, data = read_tsv(path, RAW_HEADER, "raw latency evidence") + expected_rows = warmup + samples + if len(rows) != expected_rows: + die(f"raw latency row count mismatch for {path}: expected {expected_rows}, got {len(rows)}") + durations: list[int] = [] + seen_measure = False + counts = {"warmup": 0, "measure": 0} + for row in rows: + if row["schema_version"] != "1" or row["mode"] != mode or row["status"] != "ok": + die(f"raw latency identity/status mismatch in {path}") + phase = row["phase"] + if phase not in counts: + die(f"invalid raw latency phase in {path}: {phase!r}") + counts[phase] += 1 + index = canonical_positive(row["sample_index"], "raw sample_index") + if index != counts[phase]: + die(f"non-contiguous {phase} sample index in {path}: {index}") + duration = canonical_positive(row["duration_ns"], "raw duration_ns", maximum=10**15) + if phase == "measure": + seen_measure = True + durations.append(duration) + elif seen_measure: + die(f"warmup sample follows measurement sample in {path}") + if counts != {"warmup": warmup, "measure": samples}: + die(f"raw latency phase counts mismatch in {path}: {counts}") + return { + "p50_ns": nearest_rank(durations, 50), + "p95_ns": nearest_rank(durations, 95), + "p99_ns": nearest_rank(durations, 99), + }, sha256_bytes(data) + + +def parse_native_oracle(path: Path, expected_identity: str, install: Path) -> set[tuple[str, str]]: + rows, data = read_tsv(path, NATIVE_MANIFEST_HEADER, "native oracle manifest") + if sha256_bytes(data) != expected_identity: + die("native oracle manifest does not match the declared frozen identity") + allowed: set[tuple[str, str]] = set() + seen: set[str] = set() + for row in rows: + if row["schema"] != "oliphaunt.wasix-postmaster.native-oracle.v1": + die("native oracle manifest has an unexpected schema") + relative = row["path"] + if relative in seen or relative.startswith("/") or ".." in Path(relative).parts: + die(f"native oracle manifest has an unsafe or duplicate path: {relative!r}") + seen.add(relative) + if row["kind"] == "file": + canonical_nonnegative(row["bytes"], "native oracle bytes", maximum=10**12) + digest = require_sha256(row["sha256_or_target"], "native oracle file digest") + if Path(relative).name.startswith("libpq.") or Path(relative).name == "libpq.a": + allowed.add((str((install / relative).resolve()), digest)) + elif row["kind"] != "symlink": + die(f"native oracle manifest has an unexpected kind: {row['kind']!r}") + if not allowed: + die("native oracle manifest contains no regular libpq artifact") + return allowed + + +@dataclass(frozen=True) +class Run: + block: int + order: str + pair: int + position: int + target: str + label: str + report: Path + settings: Path + settings_sha256: str + + +@dataclass(frozen=True) +class Sample: + run: Run + mode: str + p50_ns: int + p95_ns: int + p99_ns: int + raw: Path + raw_sha256: str + summary: Path + summary_sha256: str + host_fd_sha256: str + server_limits_sha256: str + server_lifecycle_sha256: str + instrumentation_policy_sha256: str + probe_sha256: str + probe_path: Path + libpq_path: str + libpq_sha256: str + + +def validate_run_index(args: argparse.Namespace) -> list[Run]: + rows, _ = read_tsv(args.runs, RUN_HEADER, "latency run index") + expected_count = args.expected_blocks * 4 + if len(rows) != expected_count: + die(f"run index row count mismatch: expected {expected_count}, got {len(rows)}") + runs: list[Run] = [] + seen_labels: set[str] = set() + seen_reports: set[Path] = set() + seen_keys: set[tuple[int, int]] = set() + for row in rows: + if row["schema_version"] != "1": + die("run index contains an unexpected schema version") + block = canonical_positive(row["block"], "run block") + pair = canonical_positive(row["pair"], "run pair") + position = canonical_positive(row["position"], "run position") + if not (1 <= block <= args.expected_blocks and pair in (1, 2) and position in (1, 2, 3, 4)): + die("run index block/pair/position is out of range") + expected_order = "ABBA" if block % 2 else "BAAB" + if row["order"] != expected_order: + die(f"block {block} must use {expected_order}, got {row['order']!r}") + expected_targets = ("native", "wasix", "wasix", "native") if expected_order == "ABBA" else ("wasix", "native", "native", "wasix") + if row["target"] != expected_targets[position - 1] or pair != (position + 1) // 2: + die(f"run index violates {expected_order} pairing at block {block}, position {position}") + key = (block, position) + if key in seen_keys: + die(f"duplicate run index position: block {block}, position {position}") + seen_keys.add(key) + if row["harness_status"] != "0": + die(f"benchmark run did not pass: {row['run_label']} status={row['harness_status']}") + label = row["run_label"] + if not LABEL_RE.fullmatch(label) or label in seen_labels: + die(f"unsafe or duplicate run label: {label!r}") + seen_labels.add(label) + report = Path(row["report_dir"]) + expected_report = args.benchmark_reports_root / label + if ( + report != expected_report + or not report.is_absolute() + or report in seen_reports + or report.is_symlink() + or not report.is_dir() + ): + die(f"run report is not a unique absolute non-symlink directory: {report}") + seen_reports.add(report) + settings = Path(row["effective_settings"]) + expected_settings = report / row["target"] / "effective-postgres-settings.tsv" + if settings != expected_settings: + die(f"effective settings path is not bound to its report: {settings}") + settings_data = read_regular_bytes(settings, "effective PostgreSQL settings") + settings_sha = require_sha256(row["effective_settings_sha256"], "effective settings SHA-256") + if sha256_bytes(settings_data) != settings_sha: + die(f"effective settings hash mismatch: {settings}") + if row["carrier_closure_identity"] != args.carrier_identity: + die(f"carrier closure identity mismatch in run {label}") + if row["native_oracle_identity"] != args.native_oracle_identity: + die(f"native oracle identity mismatch in run {label}") + if row["postgres_profile_resolution_identity"] != args.profile_identity: + die(f"PostgreSQL profile identity mismatch in run {label}") + if row["qualification_plan_identity"] != args.plan_identity: + die(f"qualification plan identity mismatch in run {label}") + runs.append(Run(block, expected_order, pair, position, row["target"], label, report, settings, settings_sha)) + if seen_keys != {(block, position) for block in range(1, args.expected_blocks + 1) for position in range(1, 5)}: + die("run index is not a complete balanced block matrix") + return sorted(runs, key=lambda run: (run.block, run.position)) + + +def validate_profile_comparisons(args: argparse.Namespace, runs: Sequence[Run]) -> None: + rows, _ = read_tsv(args.profile_comparisons, PROFILE_COMPARISON_HEADER, "profile comparison index") + expected = args.expected_blocks * 2 + if len(rows) != expected: + die(f"profile comparison row count mismatch: expected {expected}, got {len(rows)}") + by_key = {(run.block, run.pair, run.target): run for run in runs} + seen: set[tuple[int, int]] = set() + for row in rows: + if row["schema_version"] != "1": + die("profile comparison has an unexpected schema version") + block = canonical_positive(row["block"], "profile comparison block") + pair = canonical_positive(row["pair"], "profile comparison pair") + key = (block, pair) + if key in seen or key not in {(b, p) for b in range(1, args.expected_blocks + 1) for p in (1, 2)}: + die(f"duplicate or unexpected profile comparison pair: {key}") + seen.add(key) + expected_order = "ABBA" if block % 2 else "BAAB" + if row["order"] != expected_order or row["status"] != "passed": + die(f"profile comparison did not pass for block {block}, pair {pair}") + native = by_key[(block, pair, "native")] + wasix = by_key[(block, pair, "wasix")] + if Path(row["native_settings"]) != native.settings or Path(row["wasix_settings"]) != wasix.settings: + die(f"profile comparison paths are not bound to block {block}, pair {pair}") + comparison = Path(row["comparison"]) + comparison_data = read_regular_bytes(comparison, "PostgreSQL settings comparison") + if sha256_bytes(comparison_data) != require_sha256(row["comparison_sha256"], "profile comparison SHA-256"): + die(f"profile comparison hash mismatch: {comparison}") + native_values = validate_effective_settings(native.settings) + wasix_values = validate_effective_settings(wasix.settings) + if native_values != wasix_values: + die(f"native/WASIX effective settings differ for block {block}, pair {pair}") + result_rows, result_data = read_tsv(comparison, PROFILE_RESULT_HEADER, "PostgreSQL settings comparison") + if result_data != comparison_data or len(result_rows) != len(REQUIRED_SETTINGS): + die(f"PostgreSQL settings comparison row count is invalid: {comparison}") + for expected_name, result in zip(REQUIRED_SETTINGS, result_rows, strict=True): + expected = native_values[expected_name] + if ( + result["name"] != expected_name + or result["status"] != "matched" + or (result["native_setting"], result["native_unit"], result["native_source"]) != expected + or (result["wasix_setting"], result["wasix_unit"], result["wasix_source"]) != expected + ): + die(f"PostgreSQL settings comparison is not exact: {comparison}") + + +def validate_effective_settings(path: Path) -> dict[str, tuple[str, str, str]]: + rows, _ = read_tsv(path, EFFECTIVE_SETTINGS_HEADER, "effective PostgreSQL settings") + values: dict[str, tuple[str, str, str]] = {} + for row in rows: + name = row["name"] + if not name or name in values: + die(f"effective PostgreSQL settings contain a duplicate/empty name: {path}") + values[name] = (row["setting"], row["unit"], row["source"]) + if set(values) != set(REQUIRED_SETTINGS): + die( + f"effective PostgreSQL settings key set mismatch: {path}: " + f"missing={sorted(set(REQUIRED_SETTINGS) - set(values))}, " + f"extra={sorted(set(values) - set(REQUIRED_SETTINGS))}" + ) + return values + + +def validate_auxiliary_artifacts(run: Run) -> tuple[str, str, str, str]: + fd_rows, fd_data = read_tsv(run.report / "host-fd-churn-summary.tsv", HOST_FD_HEADER, "host FD churn summary") + if len(fd_rows) != 2 or {row["mode"] for row in fd_rows} != set(MODES): + die(f"host FD churn summary is incomplete: {run.report}") + for row in fd_rows: + if row["target"] != run.target or row["status"] != "passed" or row["allowance"] != "0" or row["quiescent_growth"] != "0": + die(f"host FD churn did not pass with zero allowance: {run.report}") + + limit_rows, limit_data = read_tsv(run.report / "server-limits.tsv", SERVER_LIMITS_HEADER, "server limits summary") + if len(limit_rows) != 1: + die(f"server limits summary must contain exactly one row: {run.report}") + limit = limit_rows[0] + if limit["target"] != run.target or limit["status"] != "passed" or limit["requested_soft_nofile"] != "1024" or limit["actual_soft_nofile"] != "1024": + die(f"server limit setup did not pass with the declared 1024-FD lane: {run.report}") + + lifecycle_rows, lifecycle_data = read_tsv(run.report / "server-lifecycle.tsv", SERVER_LIFECYCLE_HEADER, "server lifecycle summary") + if len(lifecycle_rows) != 1: + die(f"server lifecycle summary must contain exactly one row: {run.report}") + lifecycle = lifecycle_rows[0] + if ( + lifecycle["target"] != run.target + or lifecycle["status"] != "passed" + or lifecycle["orderly_int"] != "1" + or lifecycle["forced"] != "none" + or lifecycle["wait_status"] != "0" + or lifecycle["clean_shutdown_marker"] != "1" + or lifecycle["process_group_residue"] != "0" + or lifecycle["cgroup_residue"] != "0" + or lifecycle["port_residue"] != "0" + ): + die(f"server lifecycle was not a clean, orderly shutdown: {run.report}") + instrumentation_rows, instrumentation_data = read_tsv( + run.report / "instrumentation-policy.tsv", + INSTRUMENTATION_HEADER, + "instrumentation policy receipt", + ) + if len(instrumentation_rows) != 1: + die(f"instrumentation policy must contain exactly one row: {run.report}") + instrumentation = instrumentation_rows[0] + if ( + instrumentation["schema_version"] != "oliphaunt.wasix-postmaster.instrumentation.v1" + or instrumentation["lane"] != "benchmark" + or instrumentation["wasix_perf_stats"] != "0" + or instrumentation["wait_dump_policy"] != "prohibited" + or instrumentation["wait_dump_interval_ms"] != "0" + or instrumentation["wait_dump_max_per_wait"] != "0" + or instrumentation["wait_dump_verbose"] != "0" + or instrumentation["fence_protocol"] != "none" + or instrumentation["sanitized_environment"] != " ".join(WAIT_DUMP_ENVIRONMENT) + ): + die(f"timed sample has a noncanonical instrumentation policy: {run.report}") + return ( + sha256_bytes(fd_data), + sha256_bytes(limit_data), + sha256_bytes(lifecycle_data), + sha256_bytes(instrumentation_data), + ) + + +def validate_profile_evidence(run: Run, args: argparse.Namespace) -> None: + expected_inputs = read_regular_bytes(args.profile_inputs, "qualification profile inputs") + expected_resolution = read_regular_bytes(args.profile_resolution, "qualification profile resolution") + if read_regular_bytes(run.report / "postgres-profile-inputs.tsv", "sample profile inputs") != expected_inputs: + die(f"sample profile inputs differ from the qualification input: {run.report}") + if read_regular_bytes(run.report / "postgres-profile-resolution.tsv", "sample profile resolution") != expected_resolution: + die(f"sample profile resolution differs from the qualification input: {run.report}") + + +def validate_qualification_profile_identity(args: argparse.Namespace) -> None: + input_rows, _ = read_tsv(args.profile_inputs, PROFILE_INPUT_HEADER, "qualification profile inputs") + resolution_rows, _ = read_tsv( + args.profile_resolution, + PROFILE_RESOLUTION_HEADER, + "qualification profile resolution", + ) + if not input_rows or not resolution_rows: + die("qualification requires nonempty named-profile evidence") + identity_lines = ["schema\toliphaunt.wasix-postmaster.postgres-profile-resolution.v1"] + seen_inputs: set[tuple[str, str]] = set() + for row in input_rows: + key = (row["kind"], row["id"]) + if key in seen_inputs or row["kind"] not in ("runtime-footprint", "durability"): + die(f"duplicate or unexpected qualification profile input: {key}") + seen_inputs.add(key) + digest = require_sha256(row["sha256"], "profile input SHA-256") + source_path = Path(row["path"]) + if not source_path.is_absolute(): + die(f"profile input path is not absolute: {source_path}") + if sha256_bytes(read_regular_bytes(source_path, "profile input source")) != digest: + die(f"profile input source hash mismatch: {source_path}") + identity_lines.append(f"input\t{row['kind']}\t{row['id']}\t{digest}") + names: list[str] = [] + for row in resolution_rows: + name = row["name"] + if not name or name in names: + die("qualification profile resolution has a duplicate/empty name") + names.append(name) + require_sha256(row["profile_sha256"], "resolved profile SHA-256") + if row["source"] not in ("runtime-footprint", "durability", "explicit"): + die(f"unexpected profile resolution source: {row['source']!r}") + canonical_positive(row["precedence"], "profile precedence", maximum=3) + identity_lines.append( + "\t".join( + ( + "setting", + name, + row["value"], + row["source"], + row["profile_id"], + row["profile_sha256"], + row["precedence"], + ) + ) + ) + if names != sorted(names): + die("qualification profile resolution rows are not in canonical name order") + computed = sha256_bytes(("\n".join(identity_lines) + "\n").encode("utf-8")) + if computed != args.profile_identity: + die("qualification profile receipts do not match the declared semantic identity") + + +def validate_samples(args: argparse.Namespace, runs: Sequence[Run], allowed_libpq: set[tuple[str, str]]) -> list[Sample]: + samples: list[Sample] = [] + probe_hashes: set[str] = set() + libpq_identities: set[tuple[str, str]] = set() + raw_paths: set[Path] = set() + for run in runs: + validate_profile_evidence(run, args) + fd_sha, limit_sha, lifecycle_sha, instrumentation_sha = validate_auxiliary_artifacts(run) + probe_path = args.benchmark_runs_root / run.label / "libpq-latency-probe" + probe_data = read_regular_bytes(probe_path, "libpq latency probe binary") + probe_binary_sha = sha256_bytes(probe_data) + summary_path = run.report / "libpq-latency-summary.tsv" + summary_rows, summary_data = read_tsv(summary_path, LATENCY_HEADER, "libpq latency summary") + if len(summary_rows) != 2 or {row["mode"] for row in summary_rows} != set(MODES): + die(f"libpq latency summary is incomplete: {summary_path}") + summary_sha = sha256_bytes(summary_data) + for row in summary_rows: + mode = row["mode"] + if ( + row["schema_version"] != "1" + or row["target"] != run.target + or row["status"] != "ok" + or row["clock"] != "CLOCK_MONOTONIC" + or canonical_nonnegative(row["warmup_count"], "summary warmup_count") != args.expected_warmup + or canonical_positive(row["sample_count"], "summary sample_count") != args.expected_samples + ): + die(f"latency summary identity/count/status mismatch: {summary_path}") + raw_path = Path(row["raw_tsv"]) + expected_raw = run.report / run.target / "libpq-latency" / f"{mode}.raw.tsv" + if raw_path != expected_raw or raw_path in raw_paths: + die(f"raw latency path is unbound or duplicated: {raw_path}") + raw_paths.add(raw_path) + computed, raw_sha = validate_raw(raw_path, mode, args.expected_warmup, args.expected_samples) + summary_values = { + name: canonical_positive(row[name], f"summary {name}", maximum=10**15) + for name in ("p50_ns", "p95_ns", "p99_ns") + } + if computed != summary_values: + die(f"summary percentiles do not match raw evidence: {raw_path}") + for percentile in ("p50", "p95", "p99"): + expected_ms = format( + Decimal(summary_values[percentile + "_ns"]) / Decimal(1_000_000), + ".6f", + ) + if row[percentile + "_ms"] != expected_ms: + die(f"summary {percentile}_ms does not match {percentile}_ns: {summary_path}") + probe_sha = require_sha256(row["probe_sha256"], "probe SHA-256") + if probe_sha != probe_binary_sha: + die(f"probe SHA-256 does not match the exact run binary: {probe_path}") + libpq_sha = require_sha256(row["libpq_sha256"], "libpq SHA-256") + libpq_path = Path(row["libpq_path"]) + if not libpq_path.is_absolute(): + die(f"libpq path is not absolute: {libpq_path}") + libpq_data = read_regular_bytes(libpq_path, "linked libpq artifact") + if sha256_bytes(libpq_data) != libpq_sha: + die(f"linked libpq hash mismatch: {libpq_path}") + libpq_identity = (str(libpq_path.resolve()), libpq_sha) + if libpq_identity not in allowed_libpq: + die(f"linked libpq is outside the frozen native oracle: {libpq_path}") + probe_hashes.add(probe_sha) + libpq_identities.add(libpq_identity) + samples.append( + Sample( + run, + mode, + summary_values["p50_ns"], + summary_values["p95_ns"], + summary_values["p99_ns"], + raw_path, + raw_sha, + summary_path, + summary_sha, + fd_sha, + limit_sha, + lifecycle_sha, + instrumentation_sha, + probe_sha, + probe_path, + str(libpq_path.resolve()), + libpq_sha, + ) + ) + if len(probe_hashes) != 1: + die(f"probe binary identity changed between runs: {sorted(probe_hashes)}") + if len(libpq_identities) != 1: + die(f"linked libpq identity changed between runs: {sorted(libpq_identities)}") + return sorted(samples, key=lambda item: (item.run.block, item.run.position, item.mode)) + + +def write_tsv(path: Path, header: Sequence[str], rows: Iterable[Sequence[object]]) -> None: + if path.exists() or path.is_symlink(): + die(f"refusing to replace output: {path}") + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp") + try: + with temporary.open("w", encoding="utf-8", newline="") as handle: + writer = csv.writer(handle, delimiter="\t", lineterminator="\n") + writer.writerow(header) + writer.writerows(rows) + os.replace(temporary, path) + finally: + try: + temporary.unlink() + except FileNotFoundError: + pass + + +def compare_and_write(args: argparse.Namespace, samples: Sequence[Sample]) -> bool: + by_key = {(sample.run.block, sample.run.pair, sample.run.target, sample.mode): sample for sample in samples} + pair_rows: list[list[object]] = [] + summaries: list[list[object]] = [] + all_passed = True + budgets = { + "persistent": ( + args.max_persistent_p95_ratio, + args.max_persistent_p99_ratio, + args.max_wasix_persistent_p95_ms, + args.max_wasix_persistent_p99_ms, + ), + "reconnect": ( + args.max_reconnect_p95_ratio, + args.max_reconnect_p99_ratio, + args.max_wasix_reconnect_p95_ms, + args.max_wasix_reconnect_p99_ms, + ), + } + for mode in MODES: + p95_ratios: list[Decimal] = [] + p99_ratios: list[Decimal] = [] + wasix_p95_values: list[Decimal] = [] + wasix_p99_values: list[Decimal] = [] + for block in range(1, args.expected_blocks + 1): + order = "ABBA" if block % 2 else "BAAB" + for pair in (1, 2): + native = by_key[(block, pair, "native", mode)] + wasix = by_key[(block, pair, "wasix", mode)] + p95_ratio = Decimal(wasix.p95_ns) / Decimal(native.p95_ns) + p99_ratio = Decimal(wasix.p99_ns) / Decimal(native.p99_ns) + p95_ratios.append(p95_ratio) + p99_ratios.append(p99_ratio) + wasix_p95_values.append(ns_to_ms(wasix.p95_ns)) + wasix_p99_values.append(ns_to_ms(wasix.p99_ns)) + first, second = sorted((native, wasix), key=lambda item: item.run.position) + pair_rows.append( + [ + "1", + block, + order, + pair, + first.run.target, + second.run.target, + mode, + native.p95_ns, + wasix.p95_ns, + format_decimal(p95_ratio), + native.p99_ns, + wasix.p99_ns, + format_decimal(p99_ratio), + ] + ) + observed = ( + nearest_rank(p95_ratios, 95), + nearest_rank(p99_ratios, 95), + nearest_rank(wasix_p95_values, 95), + nearest_rank(wasix_p99_values, 95), + ) + limits = budgets[mode] + labels = ("paired_p95_ratio_p95", "paired_p99_ratio_p95", "wasix_p95_ms_p95", "wasix_p99_ms_p95") + failures = [ + f"{label}={format_decimal(value)}>{format_decimal(limit)}" + for label, value, limit in zip(labels, observed, limits, strict=True) + if value > limit + ] + status = "failed" if failures else "passed" + all_passed = all_passed and not failures + summaries.append( + [ + "1", + mode, + status, + args.expected_blocks * 2, + args.expected_blocks * 2, + args.expected_blocks * 2, + *(format_decimal(limit) for limit in limits), + format_decimal(nearest_rank(p95_ratios, 50)), + format_decimal(observed[0]), + format_decimal(nearest_rank(p99_ratios, 50)), + format_decimal(observed[1]), + format_decimal(nearest_rank(wasix_p95_values, 50)), + format_decimal(observed[2]), + format_decimal(nearest_rank(wasix_p99_values, 50)), + format_decimal(observed[3]), + ";".join(failures) if failures else "all-declared-gates-passed", + ] + ) + + receipt_rows = [ + [ + "1", + sample.run.block, + sample.run.order, + sample.run.pair, + sample.run.position, + sample.run.target, + sample.mode, + sample.run.label, + "passed", + "CLOCK_MONOTONIC", + args.expected_warmup, + args.expected_samples, + sample.p50_ns, + sample.p95_ns, + sample.p99_ns, + sample.raw, + sample.raw_sha256, + sample.summary, + sample.summary_sha256, + sample.host_fd_sha256, + sample.server_limits_sha256, + sample.server_lifecycle_sha256, + sample.instrumentation_policy_sha256, + sample.run.settings_sha256, + args.carrier_identity, + args.native_oracle_identity, + args.probe_source_sha256, + sample.probe_path, + sample.probe_sha256, + sample.libpq_path, + sample.libpq_sha256, + args.profile_identity, + args.plan_identity, + sample.run.report, + ] + for sample in samples + ] + identity_sample = samples[0] + write_tsv(args.receipt_output, RECEIPT_HEADER, receipt_rows) + write_tsv(args.pairs_output, PAIR_HEADER, pair_rows) + write_tsv(args.summary_output, SUMMARY_HEADER, summaries) + write_tsv( + args.identity_output, + IDENTITY_HEADER, + [[ + "1", + args.carrier_identity, + args.native_oracle_identity, + args.probe_source_sha256, + identity_sample.probe_path, + identity_sample.probe_sha256, + identity_sample.libpq_path, + identity_sample.libpq_sha256, + args.profile_identity, + args.plan_identity, + ]], + ) + return all_passed + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--runs", type=Path, required=True) + parser.add_argument("--profile-comparisons", type=Path, required=True) + parser.add_argument("--profile-inputs", type=Path, required=True) + parser.add_argument("--profile-resolution", type=Path, required=True) + parser.add_argument("--native-oracle-manifest", type=Path, required=True) + parser.add_argument("--native-install-dir", type=Path, required=True) + parser.add_argument("--benchmark-reports-root", type=Path, required=True) + parser.add_argument("--benchmark-runs-root", type=Path, required=True) + parser.add_argument("--expected-blocks", type=int, required=True) + parser.add_argument("--expected-warmup", type=int, required=True) + parser.add_argument("--expected-samples", type=int, required=True) + parser.add_argument("--carrier-identity", required=True) + parser.add_argument("--native-oracle-identity", required=True) + parser.add_argument("--profile-identity", required=True) + parser.add_argument("--probe-source-sha256", required=True) + parser.add_argument("--plan-identity", required=True) + parser.add_argument("--max-persistent-p95-ratio", default="2.0") + parser.add_argument("--max-persistent-p99-ratio", default="2.5") + parser.add_argument("--max-reconnect-p95-ratio", default="3.5") + parser.add_argument("--max-reconnect-p99-ratio", default="4.5") + parser.add_argument("--max-wasix-persistent-p95-ms", default="0.25") + parser.add_argument("--max-wasix-persistent-p99-ms", default="0.40") + parser.add_argument("--max-wasix-reconnect-p95-ms", default="20") + parser.add_argument("--max-wasix-reconnect-p99-ms", default="30") + parser.add_argument("--receipt-output", type=Path, required=True) + parser.add_argument("--pairs-output", type=Path, required=True) + parser.add_argument("--summary-output", type=Path, required=True) + parser.add_argument("--identity-output", type=Path, required=True) + args = parser.parse_args(argv) + if args.expected_blocks <= 0 or args.expected_blocks % 2 or args.expected_warmup < 0 or args.expected_samples <= 0: + parser.error("expected blocks must be positive/even, samples positive, and warmup nonnegative") + for name in ("carrier_identity", "native_oracle_identity", "profile_identity", "probe_source_sha256", "plan_identity"): + require_sha256(getattr(args, name), name.replace("_", " ")) + for name in ( + "max_persistent_p95_ratio", + "max_persistent_p99_ratio", + "max_reconnect_p95_ratio", + "max_reconnect_p99_ratio", + "max_wasix_persistent_p95_ms", + "max_wasix_persistent_p99_ms", + "max_wasix_reconnect_p95_ms", + "max_wasix_reconnect_p99_ms", + ): + setattr(args, name, decimal_arg(getattr(args, name), "--" + name.replace("_", "-"))) + if ( + not args.benchmark_reports_root.is_absolute() + or args.benchmark_reports_root.is_symlink() + or not args.benchmark_reports_root.is_dir() + ): + parser.error("--benchmark-reports-root must be an absolute non-symlink directory") + if ( + not args.benchmark_runs_root.is_absolute() + or args.benchmark_runs_root.is_symlink() + or not args.benchmark_runs_root.is_dir() + ): + parser.error("--benchmark-runs-root must be an absolute non-symlink directory") + if ( + not args.native_install_dir.is_absolute() + or args.native_install_dir.is_symlink() + or not args.native_install_dir.is_dir() + ): + parser.error("--native-install-dir must be an absolute non-symlink directory") + return args + + +def main(argv: Sequence[str]) -> int: + try: + args = parse_args(argv) + allowed_libpq = parse_native_oracle(args.native_oracle_manifest, args.native_oracle_identity, args.native_install_dir.resolve()) + validate_qualification_profile_identity(args) + runs = validate_run_index(args) + validate_profile_comparisons(args, runs) + samples = validate_samples(args, runs, allowed_libpq) + return 0 if compare_and_write(args, samples) else 1 + except EvidenceError as exc: + print(f"libpq latency comparison failed closed: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-libpq-latency.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-libpq-latency.test.py new file mode 100755 index 00000000..57e8c9a3 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-libpq-latency.test.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import hashlib +import subprocess +import sys +import tempfile +from pathlib import Path + + +SCRIPT = Path(__file__).with_name("compare-libpq-latency.py") +LATENCY_HEADER = [ + "schema_version", "target", "mode", "status", "clock", "warmup_count", + "sample_count", "p50_ns", "p95_ns", "p99_ns", "p50_ms", "p95_ms", + "p99_ms", "raw_tsv", "libpq_path", "libpq_sha256", "probe_sha256", +] +RUN_HEADER = [ + "schema_version", "block", "order", "pair", "position", "target", + "run_label", "harness_status", "report_dir", "effective_settings", + "effective_settings_sha256", "carrier_closure_identity", + "native_oracle_identity", "postgres_profile_resolution_identity", + "qualification_plan_identity", +] +PROFILE_HEADER = [ + "schema_version", "block", "order", "pair", "native_settings", + "wasix_settings", "comparison", "comparison_sha256", "status", +] +REQUIRED_SETTINGS = ( + "autovacuum_worker_slots", "backend_flush_after", "bgwriter_flush_after", + "checkpoint_flush_after", "checkpoint_timeout", "fsync", + "full_page_writes", "io_method", "max_connections", "max_wal_senders", + "max_worker_processes", "max_wal_size", "min_wal_size", + "shared_buffers", "synchronous_commit", "wal_segment_size", +) +SANITIZED_WAIT_DUMP_ENVIRONMENT = " ".join(( + "WASIX_PERF_WAIT_DUMP_INTERVAL_MS", "WASIX_PERF_WAIT_DUMP_FILE", + "WASIX_PERF_WAIT_DUMP_MAX_PER_WAIT", "WASIX_PERF_WAIT_DUMP_VERBOSE", + "WASIX_WAIT_DUMP_INTERVAL_MS", "WASIX_WAIT_DUMP_FILE", + "WASIX_WAIT_DUMP_MAX_PER_WAIT", "WASIX_WAIT_DUMP_VERBOSE", + "WASIX_WAIT_DUMP_FENCE_REQUEST_FILE", + "WASIX_WAIT_DUMP_FENCE_ACK_FILE", +)) + + +def digest(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def write_tsv(path: Path, header: list[str], rows: list[list[object]]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.writer(handle, delimiter="\t", lineterminator="\n") + writer.writerow(header) + writer.writerows(rows) + + +def file_digest(path: Path) -> str: + return digest(path.read_bytes()) + + +def make_fixture(root: Path) -> dict[str, object]: + install = root / "native" + (install / "lib").mkdir(parents=True) + libpq = install / "lib" / "libpq.so.5" + libpq.write_bytes(b"exact fake libpq\n") + libpq_sha = file_digest(libpq) + manifest = root / "native-oracle.tsv" + write_tsv( + manifest, + ["schema", "kind", "path", "bytes", "sha256_or_target"], + [["oliphaunt.wasix-postmaster.native-oracle.v1", "file", "lib/libpq.so.5", len(libpq.read_bytes()), libpq_sha]], + ) + native_identity = file_digest(manifest) + + profile_source = root / "embedded-concurrent-v1.gucs" + profile_source.write_text("shared_buffers=32MB\n", encoding="utf-8") + profile_source_sha = file_digest(profile_source) + profile_inputs = root / "postgres-profile-inputs.tsv" + profile_resolution = root / "postgres-profile-resolution.tsv" + write_tsv(profile_inputs, ["kind", "id", "path", "sha256"], [["runtime-footprint", "embedded-concurrent", profile_source, profile_source_sha]]) + write_tsv(profile_resolution, ["name", "value", "source", "profile_id", "profile_path", "profile_sha256", "precedence"], [["shared_buffers", "32MB", "runtime-footprint", "embedded-concurrent", profile_source, profile_source_sha, "1"]]) + + carrier = "a" * 64 + profile = digest( + ( + "schema\toliphaunt.wasix-postmaster.postgres-profile-resolution.v1\n" + f"input\truntime-footprint\tembedded-concurrent\t{profile_source_sha}\n" + f"setting\tshared_buffers\t32MB\truntime-footprint\tembedded-concurrent\t{profile_source_sha}\t1\n" + ).encode() + ) + probe_source = "c" * 64 + plan = "d" * 64 + probe_bytes = b"exact fake latency probe\n" + probe = digest(probe_bytes) + run_root = root / "runs-root" + run_rows: list[list[object]] = [] + comparison_rows: list[list[object]] = [] + settings_by_key: dict[tuple[int, int, str], Path] = {} + reports: list[Path] = [] + for block in (1, 2): + order = "ABBA" if block == 1 else "BAAB" + targets = ("native", "wasix", "wasix", "native") if block == 1 else ("wasix", "native", "native", "wasix") + for position, target in enumerate(targets, 1): + pair = (position + 1) // 2 + label = f"fixture-b{block:02d}-p{position}-{target}" + report = root / "reports" / label + report.mkdir(parents=True) + reports.append(report) + probe_path = run_root / label / "libpq-latency-probe" + probe_path.parent.mkdir(parents=True) + probe_path.write_bytes(probe_bytes) + settings = report / target / "effective-postgres-settings.tsv" + write_tsv( + settings, + ["name", "setting", "unit", "source"], + [[name, "value", "", "command line"] for name in REQUIRED_SETTINGS], + ) + settings_by_key[(block, pair, target)] = settings + (report / "postgres-profile-inputs.tsv").write_bytes(profile_inputs.read_bytes()) + (report / "postgres-profile-resolution.tsv").write_bytes(profile_resolution.read_bytes()) + + latency_rows: list[list[object]] = [] + for mode in ("persistent", "reconnect"): + if mode == "persistent": + value = 100_000 if target == "native" else 150_000 + else: + value = 5_000_000 if target == "native" else 12_000_000 + raw = report / target / "libpq-latency" / f"{mode}.raw.tsv" + write_tsv( + raw, + ["schema_version", "mode", "phase", "sample_index", "duration_ns", "status"], + [["1", mode, "warmup", 1, value, "ok"]] + + [["1", mode, "measure", sample, value, "ok"] for sample in range(1, 6)], + ) + latency_rows.append([ + "1", target, mode, "ok", "CLOCK_MONOTONIC", 1, 5, + value, value, value, f"{value / 1_000_000:.6f}", + f"{value / 1_000_000:.6f}", f"{value / 1_000_000:.6f}", + raw, libpq, libpq_sha, probe, + ]) + write_tsv(report / "libpq-latency-summary.tsv", LATENCY_HEADER, latency_rows) + write_tsv( + report / "host-fd-churn-summary.tsv", + ["target", "mode", "before_open_fds", "after_open_fds", "quiescent_open_fds", "quiescent_growth", "allowance", "status"], + [[target, mode, 10, 10, 10, 0, 0, "passed"] for mode in ("persistent", "reconnect")], + ) + write_tsv( + report / "server-limits.tsv", + ["target", "requested_soft_nofile", "pre_soft_nofile", "pre_hard_nofile", "actual_soft_nofile", "actual_hard_nofile", "status", "launch_record"], + [[target, 1024, 4096, 4096, 1024, 4096, "passed", report / "launch.tsv"]], + ) + write_tsv( + report / "server-lifecycle.tsv", + ["target", "server_pid", "server_pgid", "server_birth_identity", "cgroup_path", "cgroup_identity", "orderly_int", "forced", "wait_status", "clean_shutdown_marker", "process_group_residue", "cgroup_residue", "port_residue", "status", "report"], + [[target, 100, 100, "birth", "", "", 1, "none", 0, 1, 0, 0, 0, "passed", report / "shutdown.tsv"]], + ) + write_tsv( + report / "instrumentation-policy.tsv", + ["schema_version", "lane", "wasix_perf_stats", "wait_dump_policy", "wait_dump_interval_ms", "wait_dump_max_per_wait", "wait_dump_verbose", "fence_protocol", "sanitized_environment"], + [["oliphaunt.wasix-postmaster.instrumentation.v1", "benchmark", 0, "prohibited", 0, 0, 0, "none", SANITIZED_WAIT_DUMP_ENVIRONMENT]], + ) + run_rows.append([ + "1", block, order, pair, position, target, label, 0, report, + settings, file_digest(settings), carrier, native_identity, profile, plan, + ]) + for pair in (1, 2): + native = settings_by_key[(block, pair, "native")] + wasix = settings_by_key[(block, pair, "wasix")] + comparison = root / "comparisons" / f"b{block:02d}-p{pair}.tsv" + write_tsv( + comparison, + ["name", "native_setting", "native_unit", "native_source", "wasix_setting", "wasix_unit", "wasix_source", "status"], + [[name, "value", "", "command line", "value", "", "command line", "matched"] for name in REQUIRED_SETTINGS], + ) + comparison_rows.append(["1", block, order, pair, native, wasix, comparison, file_digest(comparison), "passed"]) + + runs = root / "runs.tsv" + comparisons = root / "profile-comparisons.tsv" + write_tsv(runs, RUN_HEADER, run_rows) + write_tsv(comparisons, PROFILE_HEADER, comparison_rows) + return { + "root": root, + "install": install, + "libpq": libpq, + "manifest": manifest, + "native_identity": native_identity, + "profile_inputs": profile_inputs, + "profile_resolution": profile_resolution, + "carrier": carrier, + "profile": profile, + "probe_source": probe_source, + "plan": plan, + "runs": runs, + "comparisons": comparisons, + "reports": reports, + "run_root": run_root, + } + + +def command(fixture: dict[str, object], suffix: str, *extra: str) -> list[str]: + root = fixture["root"] + assert isinstance(root, Path) + return [ + sys.executable, str(SCRIPT), + "--runs", str(fixture["runs"]), + "--profile-comparisons", str(fixture["comparisons"]), + "--profile-inputs", str(fixture["profile_inputs"]), + "--profile-resolution", str(fixture["profile_resolution"]), + "--native-oracle-manifest", str(fixture["manifest"]), + "--native-install-dir", str(fixture["install"]), + "--benchmark-reports-root", str(root / "reports"), + "--benchmark-runs-root", str(fixture["run_root"]), + "--expected-blocks", "2", "--expected-warmup", "1", "--expected-samples", "5", + "--carrier-identity", str(fixture["carrier"]), + "--native-oracle-identity", str(fixture["native_identity"]), + "--profile-identity", str(fixture["profile"]), + "--probe-source-sha256", str(fixture["probe_source"]), + "--plan-identity", str(fixture["plan"]), + "--receipt-output", str(root / f"samples-{suffix}.tsv"), + "--pairs-output", str(root / f"pairs-{suffix}.tsv"), + "--summary-output", str(root / f"summary-{suffix}.tsv"), + "--identity-output", str(root / f"identity-{suffix}.tsv"), + *extra, + ] + + +def run(command_line: list[str], expected: int) -> subprocess.CompletedProcess[str]: + result = subprocess.run(command_line, text=True, capture_output=True, check=False) + if result.returncode != expected: + raise AssertionError(f"expected exit {expected}, got {result.returncode}\nstdout={result.stdout}\nstderr={result.stderr}") + return result + + +def replace_cell(path: Path, header_name: str, row_index: int, value: str) -> None: + with path.open(encoding="utf-8", newline="") as handle: + rows = list(csv.reader(handle, delimiter="\t")) + column = rows[0].index(header_name) + rows[row_index][column] = value + write_tsv(path, rows[0], rows[1:]) + + +def main() -> None: + with tempfile.TemporaryDirectory(prefix="libpq-latency-comparator-") as temporary: + root = Path(temporary) + + valid = make_fixture(root / "valid") + run(command(valid, "valid"), 0) + summary = (valid["root"] / "summary-valid.tsv").read_text(encoding="utf-8") + assert "persistent\tpassed\t4\t4\t4" in summary + assert "reconnect\tpassed\t4\t4\t4" in summary + assert len((valid["root"] / "samples-valid.tsv").read_text().splitlines()) == 17 + assert len((valid["root"] / "pairs-valid.tsv").read_text().splitlines()) == 9 + + gated = make_fixture(root / "gated") + run(command(gated, "gated", "--max-persistent-p95-ratio", "1.49"), 1) + gated_summary = (gated["root"] / "summary-gated.tsv").read_text(encoding="utf-8") + assert "persistent\tfailed" in gated_summary + assert "paired_p95_ratio_p95=1.5>1.49" in gated_summary + assert (gated["root"] / "samples-gated.tsv").is_file() + + gate_cases = ( + ("persistent-p99-ratio", "--max-persistent-p99-ratio", "1.49", "paired_p99_ratio_p95"), + ("reconnect-p95-ratio", "--max-reconnect-p95-ratio", "2.39", "paired_p95_ratio_p95"), + ("reconnect-p99-ratio", "--max-reconnect-p99-ratio", "2.39", "paired_p99_ratio_p95"), + ("persistent-p95-absolute", "--max-wasix-persistent-p95-ms", "0.149", "wasix_p95_ms_p95"), + ("persistent-p99-absolute", "--max-wasix-persistent-p99-ms", "0.149", "wasix_p99_ms_p95"), + ("reconnect-p95-absolute", "--max-wasix-reconnect-p95-ms", "11.9", "wasix_p95_ms_p95"), + ("reconnect-p99-absolute", "--max-wasix-reconnect-p99-ms", "11.9", "wasix_p99_ms_p95"), + ) + for suffix, option, limit, expected_detail in gate_cases: + gate_fixture = make_fixture(root / suffix) + run(command(gate_fixture, suffix, option, limit), 1) + gate_summary = (gate_fixture["root"] / f"summary-{suffix}.tsv").read_text(encoding="utf-8") + assert expected_detail in gate_summary + + boundary = make_fixture(root / "boundary") + run( + command( + boundary, + "boundary", + "--max-persistent-p95-ratio", "1.5", + "--max-persistent-p99-ratio", "1.5", + "--max-reconnect-p95-ratio", "2.4", + "--max-reconnect-p99-ratio", "2.4", + "--max-wasix-persistent-p95-ms", "0.15", + "--max-wasix-persistent-p99-ms", "0.15", + "--max-wasix-reconnect-p95-ms", "12", + "--max-wasix-reconnect-p99-ms", "12", + ), + 0, + ) + + malformed = make_fixture(root / "raw-mutation") + report = malformed["reports"][0] + assert isinstance(report, Path) + raw = report / "native" / "libpq-latency" / "persistent.raw.tsv" + replace_cell(raw, "duration_ns", 2, "100001") + result = run(command(malformed, "raw-mutation"), 2) + assert "summary percentiles do not match raw evidence" in result.stderr + assert not (malformed["root"] / "samples-raw-mutation.tsv").exists() + + wrong_order = make_fixture(root / "wrong-order") + replace_cell(wrong_order["runs"], "order", 1, "BAAB") + result = run(command(wrong_order, "wrong-order"), 2) + assert "block 1 must use ABBA" in result.stderr + + identity = make_fixture(root / "identity") + replace_cell(identity["runs"], "qualification_plan_identity", 1, "f" * 64) + result = run(command(identity, "identity"), 2) + assert "qualification plan identity mismatch" in result.stderr + + probe = make_fixture(root / "probe") + report = probe["reports"][0] + assert isinstance(report, Path) + replace_cell(report / "libpq-latency-summary.tsv", "probe_sha256", 1, "f" * 64) + result = run(command(probe, "probe"), 2) + assert "probe SHA-256 does not match the exact run binary" in result.stderr + + profile = make_fixture(root / "profile") + replace_cell(profile["comparisons"], "status", 1, "failed") + result = run(command(profile, "profile"), 2) + assert "profile comparison did not pass" in result.stderr + + source = make_fixture(root / "profile-source") + source_path = source["root"] / "embedded-concurrent-v1.gucs" + source_path.write_text("shared_buffers=64MB\n", encoding="utf-8") + result = run(command(source, "profile-source"), 2) + assert "profile input source hash mismatch" in result.stderr + + settings = make_fixture(root / "settings") + wasix_settings = settings["reports"][1] / "wasix" / "effective-postgres-settings.tsv" + replace_cell(wasix_settings, "setting", 1, "different") + replace_cell(settings["runs"], "effective_settings_sha256", 2, file_digest(wasix_settings)) + result = run(command(settings, "settings"), 2) + assert "native/WASIX effective settings differ" in result.stderr + + instrumentation = make_fixture(root / "instrumentation") + report = instrumentation["reports"][0] + assert isinstance(report, Path) + replace_cell(report / "instrumentation-policy.tsv", "wasix_perf_stats", 1, "1") + result = run(command(instrumentation, "instrumentation"), 2) + assert "noncanonical instrumentation policy" in result.stderr + + print("libpq latency comparator tests passed") + + +if __name__ == "__main__": + main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-postgres-settings.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-postgres-settings.py new file mode 100755 index 00000000..874504d2 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-postgres-settings.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Compare the effective PostgreSQL profile of a native/WASIX pair.""" + +from __future__ import annotations + +import csv +import os +from pathlib import Path +import re +import sys +import tempfile + + +REQUIRED = ( + "autovacuum_worker_slots", + "backend_flush_after", + "bgwriter_flush_after", + "checkpoint_flush_after", + "checkpoint_timeout", + "fsync", + "full_page_writes", + "io_method", + "max_connections", + "max_wal_senders", + "max_worker_processes", + "max_wal_size", + "min_wal_size", + "shared_buffers", + "synchronous_commit", + "wal_segment_size", +) +HEADER = ("name", "setting", "unit", "source") +SETTING_NAME = re.compile(r"^[a-z][a-z0-9_]*$") + + +class EvidenceError(RuntimeError): + pass + + +def load(path: Path) -> dict[str, tuple[str, str, str]]: + if not path.is_file() or path.is_symlink(): + raise EvidenceError(f"settings input is not a regular non-symlink file: {path}") + rows: dict[str, tuple[str, str, str]] = {} + with path.open("r", encoding="utf-8", newline="") as handle: + reader = csv.DictReader(handle, delimiter="\t") + if tuple(reader.fieldnames or ()) != HEADER: + raise EvidenceError(f"settings header mismatch: {path}") + for line_number, row in enumerate(reader, 2): + if None in row or any(value is None for value in row.values()): + raise EvidenceError(f"malformed settings row {line_number}: {path}") + name = row["name"] + if not SETTING_NAME.fullmatch(name) or name in rows: + raise EvidenceError(f"invalid or duplicate setting at row {line_number}: {path}") + rows[name] = (row["setting"], row["unit"], row["source"]) + if not set(REQUIRED).issubset(rows): + raise EvidenceError( + f"required settings missing: {path}: " + f"missing={sorted(set(REQUIRED) - set(rows))!r} " + ) + return rows + + +def main(argv: list[str]) -> int: + if len(argv) != 4: + print(f"usage: {argv[0]} NATIVE.tsv WASIX.tsv OUTPUT.tsv", file=sys.stderr) + return 64 + native_path, wasix_path, output_path = map(Path, argv[1:]) + if output_path.exists() or output_path.is_symlink(): + raise EvidenceError(f"refusing to replace settings comparison: {output_path}") + if not output_path.parent.is_dir(): + raise EvidenceError(f"settings comparison parent does not exist: {output_path.parent}") + native = load(native_path) + wasix = load(wasix_path) + if set(native) != set(wasix): + raise EvidenceError( + "native/WASIX settings key set mismatch: " + f"native_only={sorted(set(native) - set(wasix))!r} " + f"wasix_only={sorted(set(wasix) - set(native))!r}" + ) + ordered_names = (*REQUIRED, *sorted(set(native) - set(REQUIRED))) + mismatch = False + descriptor, pending_name = tempfile.mkstemp(prefix=".postgres-settings.", dir=output_path.parent) + try: + with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as handle: + writer = csv.writer(handle, delimiter="\t", lineterminator="\n") + writer.writerow(( + "name", "native_setting", "native_unit", "native_source", + "wasix_setting", "wasix_unit", "wasix_source", "status", + )) + for name in ordered_names: + status = "matched" if native[name] == wasix[name] else "mismatched" + mismatch |= status != "matched" + writer.writerow((name, *native[name], *wasix[name], status)) + os.replace(pending_name, output_path) + except BaseException: + try: + os.unlink(pending_name) + except FileNotFoundError: + pass + raise + return 1 if mismatch else 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main(sys.argv)) + except EvidenceError as error: + print(f"PostgreSQL settings comparison: {error}", file=sys.stderr) + raise SystemExit(1) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-postgres-settings.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-postgres-settings.test.py new file mode 100755 index 00000000..4e147983 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/compare-postgres-settings.test.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +from pathlib import Path +import subprocess +import tempfile +import unittest + + +SCRIPT = Path(__file__).with_name("compare-postgres-settings.py") +NAMES = ( + "autovacuum_worker_slots", "backend_flush_after", "bgwriter_flush_after", + "checkpoint_flush_after", "checkpoint_timeout", "fsync", + "full_page_writes", "io_method", "max_connections", "max_wal_senders", + "max_worker_processes", "max_wal_size", "min_wal_size", + "shared_buffers", "synchronous_commit", "wal_segment_size", +) + + +class SettingsComparisonTests(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory() + self.root = Path(self.temp.name) + + def tearDown(self) -> None: + self.temp.cleanup() + + def write( + self, + name: str, + overrides: dict[str, tuple[str, str, str]] | None = None, + extras: dict[str, tuple[str, str, str]] | None = None, + ) -> Path: + path = self.root / name + overrides = overrides or {} + extras = extras or {} + with path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.writer(handle, delimiter="\t", lineterminator="\n") + writer.writerow(("name", "setting", "unit", "source")) + for setting in NAMES: + writer.writerow((setting, *overrides.get(setting, ("1", "", "command line")))) + for setting in reversed(tuple(extras)): + writer.writerow((setting, *extras[setting])) + return path + + def run_compare(self, native: Path, wasix: Path, output: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [str(SCRIPT), str(native), str(wasix), str(self.root / output)], + check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ) + + def test_exact_profile_passes(self) -> None: + result = self.run_compare(self.write("native.tsv"), self.write("wasix.tsv"), "match.tsv") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue((self.root / "match.tsv").is_file()) + + def test_value_or_source_mismatch_fails_with_evidence(self) -> None: + native = self.write("native.tsv") + wasix = self.write("wasix.tsv", {"io_method": ("worker", "", "default")}) + result = self.run_compare(native, wasix, "mismatch.tsv") + self.assertEqual(result.returncode, 1) + self.assertIn("mismatched", (self.root / "mismatch.tsv").read_text(encoding="utf-8")) + + def test_identical_explicit_settings_are_compared_after_baseline(self) -> None: + extras = { + "work_mem": ("4096", "kB", "command line"), + "random_page_cost": ("1.1", "", "command line"), + } + result = self.run_compare( + self.write("native.tsv", extras=extras), + self.write("wasix.tsv", extras=extras), + "extras.tsv", + ) + self.assertEqual(result.returncode, 0, result.stderr) + with (self.root / "extras.tsv").open(encoding="utf-8", newline="") as handle: + names = [row["name"] for row in csv.DictReader(handle, delimiter="\t")] + self.assertEqual(names, [*NAMES, "random_page_cost", "work_mem"]) + + def test_explicit_setting_mismatch_fails_with_evidence(self) -> None: + native = self.write( + "native.tsv", extras={"work_mem": ("4096", "kB", "command line")} + ) + wasix = self.write( + "wasix.tsv", extras={"work_mem": ("8192", "kB", "command line")} + ) + result = self.run_compare(native, wasix, "work-mem-mismatch.tsv") + self.assertEqual(result.returncode, 1) + self.assertIn( + "work_mem\t4096\tkB\tcommand line\t8192\tkB\tcommand line\tmismatched", + (self.root / "work-mem-mismatch.tsv").read_text(encoding="utf-8"), + ) + + def test_explicit_key_set_mismatch_fails_closed(self) -> None: + native = self.write( + "native.tsv", extras={"work_mem": ("4096", "kB", "command line")} + ) + wasix = self.write("wasix.tsv") + result = self.run_compare(native, wasix, "key-set-mismatch.tsv") + self.assertEqual(result.returncode, 1) + self.assertIn("native/WASIX settings key set mismatch", result.stderr) + self.assertFalse((self.root / "key-set-mismatch.tsv").exists()) + + def test_missing_required_key_fails_closed(self) -> None: + native = self.write("native.tsv") + wasix = self.write("wasix.tsv") + lines = wasix.read_text(encoding="utf-8").splitlines() + wasix.write_text("\n".join(line for line in lines if not line.startswith("io_method\t")) + "\n", encoding="utf-8") + result = self.run_compare(native, wasix, "missing.tsv") + self.assertEqual(result.returncode, 1) + self.assertFalse((self.root / "missing.tsv").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/compose-wasix-embedded-performance-viability.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/compose-wasix-embedded-performance-viability.py new file mode 100755 index 00000000..0b272bfd --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/compose-wasix-embedded-performance-viability.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +"""Fail-closed gate for the not-yet-composable embedded performance claim. + +The four component lanes are useful independently, but they cannot yet prove a +single embedded-environment viability claim. In particular, they do not share +one immutable Linux host/boot identity, and the memory lane does not yet bind +its complete workload shape and sampling policy. This CLI validates that the +checked-in policy still declares those blockers, then emits an immutable +machine-readable failed receipt. It intentionally has no positive path. +""" + +from __future__ import annotations + +import argparse +import csv +from datetime import datetime, timezone +import hashlib +import io +import os +from pathlib import Path +import re +import stat +import sys + + +SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +CLASSIFICATION = "embedded-performance-viability-candidate-non-release" +FAILED_CLASSIFICATION = "failed-embedded-performance-viability-composition-non-release" +BLOCKER_DETAIL = ( + "positive-composition-disabled:same-host-session," + "memory-workload-shape,memory-sampling-policy-unresolved," + "lifecycle-qualified-baseline-policy-unresolved" +) +POLICY_HEADER = ( + "schema_version", "policy_id", "policy_status", "classification", + "postgres_major", "runtime_footprint", "durability_profile", + "throughput_classification", "latency_classification", "lifecycle_status", + "lifecycle_policy_status", "lifecycle_claim_scope", "lifecycle_policy_id", + "lifecycle_policy_sha256", "memory_budget_required", "memory_budget_profile_id", + "memory_budget_profile_sha256", "host_session_binding_status", + "memory_workload_shape_status", "memory_sampling_policy_status", +) +MEMORY_PROFILE_HEADER = ( + "schema_version", "profile_id", "status", "connections", "workloads", + "runtime_footprint", "durability", "memory_max", "memory_high", "swap_max", + "max_peak_pss_kib", "max_peak_pss_anon_kib", "max_peak_page_table_kib", + "max_cgroup_high_events_delta", "max_psi_some_stall_fraction", + "max_psi_full_stall_fraction", +) +RESULT_HEADER = ( + "schema_version", "status", "classification", "detail", "created_utc", + "policy_sha256", "memory_budget_profile_sha256", "carrier_closure_identity", + "postgres_profile_resolution_identity", "wasmer_bin_sha256", + "postgres_module_sha256", "lifecycle_nonce", "component_manifest", + "component_manifest_sha256", "throughput_result_sha256", + "latency_result_sha256", "lifecycle_plateau_sha256", "memory_evidence_sha256", +) + + +class EvidenceError(ValueError): + pass + + +def require_sha256(value: str, name: str) -> str: + if not SHA256_RE.fullmatch(value): + raise EvidenceError(f"{name} is not a lowercase SHA-256") + return value + + +def read_regular(path: Path, name: str) -> bytes: + before = os.lstat(path) + if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode): + raise EvidenceError(f"{name} is not a regular non-symlink file: {path}") + descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)) + try: + opened = os.fstat(descriptor) + if (opened.st_dev, opened.st_ino) != (before.st_dev, before.st_ino): + raise EvidenceError(f"{name} changed while opening: {path}") + chunks: list[bytes] = [] + while True: + chunk = os.read(descriptor, 1024 * 1024) + if not chunk: + break + chunks.append(chunk) + after = os.fstat(descriptor) + payload = b"".join(chunks) + if ( + (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns) + != (opened.st_dev, opened.st_ino, opened.st_size, opened.st_mtime_ns) + or len(payload) != opened.st_size + ): + raise EvidenceError(f"{name} changed while reading: {path}") + return payload + finally: + os.close(descriptor) + + +def read_one_tsv( + path: Path, name: str, expected_header: tuple[str, ...] +) -> tuple[dict[str, str], bytes]: + raw = read_regular(path, name) + if not raw.endswith(b"\n") or raw.count(b"\n") != 2: + raise EvidenceError(f"{name} must contain exactly two newline-terminated rows") + text = raw.decode("utf-8", errors="strict") + if "\r" in text or "\0" in text: + raise EvidenceError(f"{name} contains a forbidden control character") + rows = list(csv.reader(text.splitlines(), delimiter="\t", strict=True)) + if tuple(rows[0]) != expected_header or len(rows[1]) != len(expected_header): + raise EvidenceError(f"{name} has an unexpected ordered schema") + if any(value == "" for value in rows[1]): + raise EvidenceError(f"{name} contains an empty field") + return dict(zip(expected_header, rows[1], strict=True)), raw + + +def require_distinct_directories(paths: tuple[Path, ...]) -> None: + resolved: list[Path] = [] + for path in paths: + metadata = os.lstat(path) + if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode): + raise EvidenceError(f"component path is not a non-symlink directory: {path}") + resolved.append(path.resolve()) + if len(set(resolved)) != len(resolved): + raise EvidenceError("component evidence directories must be distinct") + + +def carrier_closure(manifest: str, receipt: str, payload: str, headless: str) -> str: + digest = hashlib.sha256() + for value in ( + "oliphaunt.wasix-postmaster.qualification-carrier.v1", + manifest, + receipt, + payload, + headless, + ): + digest.update(value.encode("ascii")) + digest.update(b"\0") + return digest.hexdigest() + + +def validate_blocked_policy(args: argparse.Namespace) -> dict[str, str]: + require_distinct_directories( + ( + args.throughput_dir, + args.latency_dir, + args.lifecycle_report, + args.memory_report, + ) + ) + manifest = require_sha256(args.expected_manifest_sha256, "carrier manifest identity") + receipt = require_sha256(args.expected_receipt_sha256, "carrier receipt identity") + payload = require_sha256(args.expected_payload_sha256, "carrier payload identity") + headless = require_sha256(args.expected_headless_sha256, "carrier headless identity") + closure = require_sha256(args.expected_carrier_identity, "carrier closure identity") + if closure != carrier_closure(manifest, receipt, payload, headless): + raise EvidenceError("carrier closure identity does not bind the declared carrier pieces") + + policy, policy_raw = read_one_tsv(args.policy, "composite policy", POLICY_HEADER) + policy_sha256 = hashlib.sha256(policy_raw).hexdigest() + exact_policy = { + "schema_version": "oliphaunt.wasix-postmaster.performance-composite-policy.v1", + "policy_id": "embedded-performance-viability-candidate-v1", + "policy_status": "awaiting-composite-environment-and-memory-contracts", + "classification": CLASSIFICATION, + "postgres_major": "18", + "runtime_footprint": "embedded-concurrent", + "durability_profile": "safe", + "throughput_classification": "throughput-qualified-non-promotional", + "latency_classification": "latency-qualified-non-promotional", + "lifecycle_status": "passed", + "lifecycle_policy_status": "qualification-bounded", + "lifecycle_claim_scope": "relative-to-stabilized-baseline", + "lifecycle_policy_id": "unresolved-after-exploratory-run", + "lifecycle_policy_sha256": "0" * 64, + "memory_budget_required": "1", + "memory_budget_profile_id": "embedded-c4-lower-pressure-v1", + "host_session_binding_status": "unresolved", + "memory_workload_shape_status": "unresolved", + "memory_sampling_policy_status": "unresolved", + } + for field, expected in exact_policy.items(): + if policy[field] != expected: + raise EvidenceError(f"composite policy {field} does not retain its fail-closed value") + require_sha256(policy["memory_budget_profile_sha256"], "memory profile policy hash") + + memory_profile, memory_raw = read_one_tsv( + args.memory_budget_profile, "memory budget profile", MEMORY_PROFILE_HEADER + ) + memory_sha256 = hashlib.sha256(memory_raw).hexdigest() + if memory_sha256 != policy["memory_budget_profile_sha256"]: + raise EvidenceError("memory budget profile hash differs from the composite policy") + for field, expected in { + "schema_version": "oliphaunt.wasix-postmaster.memory-budget-profile.v1", + "profile_id": policy["memory_budget_profile_id"], + "status": "candidate-unqualified", + "connections": "4", + "runtime_footprint": policy["runtime_footprint"], + "durability": policy["durability_profile"], + }.items(): + if memory_profile[field] != expected: + raise EvidenceError(f"memory budget profile {field} is incompatible") + + return { + "policy_sha256": policy_sha256, + "memory_budget_profile_sha256": memory_sha256, + "carrier_closure_identity": closure, + "wasmer_bin_sha256": headless, + } + + +def result_bytes(row: dict[str, str]) -> bytes: + stream = io.StringIO(newline="") + writer = csv.DictWriter( + stream, fieldnames=RESULT_HEADER, delimiter="\t", lineterminator="\n" + ) + writer.writeheader() + writer.writerow(row) + return stream.getvalue().encode("utf-8") + + +def publish_no_replace(path: Path, payload: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + if path.exists() or path.is_symlink(): + raise EvidenceError(f"composite result already exists: {path}") + pending = path.with_name(f"{path.name}.pending.{os.getpid()}") + descriptor = -1 + try: + descriptor = os.open( + pending, + os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0), + 0o600, + ) + view = memoryview(payload) + while view: + written = os.write(descriptor, view) + if written <= 0: + raise OSError("short write while publishing composite result") + view = view[written:] + os.fsync(descriptor) + os.close(descriptor) + descriptor = -1 + os.chmod(pending, 0o444) + os.link(pending, path, follow_symlinks=False) + directory = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)) + try: + os.fsync(directory) + finally: + os.close(directory) + finally: + if descriptor >= 0: + os.close(descriptor) + try: + pending.unlink() + except FileNotFoundError: + pass + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Emit the fail-closed WASIX embedded performance composition receipt" + ) + parser.add_argument("--policy", required=True, type=Path) + parser.add_argument("--memory-budget-profile", required=True, type=Path) + parser.add_argument("--throughput-dir", required=True, type=Path) + parser.add_argument("--latency-dir", required=True, type=Path) + parser.add_argument("--lifecycle-report", required=True, type=Path) + parser.add_argument("--memory-report", required=True, type=Path) + parser.add_argument("--expected-carrier-identity", required=True) + parser.add_argument("--expected-manifest-sha256", required=True) + parser.add_argument("--expected-receipt-sha256", required=True) + parser.add_argument("--expected-payload-sha256", required=True) + parser.add_argument("--expected-headless-sha256", required=True) + parser.add_argument("--manifest-output", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + + if args.output.resolve() == args.manifest_output.resolve(): + print("composite result and component manifest outputs must be distinct", file=sys.stderr) + return 1 + for path, label in ( + (args.output, "composite result"), + (args.manifest_output, "component manifest"), + ): + if path.exists() or path.is_symlink(): + print(f"{label} already exists: {path}", file=sys.stderr) + return 1 + + evidence = { + "policy_sha256": "", + "memory_budget_profile_sha256": "", + "carrier_closure_identity": "", + "wasmer_bin_sha256": "", + } + detail = BLOCKER_DETAIL + try: + evidence |= validate_blocked_policy(args) + except (EvidenceError, OSError, UnicodeError, csv.Error) as error: + detail = str(error).replace("\t", " ").replace("\r", " ").replace("\n", " ") + + row = {field: "" for field in RESULT_HEADER} + row |= { + "schema_version": "oliphaunt.wasix-postmaster.performance-composite-result.v1", + "status": "failed", + "classification": FAILED_CLASSIFICATION, + "detail": detail, + "created_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"), + **evidence, + } + try: + publish_no_replace(args.output, result_bytes(row)) + except (EvidenceError, OSError) as error: + print(f"could not write performance composite result: {error}", file=sys.stderr) + return 1 + print(f"WASIX performance evidence composition blocked: {detail}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/compose-wasix-embedded-performance-viability.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/compose-wasix-embedded-performance-viability.test.py new file mode 100755 index 00000000..d8a7505a --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/compose-wasix-embedded-performance-viability.test.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import hashlib +from pathlib import Path +import stat +import subprocess +import tempfile +import unittest + + +SCRIPT = Path(__file__).with_name("compose-wasix-embedded-performance-viability.py") +PROJECT = SCRIPT.parents[1] +POLICY = PROJECT / "profiles/qualification/embedded-performance-viability-candidate-v1.tsv" +MEMORY_PROFILE = PROJECT / "profiles/memory-budgets/embedded-c4-lower-pressure-v1.tsv" +FAILED_CLASSIFICATION = "failed-embedded-performance-viability-composition-non-release" +BLOCKER_DETAIL = ( + "positive-composition-disabled:same-host-session," + "memory-workload-shape,memory-sampling-policy-unresolved," + "lifecycle-qualified-baseline-policy-unresolved" +) + + +def closure(parts: tuple[str, str, str, str]) -> str: + digest = hashlib.sha256() + for value in ( + "oliphaunt.wasix-postmaster.qualification-carrier.v1", + *parts, + ): + digest.update(value.encode("ascii")) + digest.update(b"\0") + return digest.hexdigest() + + +class CompositeBlockerTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.components = tuple( + self.root / name for name in ("throughput", "latency", "lifecycle", "memory") + ) + for component in self.components: + component.mkdir() + self.parts = ("1" * 64, "2" * 64, "3" * 64, "4" * 64) + self.carrier = closure(self.parts) + self.sequence = 0 + + def tearDown(self) -> None: + self.temporary.cleanup() + + def invoke( + self, + *, + carrier: str | None = None, + policy: Path = POLICY, + memory_profile: Path = MEMORY_PROFILE, + components: tuple[Path, Path, Path, Path] | None = None, + output: Path | None = None, + manifest: Path | None = None, + ) -> tuple[subprocess.CompletedProcess[str], Path, Path]: + self.sequence += 1 + output = output or self.root / f"result-{self.sequence}.tsv" + manifest = manifest or self.root / f"manifest-{self.sequence}.tsv" + evidence = components or self.components + result = subprocess.run( + [ + str(SCRIPT), + "--policy", str(policy), + "--memory-budget-profile", str(memory_profile), + "--throughput-dir", str(evidence[0]), + "--latency-dir", str(evidence[1]), + "--lifecycle-report", str(evidence[2]), + "--memory-report", str(evidence[3]), + "--expected-carrier-identity", carrier or self.carrier, + "--expected-manifest-sha256", self.parts[0], + "--expected-receipt-sha256", self.parts[1], + "--expected-payload-sha256", self.parts[2], + "--expected-headless-sha256", self.parts[3], + "--manifest-output", str(manifest), + "--output", str(output), + ], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return result, output, manifest + + def read_result(self, output: Path) -> dict[str, str]: + with output.open(encoding="utf-8", newline="") as handle: + rows = list(csv.DictReader(handle, delimiter="\t")) + self.assertEqual(len(rows), 1) + return rows[0] + + def test_production_policy_is_intentionally_blocked(self) -> None: + result, output, manifest = self.invoke() + self.assertEqual(result.returncode, 1) + self.assertFalse(manifest.exists()) + row = self.read_result(output) + self.assertEqual(row["status"], "failed") + self.assertEqual(row["classification"], FAILED_CLASSIFICATION) + self.assertEqual(row["detail"], BLOCKER_DETAIL) + self.assertEqual(row["policy_sha256"], hashlib.sha256(POLICY.read_bytes()).hexdigest()) + self.assertEqual( + row["memory_budget_profile_sha256"], + hashlib.sha256(MEMORY_PROFILE.read_bytes()).hexdigest(), + ) + self.assertEqual(row["carrier_closure_identity"], self.carrier) + self.assertEqual(row["wasmer_bin_sha256"], self.parts[3]) + self.assertEqual(stat.S_IMODE(output.stat().st_mode), 0o444) + + def test_result_and_manifest_are_no_replace(self) -> None: + result, output, manifest = self.invoke() + self.assertEqual(result.returncode, 1) + original = output.read_bytes() + repeated, _, _ = self.invoke(output=output, manifest=manifest) + self.assertEqual(repeated.returncode, 1) + self.assertIn("composite result already exists", repeated.stderr) + self.assertEqual(output.read_bytes(), original) + self.assertFalse(manifest.exists()) + + occupied_manifest = self.root / "occupied-manifest.tsv" + occupied_manifest.write_text("foreign\n", encoding="utf-8") + unused_output = self.root / "must-not-exist.tsv" + occupied, _, _ = self.invoke(output=unused_output, manifest=occupied_manifest) + self.assertEqual(occupied.returncode, 1) + self.assertIn("component manifest already exists", occupied.stderr) + self.assertFalse(unused_output.exists()) + self.assertEqual(occupied_manifest.read_text(encoding="utf-8"), "foreign\n") + + def test_same_output_path_is_rejected_without_writing(self) -> None: + shared = self.root / "shared-output.tsv" + result, _, _ = self.invoke(output=shared, manifest=shared) + self.assertEqual(result.returncode, 1) + self.assertIn("must be distinct", result.stderr) + self.assertFalse(shared.exists()) + + def test_malformed_or_stale_identity_fails_with_receipt(self) -> None: + malformed, output, manifest = self.invoke(carrier="Z" * 64) + self.assertEqual(malformed.returncode, 1) + self.assertFalse(manifest.exists()) + self.assertIn("not a lowercase SHA-256", self.read_result(output)["detail"]) + + foreign_closure, output, manifest = self.invoke(carrier="5" * 64) + self.assertEqual(foreign_closure.returncode, 1) + self.assertFalse(manifest.exists()) + self.assertIn("does not bind the declared carrier pieces", self.read_result(output)["detail"]) + + def test_stale_memory_profile_hash_fails_closed(self) -> None: + stale_profile = self.root / "stale-memory-profile.tsv" + stale_profile.write_bytes(MEMORY_PROFILE.read_bytes() + b"# stale\n") + result, output, manifest = self.invoke(memory_profile=stale_profile) + self.assertEqual(result.returncode, 1) + self.assertFalse(manifest.exists()) + self.assertIn("must contain exactly two", self.read_result(output)["detail"]) + + def test_swapped_or_duplicated_components_cannot_pass(self) -> None: + swapped = ( + self.components[1], self.components[0], self.components[3], self.components[2] + ) + result, output, manifest = self.invoke(components=swapped) + self.assertEqual(result.returncode, 1) + self.assertEqual(self.read_result(output)["detail"], BLOCKER_DETAIL) + self.assertFalse(manifest.exists()) + + duplicate = ( + self.components[0], self.components[0], self.components[2], self.components[3] + ) + result, output, manifest = self.invoke(components=duplicate) + self.assertEqual(result.returncode, 1) + self.assertIn("must be distinct", self.read_result(output)["detail"]) + self.assertFalse(manifest.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/connected-client-gate.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/connected-client-gate.test.sh new file mode 100755 index 00000000..014c359b --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/connected-client-gate.test.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +bench="$root/bin/bench-wasix-concurrent-query-suite.sh" +tmp="$(mktemp -d)" +worker_pid="" +cleanup() { + if [ -n "$worker_pid" ] && kill -0 "$worker_pid" 2>/dev/null; then + kill "$worker_pid" 2>/dev/null || true + wait "$worker_pid" 2>/dev/null || true + fi + rm -rf -- "$tmp" +} +trap cleanup EXIT + +sed -n '/^write_connected_client_script()/,/^run_client_process()/p' "$bench" | + sed '$d' >"$tmp/write-connected-client-script.sh" +# shellcheck source=/dev/null +source "$tmp/write-connected-client-script.sh" + +gate_root="$tmp/gates with a quote'" +mkdir -p "$gate_root" +source_sql="$gate_root/source.sql" +connected_sql="$gate_root/connected.sql" +ready_file="$gate_root/client.ready" +start_gate="$gate_root/fanout.start" +end_file="$gate_root/client.end_ms" +drain_gate="$gate_root/fanout.drain" +finished_file="$gate_root/client.finished" + +printf '%s\n' '\set ON_ERROR_STOP 1' 'SELECT 42;' >"$source_sql" +write_connected_client_script "$source_sql" "$connected_sql" \ + "$ready_file" "$start_gate" "$end_file" "$drain_gate" + +[ "$(stat -c '%a' "$connected_sql")" = "600" ] +grep -Fxq '\set ON_ERROR_STOP 1' "$connected_sql" +grep -Fxq 'SELECT 42;' "$connected_sql" +mapfile -t gate_commands < <(sed -n 's/^\\! //p' "$connected_sql") +[ "${#gate_commands[@]}" -eq 4 ] + +( + sh -c "${gate_commands[0]}" + sh -c "${gate_commands[1]}" + sh -c "${gate_commands[2]}" + sh -c "${gate_commands[3]}" + : >"$finished_file" +) & +worker_pid="$!" + +for _ in $(seq 1 500); do + [ -f "$ready_file" ] && break + sleep 0.01 +done +[ -f "$ready_file" ] +kill -0 "$worker_pid" +[ ! -e "$end_file" ] + +: >"$start_gate" +for _ in $(seq 1 500); do + [ -s "$end_file" ] && break + sleep 0.01 +done +[ -s "$end_file" ] +case "$(tr -d '[:space:]' <"$end_file")" in + '' | *[!0-9]*) + echo 'completion marker was not a monotonic millisecond timestamp' >&2 + exit 1 + ;; +esac +kill -0 "$worker_pid" +[ ! -e "$finished_file" ] + +: >"$drain_gate" +wait "$worker_pid" +worker_pid="" +[ -f "$finished_file" ] + +if write_connected_client_script "$source_sql" "$connected_sql" \ + "$ready_file" "$start_gate" "$end_file" "$drain_gate" 2>/dev/null; then + echo 'connected-client generation overwrote an existing script' >&2 + exit 1 +fi + +symlink_source="$gate_root/source-link.sql" +ln -s "$source_sql" "$symlink_source" +if write_connected_client_script "$symlink_source" "$gate_root/from-link.sql" \ + "$gate_root/link.ready" "$gate_root/link.start" "$gate_root/link.end" \ + "$gate_root/link.drain" 2>/dev/null; then + echo 'connected-client generation accepted a symlink SQL source' >&2 + exit 1 +fi + +echo 'connected client gate tests passed' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/current-evidence-manifest.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/current-evidence-manifest.py new file mode 100755 index 00000000..4c4e62e5 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/current-evidence-manifest.py @@ -0,0 +1,589 @@ +#!/usr/bin/env python3 +"""Materialize or verify an artifact-derived WASIX postmaster identity. + +This manifest deliberately proves only an exact local candidate closure. It has +no qualified or release mode: behavioral qualification must bind its digest in +a separate evidence receipt before documentation can call the candidate +current. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from pathlib import Path, PurePosixPath +import re +import stat +import subprocess +import sys +import tomllib +from typing import Any + + +SCHEMA = "oliphaunt.wasix-postmaster.current-evidence.v1" +CLASSIFICATION = "candidate-unqualified-non-release" +CLAIM_SCOPE = "artifact-identity-only" +SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +DETERMINISTIC_START_ANALYZER_POLICY = ( + "llvm-shared-memory-init-restricted-effects.v1" +) +LINEAR_MEMORY_PROFILE_ID = ( + "oliphaunt.wasix-postmaster.linear-memory." + "wasm32-max256m-u64-static4g-guard2g.v1" +) +RECEIPT_KEYS = ( + "schema", + "build_recipe_sha256", + "wasmer_source_commit", + "wasmer_napi_commit", + "wasmer_test_files_commit", + "wasmer_spec_commit", + "wasmer_patch_sha256", + "wasmer_prepared_signature_sha256", + "wasmer_cargo_lock_sha256", + "wasmer_binary_sha256", + "wasmer_features", + "wasmer_headless_binary_sha256", + "wasmer_headless_features", + "runtime_abi_id", + "artifact_abi_version", + "wasix_libc_source_commit", + "wasix_libc_patch_sha256", + "wasix_libc_prepared_signature_sha256", + "sysroot_carrier_manifest_sha256", + "sysroot_variant", + "sysroot_variant_manifest_sha256", + "host_platform", + "host_abi", + "rustc_host", + "rustc_version", + "llvm_version", +) +POSTMASTER_EXECUTOR_RECEIPT_KEYS = ( + "schema", + "build_recipe_sha256", + "wasmer_build_receipt_sha256", + "wasmer_source_commit", + "wasmer_patch_sha256", + "wasmer_prepared_signature_sha256", + "wasmer_cargo_lock_sha256", + "runtime_abi_id", + "artifact_abi_version", + "executor_package", + "executor_binary", + "executor_features", + "executor_role", + "runtime_policy_id", + "cli_contract", + "executor_binary_sha256", + "start_proof_binary", + "start_proof_features", + "start_proof_policy", + "start_proof_binary_sha256", + "memory_profile_binary", + "memory_profile_features", + "linear_memory_profile_id", + "memory_profile_binary_sha256", + "postmaster_compiler_binary", + "postmaster_compiler_features", + "compiler_cpu_policy", + "compiler_cpu_features", + "postmaster_compiler_binary_sha256", + "host_platform", + "host_abi", + "rustc_host", + "rustc_version", +) + + +class EvidenceError(ValueError): + pass + + +def sha256_bytes(payload: bytes) -> str: + return hashlib.sha256(payload).hexdigest() + + +def require_sha256(value: Any, label: str) -> str: + if not isinstance(value, str) or SHA256_RE.fullmatch(value) is None: + raise EvidenceError(f"{label} is not a lowercase SHA-256") + return value + + +def read_regular(path: Path, label: str, *, limit: int = 32 * 1024 * 1024) -> bytes: + before = os.lstat(path) + if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode): + raise EvidenceError(f"{label} is not a regular non-symlink file: {path}") + if before.st_size > limit: + raise EvidenceError(f"{label} exceeds the {limit}-byte parser limit: {path}") + descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)) + try: + opened = os.fstat(descriptor) + if ( + not stat.S_ISREG(opened.st_mode) + or (opened.st_dev, opened.st_ino) != (before.st_dev, before.st_ino) + ): + raise EvidenceError(f"{label} changed while opening: {path}") + chunks: list[bytes] = [] + remaining = opened.st_size + while remaining: + chunk = os.read(descriptor, min(remaining, 1024 * 1024)) + if not chunk: + raise EvidenceError(f"{label} was truncated while reading: {path}") + chunks.append(chunk) + remaining -= len(chunk) + after = os.fstat(descriptor) + if ( + after.st_size, + after.st_mtime_ns, + after.st_dev, + after.st_ino, + ) != ( + opened.st_size, + opened.st_mtime_ns, + opened.st_dev, + opened.st_ino, + ): + raise EvidenceError(f"{label} changed while reading: {path}") + return b"".join(chunks) + finally: + os.close(descriptor) + + +def reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise EvidenceError(f"duplicate JSON field: {key}") + result[key] = value + return result + + +def parse_json(payload: bytes, label: str) -> dict[str, Any]: + try: + text = payload.decode("utf-8") + except UnicodeDecodeError as error: + raise EvidenceError(f"{label} is not UTF-8: {error}") from error + if "\r" in text or "\0" in text: + raise EvidenceError(f"{label} contains a forbidden control character") + try: + value = json.loads(text, object_pairs_hook=reject_duplicate_keys) + except json.JSONDecodeError as error: + raise EvidenceError(f"invalid {label}: {error}") from error + if not isinstance(value, dict): + raise EvidenceError(f"{label} must be a JSON object") + return value + + +def parse_receipt(payload: bytes) -> dict[str, str]: + try: + text = payload.decode("utf-8") + except UnicodeDecodeError as error: + raise EvidenceError(f"Wasmer receipt is not UTF-8: {error}") from error + if "\r" in text or not text.endswith("\n"): + raise EvidenceError("Wasmer receipt is not canonical newline-terminated text") + lines = text.splitlines() + if len(lines) != len(RECEIPT_KEYS): + raise EvidenceError("Wasmer receipt field count differs") + receipt: dict[str, str] = {} + for expected, line in zip(RECEIPT_KEYS, lines, strict=True): + if line.count("=") != 1: + raise EvidenceError(f"invalid Wasmer receipt field: {line!r}") + key, value = line.split("=", 1) + if key != expected or not value: + raise EvidenceError(f"non-canonical Wasmer receipt field: expected {expected}") + receipt[key] = value + if receipt["schema"] != "oliphaunt.wasix-postmaster.wasmer-build.v2": + raise EvidenceError("unexpected Wasmer build receipt schema") + for key in ( + "build_recipe_sha256", + "wasmer_patch_sha256", + "wasmer_binary_sha256", + "wasmer_headless_binary_sha256", + "runtime_abi_id", + "wasix_libc_patch_sha256", + ): + require_sha256(receipt[key], f"Wasmer receipt {key}") + return receipt + + +def parse_postmaster_executor_receipt(payload: bytes) -> dict[str, str]: + try: + text = payload.decode("utf-8") + except UnicodeDecodeError as error: + raise EvidenceError(f"postmaster executor receipt is not UTF-8: {error}") from error + if "\r" in text or not text.endswith("\n"): + raise EvidenceError("postmaster executor receipt is not canonical text") + lines = text.splitlines() + if len(lines) != len(POSTMASTER_EXECUTOR_RECEIPT_KEYS): + raise EvidenceError("postmaster executor receipt field count differs") + receipt: dict[str, str] = {} + for expected, line in zip(POSTMASTER_EXECUTOR_RECEIPT_KEYS, lines, strict=True): + if line.count("=") != 1: + raise EvidenceError(f"invalid postmaster executor receipt field: {line!r}") + key, value = line.split("=", 1) + if key != expected or not value: + raise EvidenceError( + f"non-canonical postmaster executor receipt field: expected {expected}" + ) + receipt[key] = value + if ( + receipt["schema"] + != "oliphaunt.wasix-postmaster.postmaster-executor-build.v3" + ): + raise EvidenceError("unexpected postmaster executor receipt schema") + for key in ( + "build_recipe_sha256", + "wasmer_build_receipt_sha256", + "wasmer_patch_sha256", + "wasmer_prepared_signature_sha256", + "wasmer_cargo_lock_sha256", + "runtime_abi_id", + "executor_binary_sha256", + "start_proof_binary_sha256", + "memory_profile_binary_sha256", + "postmaster_compiler_binary_sha256", + ): + require_sha256(receipt[key], f"postmaster executor receipt {key}") + if ( + receipt["start_proof_binary"] != "oliphaunt-wasix-start-proof" + or receipt["start_proof_features"] != "start-proof-tool" + or receipt["start_proof_policy"] != DETERMINISTIC_START_ANALYZER_POLICY + ): + raise EvidenceError( + "postmaster executor deterministic-start analyzer identity differs" + ) + if ( + receipt["memory_profile_binary"] != "oliphaunt-wasix-memory-profile" + or receipt["memory_profile_features"] != "memory-profile-tool" + or receipt["linear_memory_profile_id"] != LINEAR_MEMORY_PROFILE_ID + ): + raise EvidenceError( + "postmaster executor linear-memory tool identity differs" + ) + if ( + receipt["postmaster_compiler_binary"] + != "oliphaunt-wasix-postmaster-compiler" + or receipt["postmaster_compiler_features"] != "product-compiler" + or receipt["compiler_cpu_policy"] != "generic-baseline" + or receipt["compiler_cpu_features"] != "none" + ): + raise EvidenceError("postmaster product compiler identity differs") + return receipt + + +def required_table(table: dict[str, Any], key: str) -> dict[str, Any]: + value = table.get(key) + if not isinstance(value, dict): + raise EvidenceError(f"source lock is missing table {key}") + return value + + +def required_string(table: dict[str, Any], key: str, label: str) -> str: + value = table.get(key) + if not isinstance(value, str) or not value: + raise EvidenceError(f"{label} must be a nonempty string") + return value + + +def locked_patch( + project_root: Path, + lock_table: dict[str, Any], + label: str, +) -> tuple[str, int, str]: + relative = required_string(lock_table, "path", f"{label} patch path") + pure = PurePosixPath(relative) + if pure.is_absolute() or any(part in ("", ".", "..") for part in pure.parts): + raise EvidenceError(f"unsafe {label} patch path in source lock: {relative!r}") + path = project_root.joinpath(*pure.parts) + try: + path.resolve().relative_to(project_root.resolve()) + except ValueError as error: + raise EvidenceError(f"{label} patch path escapes the project root") from error + payload = read_regular(path, f"{label} patch") + declared_hash = require_sha256(lock_table.get("sha256"), f"{label} lock hash") + declared_size = lock_table.get("bytes") + if type(declared_size) is not int or declared_size < 1: + raise EvidenceError(f"{label} lock byte count must be a positive integer") + if len(payload) != declared_size or sha256_bytes(payload) != declared_hash: + raise EvidenceError(f"{label} patch bytes differ from sources.lock.toml") + return declared_hash, declared_size, relative + + +def carrier_directory(path: Path) -> Path: + info = os.lstat(path) + if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode): + raise EvidenceError(f"carrier is not a non-symlink directory: {path}") + return path.resolve() + + +def run_carrier_verifier(project_root: Path, carrier: Path) -> None: + verifier = project_root / "bin" / "verify-sealed-headless-carrier.sh" + info = os.lstat(verifier) + if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode): + raise EvidenceError(f"carrier verifier is not a regular file: {verifier}") + environment = os.environ.copy() + for name in ( + "FRESH_ROOT", + "REPO_ROOT", + "WASIX_TOOLCHAIN_ROOT", + "POSTGRES_VERSION", + "FRESH_WASMER_VERSION", + "FRESH_WASMER_WASIX_VERSION", + "FRESH_WASMER_ARTIFACT_ABI_VERSION", + "FRESH_WASMER_SOURCE_COMMIT", + "FRESH_WASMER_NAPI_COMMIT", + "FRESH_WASMER_TEST_FILES_COMMIT", + "FRESH_WASMER_SPEC_COMMIT", + "FRESH_WASIX_LIBC_SOURCE_COMMIT", + ): + environment.pop(name, None) + completed = subprocess.run( + [str(verifier), str(carrier)], + cwd=project_root, + env=environment, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + if completed.returncode != 0: + detail = completed.stderr.strip().splitlines() + suffix = f": {detail[-1]}" if detail else "" + raise EvidenceError(f"sealed carrier verification failed{suffix}") + + +def artifact_snapshot(project_root: Path, carrier: Path) -> dict[str, Any]: + source_lock_path = project_root / "sources.lock.toml" + source_lock_raw = read_regular(source_lock_path, "source lock") + try: + source_lock = tomllib.loads(source_lock_raw.decode("utf-8")) + except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error: + raise EvidenceError(f"invalid source lock: {error}") from error + + current_patches = required_table(source_lock, "current_runtime_patches") + wasmer_patch_hash, wasmer_patch_bytes, wasmer_patch_path = locked_patch( + project_root, + required_table(current_patches, "wasmer"), + "Wasmer", + ) + libc_patch_hash, libc_patch_bytes, libc_patch_path = locked_patch( + project_root, + required_table(current_patches, "wasix_libc"), + "wasix-libc", + ) + + manifest_raw = read_regular(carrier / "manifest.json", "sealed manifest") + receipt_raw = read_regular(carrier / "wasmer-build.receipt", "Wasmer receipt") + postmaster_receipt_path = carrier / "postmaster-executor.receipt" + try: + postmaster_receipt_raw = read_regular( + postmaster_receipt_path, "postmaster executor receipt" + ) + except FileNotFoundError: + postmaster_receipt_raw = None + payload_raw = read_regular(carrier / "payload.files", "payload inventory") + manifest = parse_json(manifest_raw, "sealed manifest") + receipt = parse_receipt(receipt_raw) + postmaster_receipt = ( + None + if postmaster_receipt_raw is None + else parse_postmaster_executor_receipt(postmaster_receipt_raw) + ) + + wasmer = required_table(source_lock, "wasmer") + wasix_libc = required_table(source_lock, "wasix_libc") + postgresql = required_table(source_lock, "postgresql") + exact_pairs = ( + ( + receipt["wasmer_source_commit"], + required_string(wasmer, "commit", "Wasmer commit"), + "Wasmer commit", + ), + ( + receipt["wasix_libc_source_commit"], + required_string(wasix_libc, "commit", "wasix-libc commit"), + "wasix-libc commit", + ), + (receipt["wasmer_patch_sha256"], wasmer_patch_hash, "Wasmer patch"), + (receipt["wasix_libc_patch_sha256"], libc_patch_hash, "wasix-libc patch"), + ( + manifest.get("postgres-version"), + required_string(postgresql, "version", "PostgreSQL version"), + "PostgreSQL version", + ), + (manifest.get("runtime-abi-id"), receipt["runtime_abi_id"], "runtime ABI"), + ( + manifest.get("executor-sha256"), + receipt["wasmer_headless_binary_sha256"] + if postmaster_receipt is None + else postmaster_receipt["executor_binary_sha256"], + "role-selected executor", + ), + (manifest.get("wasmer-patch-sha256"), wasmer_patch_hash, "sealed Wasmer patch"), + ) + for actual, expected, label in exact_pairs: + if actual != expected: + raise EvidenceError(f"{label} differs across the source lock, receipt, or carrier") + + if postmaster_receipt is None: + executor_role = "full-headless" + executor_receipt_hash = sha256_bytes(receipt_raw) + else: + executor_role = "postmaster-product" + executor_receipt_hash = sha256_bytes(postmaster_receipt_raw) + product_pairs = ( + ( + postmaster_receipt["wasmer_build_receipt_sha256"], + sha256_bytes(receipt_raw), + "postmaster parent receipt", + ), + ( + postmaster_receipt["runtime_abi_id"], + receipt["runtime_abi_id"], + "postmaster runtime ABI", + ), + ( + postmaster_receipt["executor_role"], + executor_role, + "postmaster executor role", + ), + ) + for actual, expected, label in product_pairs: + if actual != expected: + raise EvidenceError(f"{label} differs across executor receipts") + + runtime_abi = require_sha256(manifest.get("runtime-abi-id"), "runtime ABI") + executor = require_sha256(manifest.get("executor-sha256"), "headless executor") + producer = require_sha256(manifest.get("producer-recipe-sha256"), "producer recipe") + source_fingerprint = require_sha256(manifest.get("source-fingerprint"), "source fingerprint") + payload_hash = sha256_bytes(payload_raw) + artifact_abi = manifest.get("artifact-abi-version") + if type(artifact_abi) is not int or str(artifact_abi) != receipt["artifact_abi_version"]: + raise EvidenceError("artifact ABI differs between manifest and receipt") + compiler_config = manifest.get("compiler-config") + if not isinstance(compiler_config, str) or not compiler_config: + raise EvidenceError("sealed compiler config must be a nonempty string") + postgres_version = required_string(postgresql, "version", "PostgreSQL version") + + return { + "schema-version": SCHEMA, + "classification": CLASSIFICATION, + "qualification-status": "not-qualified", + "claim-scope": CLAIM_SCOPE, + "source-lock": { + "path": "sources.lock.toml", + "sha256": sha256_bytes(source_lock_raw), + }, + "runtime-sources": { + "wasmer-commit": receipt["wasmer_source_commit"], + "wasmer-patch-path": wasmer_patch_path, + "wasmer-patch-bytes": wasmer_patch_bytes, + "wasmer-patch-sha256": wasmer_patch_hash, + "wasix-libc-commit": receipt["wasix_libc_source_commit"], + "wasix-libc-patch-path": libc_patch_path, + "wasix-libc-patch-bytes": libc_patch_bytes, + "wasix-libc-patch-sha256": libc_patch_hash, + }, + "carrier": { + "observed-directory-name": carrier.name, + "canonical-directory-name": ( + f"wasix-postmaster-{postgres_version}-{runtime_abi[:16]}-{payload_hash}" + ), + "postgres-version": postgres_version, + "runtime-abi-id": runtime_abi, + "artifact-abi-version": artifact_abi, + "compiler-config": compiler_config, + "source-fingerprint": source_fingerprint, + "producer-recipe-sha256": producer, + "executor-sha256": executor, + "executor-role": executor_role, + "executor-receipt-sha256": executor_receipt_hash, + "postmaster-executor-receipt-sha256": ( + None + if postmaster_receipt_raw is None + else sha256_bytes(postmaster_receipt_raw) + ), + "manifest-sha256": sha256_bytes(manifest_raw), + "wasmer-build-receipt-sha256": sha256_bytes(receipt_raw), + "payload-files-sha256": payload_hash, + }, + } + + +def render(project_root: Path, carrier: Path) -> bytes: + run_carrier_verifier(project_root, carrier) + first = artifact_snapshot(project_root, carrier) + run_carrier_verifier(project_root, carrier) + second = artifact_snapshot(project_root, carrier) + if first != second: + raise EvidenceError("carrier or source inputs changed while evidence was materialized") + return (json.dumps(first, indent=2, sort_keys=True) + "\n").encode("utf-8") + + +def publish_no_replace(path: Path, payload: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + pending = path.with_name(f".{path.name}.pending.{os.getpid()}") + descriptor = -1 + try: + descriptor = os.open(pending, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) + view = memoryview(payload) + while view: + written = os.write(descriptor, view) + if written <= 0: + raise EvidenceError("short write while publishing current-evidence manifest") + view = view[written:] + os.fsync(descriptor) + os.close(descriptor) + descriptor = -1 + os.link(pending, path, follow_symlinks=False) + except FileExistsError as error: + raise EvidenceError(f"evidence output already exists: {path}") from error + finally: + if descriptor >= 0: + os.close(descriptor) + try: + pending.unlink() + except FileNotFoundError: + pass + if read_regular(path, "published evidence manifest") != payload: + raise EvidenceError("published evidence manifest differs from generated bytes") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + for command in ("write", "verify"): + subparser = subparsers.add_parser(command) + subparser.add_argument("--carrier", required=True, type=Path) + subparser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + + project_root = Path(__file__).resolve().parents[1] + try: + carrier = carrier_directory(args.carrier) + output = args.output.resolve(strict=False) + if output == carrier or carrier in output.parents: + raise EvidenceError("evidence output must be outside the sealed carrier") + expected = render(project_root, carrier) + if args.command == "write": + publish_no_replace(output, expected) + print(f"wrote unqualified current-evidence manifest: {output}") + else: + actual = read_regular(output, "current-evidence manifest") + if actual != expected: + raise EvidenceError( + "current-evidence manifest does not exactly match the verified candidate" + ) + print(f"verified unqualified current-evidence manifest: {output}") + return 0 + except (EvidenceError, OSError) as error: + print(f"current-evidence manifest failed: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/delta-pg-stat-io.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/delta-pg-stat-io.py new file mode 100755 index 00000000..3844bf7c --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/delta-pg-stat-io.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Validate and subtract two PostgreSQL 18 pg_stat_io CSV snapshots.""" + +from __future__ import annotations + +import csv +from decimal import Decimal, InvalidOperation +import os +from pathlib import Path +import sys +import tempfile + + +KEYS = ("backend_type", "object", "context") +COUNTERS = ( + "reads", + "read_bytes", + "writes", + "write_bytes", + "writebacks", + "extends", + "extend_bytes", + "hits", + "evictions", + "reuses", + "fsyncs", +) +TIMERS = ("read_time", "write_time", "writeback_time", "extend_time", "fsync_time") +METRICS = ( + "reads", + "read_bytes", + "read_time", + "writes", + "write_bytes", + "write_time", + "writebacks", + "writeback_time", + "extends", + "extend_bytes", + "extend_time", + "hits", + "evictions", + "reuses", + "fsyncs", + "fsync_time", +) +HEADER = (*KEYS, *METRICS, "stats_reset") + + +class EvidenceError(RuntimeError): + pass + + +def load(path: Path) -> dict[tuple[str, str, str], dict[str, str]]: + if not path.is_file() or path.is_symlink(): + raise EvidenceError(f"pg_stat_io input is not a regular non-symlink file: {path}") + with path.open("r", encoding="utf-8", newline="") as handle: + reader = csv.DictReader(handle) + if tuple(reader.fieldnames or ()) != HEADER: + raise EvidenceError(f"pg_stat_io header mismatch: {path}") + rows: dict[tuple[str, str, str], dict[str, str]] = {} + for line_number, row in enumerate(reader, 2): + if None in row or any(value is None for value in row.values()): + raise EvidenceError(f"malformed pg_stat_io row {line_number}: {path}") + key = tuple(row[name] for name in KEYS) + if any(not value for value in key) or key in rows: + raise EvidenceError(f"duplicate or empty pg_stat_io key at row {line_number}: {path}") + if not row["stats_reset"]: + raise EvidenceError(f"missing pg_stat_io stats_reset at row {line_number}: {path}") + rows[key] = row + if not rows: + raise EvidenceError(f"pg_stat_io snapshot is empty: {path}") + reset_values = {row["stats_reset"] for row in rows.values()} + if len(reset_values) != 1: + raise EvidenceError(f"pg_stat_io snapshot has inconsistent stats_reset values: {path}") + return rows + + +def number(value: str, *, integer: bool, label: str) -> Decimal: + try: + parsed = Decimal(value) + except InvalidOperation as error: + raise EvidenceError(f"invalid pg_stat_io numeric value for {label}: {value!r}") from error + if not parsed.is_finite() or parsed < 0 or (integer and parsed != parsed.to_integral_value()): + raise EvidenceError(f"invalid pg_stat_io numeric value for {label}: {value!r}") + return parsed + + +def canonical_decimal(value: Decimal, *, integer: bool) -> str: + if integer: + return str(int(value)) + rendered = format(value, "f") + if "." in rendered: + rendered = rendered.rstrip("0").rstrip(".") + return rendered or "0" + + +def subtract( + before: dict[tuple[str, str, str], dict[str, str]], + after: dict[tuple[str, str, str], dict[str, str]], +) -> list[list[str]]: + if set(before) != set(after): + missing = sorted(set(before) - set(after)) + added = sorted(set(after) - set(before)) + raise EvidenceError(f"pg_stat_io key set changed: missing={missing!r} added={added!r}") + output: list[list[str]] = [] + for key in sorted(before): + old = before[key] + new = after[key] + if old["stats_reset"] != new["stats_reset"]: + raise EvidenceError(f"pg_stat_io stats_reset changed for key {key!r}") + deltas: list[str] = [] + for metric in METRICS: + old_value = old[metric] + new_value = new[metric] + if (old_value == "") != (new_value == ""): + raise EvidenceError(f"pg_stat_io NULL applicability changed for {key!r}/{metric}") + if old_value == "": + deltas.append("") + continue + integer = metric in COUNTERS + old_number = number(old_value, integer=integer, label=f"before {key!r}/{metric}") + new_number = number(new_value, integer=integer, label=f"after {key!r}/{metric}") + if new_number < old_number: + raise EvidenceError(f"pg_stat_io counter decreased for {key!r}/{metric}") + deltas.append(canonical_decimal(new_number - old_number, integer=integer)) + output.append([*key, *deltas, old["stats_reset"]]) + return output + + +def main(argv: list[str]) -> int: + if len(argv) != 4: + print(f"usage: {argv[0]} BEFORE.csv AFTER.csv DELTA.tsv", file=sys.stderr) + return 64 + before_path, after_path, output_path = map(Path, argv[1:]) + if output_path.exists() or output_path.is_symlink(): + raise EvidenceError(f"refusing to replace pg_stat_io delta: {output_path}") + if not output_path.parent.is_dir(): + raise EvidenceError(f"pg_stat_io delta parent does not exist: {output_path.parent}") + rows = subtract(load(before_path), load(after_path)) + descriptor, pending_name = tempfile.mkstemp(prefix=".pg-stat-io-delta.", dir=output_path.parent) + try: + with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as handle: + writer = csv.writer(handle, delimiter="\t", lineterminator="\n") + writer.writerow((*KEYS, *(f"{name}_delta" for name in METRICS), "stats_reset")) + writer.writerows(rows) + os.replace(pending_name, output_path) + except BaseException: + try: + os.unlink(pending_name) + except FileNotFoundError: + pass + raise + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main(sys.argv)) + except EvidenceError as error: + print(f"pg_stat_io delta: {error}", file=sys.stderr) + raise SystemExit(1) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/delta-pg-stat-io.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/delta-pg-stat-io.test.py new file mode 100755 index 00000000..963588c2 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/delta-pg-stat-io.test.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +from pathlib import Path +import subprocess +import tempfile +import unittest + + +SCRIPT = Path(__file__).with_name("delta-pg-stat-io.py") +HEADER = [ + "backend_type", "object", "context", "reads", "read_bytes", "read_time", + "writes", "write_bytes", "write_time", "writebacks", "writeback_time", + "extends", "extend_bytes", "extend_time", "hits", "evictions", "reuses", + "fsyncs", "fsync_time", "stats_reset", +] + + +def row(*, reads: str = "1", reset: str = "2026-08-09 00:00:00+00") -> list[str]: + return [ + "client backend", "relation", "normal", reads, "8192", "0.25", "2", + "16384", "0.5", "", "", "3", "24576", "0.75", "4", "0", "", + "", "", reset, + ] + + +class DeltaPgStatIoTests(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory() + self.root = Path(self.temp.name) + + def tearDown(self) -> None: + self.temp.cleanup() + + def write(self, name: str, rows: list[list[str]]) -> Path: + path = self.root / name + with path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.writer(handle, lineterminator="\n") + writer.writerow(HEADER) + writer.writerows(rows) + return path + + def run_delta(self, before: Path, after: Path, output: str = "delta.tsv") -> subprocess.CompletedProcess[str]: + return subprocess.run( + [str(SCRIPT), str(before), str(after), str(self.root / output)], + check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ) + + def test_valid_delta_preserves_nulls(self) -> None: + before = self.write("before.csv", [row()]) + changed = row(reads="5") + changed[5] = "1.00" + after = self.write("after.csv", [changed]) + result = self.run_delta(before, after) + self.assertEqual(result.returncode, 0, result.stderr) + fields = (self.root / "delta.tsv").read_text(encoding="utf-8").splitlines()[1].split("\t") + self.assertEqual(fields[3], "4") + self.assertEqual(fields[5], "0.75") + self.assertEqual(fields[9], "") + + def test_rejects_key_change(self) -> None: + before = self.write("before.csv", [row()]) + changed = row() + changed[2] = "bulkread" + after = self.write("after.csv", [changed]) + self.assertNotEqual(self.run_delta(before, after).returncode, 0) + + def test_rejects_stats_reset_change(self) -> None: + before = self.write("before.csv", [row()]) + after = self.write("after.csv", [row(reset="2026-08-09 01:00:00+00")]) + self.assertNotEqual(self.run_delta(before, after).returncode, 0) + + def test_rejects_decrease_and_null_shape_change(self) -> None: + before = self.write("before.csv", [row(reads="5")]) + after = self.write("after.csv", [row(reads="4")]) + self.assertNotEqual(self.run_delta(before, after).returncode, 0) + changed = row(reads="5") + changed[9] = "1" + after = self.write("after-null.csv", [changed]) + self.assertNotEqual(self.run_delta(before, after, "delta-null.tsv").returncode, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/deploy-immutable-sealed-carrier.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/deploy-immutable-sealed-carrier.sh new file mode 100755 index 00000000..692cf389 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/deploy-immutable-sealed-carrier.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$project_root/lib/common.sh" +source "$project_root/lib/sealed-carrier.sh" +source "$project_root/lib/qualification-identities.sh" + +usage() { + cat <<'USAGE' +Usage: deploy-immutable-sealed-carrier.sh --sealed-carrier DIR --receipt FILE [--remove] + +Deploy the exact sealed carrier's five AOT artifacts and two executable memory +images as Linux ext-family immutable inodes. This command requires effective +UID 0 and CAP_LINUX_IMMUTABLE. The canonical receipt is written outside the +carrier before any inode flag changes and doubles as a crash-recovery journal. + +--remove restores only the exact receipt-bound inodes to their recorded +pre-deployment flags and then removes that exact receipt. It never traverses or +changes another carrier entry. +USAGE +} + +carrier="" +receipt="" +remove=0 +while [ "$#" -gt 0 ]; do + case "$1" in + --sealed-carrier) + shift + [ "$#" -gt 0 ] || { echo '--sealed-carrier requires a directory' >&2; exit 2; } + [ -z "$carrier" ] || { echo '--sealed-carrier may only be specified once' >&2; exit 2; } + carrier="$1" + ;; + --receipt) + shift + [ "$#" -gt 0 ] || { echo '--receipt requires a file' >&2; exit 2; } + [ -z "$receipt" ] || { echo '--receipt may only be specified once' >&2; exit 2; } + receipt="$1" + ;; + --remove) remove=1 ;; + -h|--help) usage; exit 0 ;; + *) printf 'unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +[ -n "$carrier" ] || { echo '--sealed-carrier is required' >&2; exit 2; } +[ -n "$receipt" ] || { echo '--receipt is required' >&2; exit 2; } +[ -d "$carrier" ] && [ ! -L "$carrier" ] || { + printf 'sealed carrier must be a non-symlink directory: %s\n' "$carrier" >&2 + exit 2 +} +carrier="$(cd "$carrier" && pwd -P)" +receipt_parent="$(dirname "$receipt")" +[ -d "$receipt_parent" ] && [ ! -L "$receipt_parent" ] || { + printf 'receipt parent must be a non-symlink directory: %s\n' "$receipt_parent" >&2 + exit 2 +} +receipt="$(cd "$receipt_parent" && pwd -P)/$(basename "$receipt")" + +fresh_capture_qualification_carrier_identity "$carrier" || { + printf 'sealed carrier verification failed: %s\n' "$carrier" >&2 + exit 1 +} + +arguments=( + --carrier "$carrier" + --receipt "$receipt" + --manifest-sha256 "$FRESH_QUALIFICATION_CARRIER_MANIFEST_SHA256" + --wasmer-build-receipt-sha256 "$FRESH_QUALIFICATION_CARRIER_RECEIPT_SHA256" + --payload-inventory-sha256 "$FRESH_QUALIFICATION_CARRIER_PAYLOAD_SHA256" + --headless-sha256 "$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" +) +if [ "$remove" -eq 1 ]; then + python3 "$project_root/lib/immutable-carrier.py" --remove "${arguments[@]}" + fresh_verify_sealed_headless_carrier "$carrier" || { + echo 'carrier verification failed after immutable deployment removal' >&2 + exit 1 + } +else + python3 "$project_root/lib/immutable-carrier.py" --deploy "${arguments[@]}" + # The complete payload is verified again after +i, then the read-only + # deployment verifier proves the receipt and every live immutable inode. + fresh_capture_qualification_carrier_identity "$carrier" || { + echo 'carrier verification failed after immutable deployment' >&2 + exit 1 + } + python3 "$project_root/lib/immutable-carrier.py" --verify "${arguments[@]}" +fi diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/extract-checkpoint-summary.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/extract-checkpoint-summary.py new file mode 100755 index 00000000..c9bb25a9 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/extract-checkpoint-summary.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Extract the three paired-qualification fields from one validated sample.""" + +from __future__ import annotations + +import argparse +import csv +import sys +from pathlib import Path + + +HEADER = [ + "schema_version", "target", "mode", "status", "performance_enforced", + "expected_transactions", "completed_transactions", "completion_fraction", + "achieved_tps", "observed_window_ns", "scheduled_span_ns", "p50_ns", + "p95_ns", "p99_ns", "max_ns", "overlap_samples", "overlap_p99_ns", + "non_overlap_p99_ns", "timed_checkpoints", "requested_checkpoints", + "completed_checkpoints", "logged_periodic_checkpoints", "wal_bytes", "gates", +] + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("summary", type=Path) + args = parser.parse_args() + try: + if not args.summary.is_file() or args.summary.is_symlink(): + raise ValueError("summary must be a regular non-symlink file") + with args.summary.open(newline="", encoding="utf-8") as stream: + reader = csv.DictReader(stream, delimiter="\t") + if reader.fieldnames != HEADER: + raise ValueError(f"unexpected checkpoint summary header: {reader.fieldnames!r}") + rows = list(reader) + if len(rows) != 1: + raise ValueError("checkpoint summary must contain exactly one row") + row = rows[0] + if row["status"] not in ("passed", "failed"): + raise ValueError("checkpoint summary status is malformed") + for field in ("p95_ns", "p99_ns"): + if not row[field].isdigit() or int(row[field]) <= 0: + raise ValueError(f"checkpoint summary {field} is malformed") + if int(row["p99_ns"]) < int(row["p95_ns"]): + raise ValueError("checkpoint summary percentiles are nonmonotonic") + print(row["status"], row["p95_ns"], row["p99_ns"], sep="\t") + return 0 + except (OSError, csv.Error, ValueError) as error: + print(f"checkpoint summary extraction failed: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/extract-checkpoint-summary.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/extract-checkpoint-summary.test.py new file mode 100755 index 00000000..fd002dd7 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/extract-checkpoint-summary.test.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent +EXTRACTOR = ROOT / "extract-checkpoint-summary.py" +HEADER = [ + "schema_version", "target", "mode", "status", "performance_enforced", + "expected_transactions", "completed_transactions", "completion_fraction", + "achieved_tps", "observed_window_ns", "scheduled_span_ns", "p50_ns", + "p95_ns", "p99_ns", "max_ns", "overlap_samples", "overlap_p99_ns", + "non_overlap_p99_ns", "timed_checkpoints", "requested_checkpoints", + "completed_checkpoints", "logged_periodic_checkpoints", "wal_bytes", "gates", +] + + +class CheckpointSummaryExtractorTest(unittest.TestCase): + def run_fixture(self, *, p99: object = 300) -> subprocess.CompletedProcess[str]: + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + summary = Path(temporary.name) / "summary.tsv" + row = [ + "oliphaunt.wasix-postmaster.checkpoint-sample.v1", "wasix", "smoke", + "passed", 0, 2400, 2400, "1.000000000", "59.900000", 40000000000, + 39900000000, 100, 200, p99, 400, 100, 300, 250, 1, 0, 1, 1, + 33554432, "/gates.tsv", + ] + with summary.open("x", newline="", encoding="utf-8") as stream: + writer = csv.writer(stream, delimiter="\t", lineterminator="\n") + writer.writerow(HEADER) + writer.writerow(row) + return subprocess.run( + [str(EXTRACTOR), str(summary)], text=True, capture_output=True, check=False + ) + + def test_extracts_status_and_percentiles(self) -> None: + result = self.run_fixture() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "passed\t200\t300\n") + + def test_rejects_non_numeric_p99(self) -> None: + result = self.run_fixture(p99="}") + self.assertEqual(result.returncode, 2, result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-evidence.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-evidence.py new file mode 100755 index 00000000..6e786eeb --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-evidence.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +from dataclasses import dataclass +import hashlib +import os +from pathlib import Path +import re +import stat +import sys + + +NONCE_RE = re.compile(r"^[0-9a-f]{32}$") +COMMIT_PREFIX = b"wasix-runtime-fence-commit-v1" +U64_MAX = (1 << 64) - 1 + + +class FreezeError(ValueError): + pass + + +@dataclass(frozen=True) +class CommitAck: + raw: bytes + sha256: str + nonce: str + sequence: int + mono_ns: int + phase: str + observer_pid: int + observer_tid: int + request_sequence: int + fence_end_offset: int + + +def parse_positive_u64(value: bytes, field: str) -> int: + if not value.isascii() or not value.isdigit(): + raise FreezeError(f"committed ACK {field} must be an unsigned decimal integer") + parsed = int(value) + if parsed <= 0 or parsed > U64_MAX: + raise FreezeError(f"committed ACK {field} must be a positive u64") + return parsed + + +def read_regular_file(path: Path, *, byte_limit: int | None = None) -> tuple[bytes, int]: + before = os.lstat(path) + if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode): + raise FreezeError(f"lifecycle evidence input is not a regular non-symlink file: {path}") + flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) + fd = os.open(path, flags) + try: + metadata = os.fstat(fd) + if ( + not stat.S_ISREG(metadata.st_mode) + or metadata.st_dev != before.st_dev + or metadata.st_ino != before.st_ino + ): + raise FreezeError(f"lifecycle evidence input changed while opening: {path}") + observed_size = metadata.st_size + wanted = observed_size if byte_limit is None else byte_limit + if wanted < 0 or wanted > observed_size: + raise FreezeError( + f"committed fence offset {wanted} exceeds raw lifecycle size {observed_size}" + ) + chunks: list[bytes] = [] + remaining = wanted + while remaining: + chunk = os.read(fd, min(remaining, 1024 * 1024)) + if not chunk: + raise FreezeError("lifecycle evidence input was truncated during read") + chunks.append(chunk) + remaining -= len(chunk) + return b"".join(chunks), observed_size + finally: + os.close(fd) + + +def parse_commit_ack(path: Path, nonce: str, observer_pid: int) -> CommitAck: + raw, _ = read_regular_file(path) + if not raw.endswith(b"\n") or raw.count(b"\n") != 1: + raise FreezeError("committed ACK must contain exactly one newline-terminated record") + parts = raw[:-1].split(b"\t") + if len(parts) != 9 or parts[0] != COMMIT_PREFIX: + raise FreezeError("malformed committed ACK record") + expected_fields = ( + b"nonce=", + b"seq=", + b"mono_ns=", + b"phase=", + b"observer_pid=", + b"observer_tid=", + b"request_seq=", + b"fence_end_offset=", + ) + values: list[bytes] = [] + for token, prefix in zip(parts[1:], expected_fields, strict=True): + if not token.startswith(prefix) or token == prefix: + raise FreezeError( + f"committed ACK expected ordered field {prefix[:-1].decode('ascii')}" + ) + values.append(token[len(prefix):]) + ack_nonce = values[0].decode("ascii", errors="strict") + phase = values[3].decode("ascii", errors="strict") + sequence = parse_positive_u64(values[1], "seq") + mono_ns = parse_positive_u64(values[2], "mono_ns") + ack_pid = parse_positive_u64(values[4], "observer_pid") + observer_tid = parse_positive_u64(values[5], "observer_tid") + request_sequence = parse_positive_u64(values[6], "request_seq") + fence_end_offset = parse_positive_u64(values[7], "fence_end_offset") + if ack_nonce != nonce: + raise FreezeError("committed ACK nonce does not match the lifecycle nonce") + if phase != "post-quiescence": + raise FreezeError("committed ACK is not for post-quiescence") + if ack_pid != observer_pid: + raise FreezeError("committed ACK observer PID does not match the postmaster") + if request_sequence != 2: + raise FreezeError("committed ACK request sequence is not the final request") + return CommitAck( + raw=raw, + sha256=hashlib.sha256(raw).hexdigest(), + nonce=ack_nonce, + sequence=sequence, + mono_ns=mono_ns, + phase=phase, + observer_pid=ack_pid, + observer_tid=observer_tid, + request_sequence=request_sequence, + fence_end_offset=fence_end_offset, + ) + + +def write_atomic(path: Path, payload: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + pending = path.with_name(f"{path.name}.pending.{os.getpid()}") + try: + with pending.open("xb") as handle: + handle.write(payload) + handle.flush() + os.fsync(handle.fileno()) + pending.replace(path) + finally: + try: + pending.unlink() + except FileNotFoundError: + pass + + +def freeze( + *, + raw_log: Path, + commit_ack: Path, + output: Path, + receipt: Path, + nonce: str, + observer_pid: int, + phase_sequence: int, + phase_mono_ns: int, +) -> None: + resolved = [path.resolve() for path in (raw_log, commit_ack, output, receipt)] + if len(set(resolved)) != len(resolved): + raise FreezeError("raw, committed ACK, frozen output, and receipt paths must differ") + for path in (raw_log, commit_ack, output, receipt): + if "\t" in str(path) or "\n" in str(path): + raise FreezeError("lifecycle evidence paths cannot contain tabs or newlines") + + ack = parse_commit_ack(commit_ack, nonce, observer_pid) + prefix, raw_observed_size = read_regular_file( + raw_log, byte_limit=ack.fence_end_offset + ) + fence = ( + "wasix-runtime-fence-v1" + f"\tnonce={ack.nonce}\tseq={ack.sequence}\tmono_ns={ack.mono_ns}" + f"\tphase={ack.phase}\tobserver_pid={ack.observer_pid}" + f"\tobserver_tid={ack.observer_tid}\trequest_seq={ack.request_sequence}\n" + ).encode("ascii") + if not prefix.endswith(fence): + raise FreezeError( + "committed fence offset does not end at the matching runtime fence record" + ) + if prefix.count(fence) != 1: + raise FreezeError("committed runtime fence record is not unique in the frozen prefix") + + complete = ( + "wasix-runtime-phase-v1" + f"\tnonce={nonce}\tseq={phase_sequence}\tmono_ns={phase_mono_ns}" + f"\tphase=complete\tobserver_pid={observer_pid}\n" + ).encode("ascii") + frozen = prefix + complete + digest = hashlib.sha256(frozen).hexdigest() + write_atomic(output, frozen) + + # The ACK is a mutable rendezvous pathname. Re-open it only after the + # frozen artifact is durable and require byte-for-byte identity with the + # ACK that selected the prefix. This closes the pathname replacement race + # before the receipt binds the ACK hash and offset. + confirmed_ack = parse_commit_ack(commit_ack, nonce, observer_pid) + if confirmed_ack.raw != ack.raw or confirmed_ack.sha256 != ack.sha256: + raise FreezeError("committed ACK changed while lifecycle evidence was frozen") + confirmed_frozen, _ = read_regular_file(output) + if confirmed_frozen != frozen: + raise FreezeError("frozen lifecycle artifact changed before receipt publication") + + receipt_payload = ( + "schema_version\traw_log\traw_observed_size\tcommit_ack\tcommit_ack_sha256" + "\tfence_end_offset\tfrozen_log\tfrozen_size\tsha256\tnonce\tobserver_pid" + "\tfence_sequence\tfence_mono_ns\tcomplete_phase_sequence" + "\tcomplete_phase_mono_ns\n" + "oliphaunt.wasix-postmaster.lifecycle-freeze.v2" + f"\t{raw_log}\t{raw_observed_size}\t{commit_ack}\t{ack.sha256}" + f"\t{ack.fence_end_offset}\t{output}\t{len(frozen)}\t{digest}" + f"\t{nonce}\t{observer_pid}\t{ack.sequence}\t{ack.mono_ns}" + f"\t{phase_sequence}\t{phase_mono_ns}\n" + ).encode("utf-8") + write_atomic(receipt, receipt_payload) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Freeze a WASIX lifecycle log through its committed writer fence" + ) + parser.add_argument("--raw-log", required=True, type=Path) + parser.add_argument("--commit-ack", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--receipt", required=True, type=Path) + parser.add_argument("--nonce", required=True) + parser.add_argument("--observer-pid", required=True, type=int) + parser.add_argument("--complete-phase-sequence", required=True, type=int) + parser.add_argument("--complete-phase-mono-ns", required=True, type=int) + args = parser.parse_args() + if not NONCE_RE.fullmatch(args.nonce): + parser.error("--nonce must be exactly 32 lowercase hexadecimal characters") + for option, value in ( + ("--observer-pid", args.observer_pid), + ("--complete-phase-sequence", args.complete_phase_sequence), + ("--complete-phase-mono-ns", args.complete_phase_mono_ns), + ): + if value <= 0 or value > U64_MAX: + parser.error(f"{option} must be a positive u64") + try: + freeze( + raw_log=args.raw_log, + commit_ack=args.commit_ack, + output=args.output, + receipt=args.receipt, + nonce=args.nonce, + observer_pid=args.observer_pid, + phase_sequence=args.complete_phase_sequence, + phase_mono_ns=args.complete_phase_mono_ns, + ) + except (FreezeError, OSError, UnicodeError) as error: + print(f"could not freeze WASIX lifecycle evidence: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-evidence.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-evidence.test.py new file mode 100755 index 00000000..8c45ae5e --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-evidence.test.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import hashlib +from pathlib import Path +import subprocess +import tempfile +import unittest + + +SCRIPT = Path(__file__).with_name("freeze-wasix-lifecycle-evidence.py") +NONCE = "0123456789abcdef0123456789abcdef" +FENCE = ( + "wasix-runtime-fence-v1" + f"\tnonce={NONCE}\tseq=17\tmono_ns=9000\tphase=post-quiescence" + "\tobserver_pid=42\tobserver_tid=1\trequest_seq=2\n" +).encode("ascii") +COMPLETE = ( + "wasix-runtime-phase-v1" + f"\tnonce={NONCE}\tseq=6\tmono_ns=10000\tphase=complete\tobserver_pid=42\n" +).encode("ascii") + + +def commit_ack(offset: int, *, sequence: int = 17, mono_ns: int = 9000) -> bytes: + return ( + "wasix-runtime-fence-commit-v1" + f"\tnonce={NONCE}\tseq={sequence}\tmono_ns={mono_ns}" + "\tphase=post-quiescence\tobserver_pid=42\tobserver_tid=1" + f"\trequest_seq=2\tfence_end_offset={offset}\n" + ).encode("ascii") + + +class FreezeLifecycleEvidenceTests(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory() + self.root = Path(self.temp.name) + self.raw = self.root / "raw.log" + self.ack = self.root / "commit.ack" + self.frozen = self.root / "frozen.log" + self.receipt = self.root / "freeze.tsv" + + def tearDown(self) -> None: + self.temp.cleanup() + + def run_freezer(self) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [ + str(SCRIPT), + "--raw-log", str(self.raw), + "--commit-ack", str(self.ack), + "--output", str(self.frozen), + "--receipt", str(self.receipt), + "--nonce", NONCE, + "--observer-pid", "42", + "--complete-phase-sequence", "6", + "--complete-phase-mono-ns", "10000", + ], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + def test_freezes_committed_offset_and_excludes_existing_and_future_tail(self) -> None: + prefix = b"runtime-prefix\n" + FENCE + self.raw.write_bytes(prefix + b"garbage-already-after-fence\n") + ack = commit_ack(len(prefix)) + self.ack.write_bytes(ack) + result = self.run_freezer() + self.assertEqual(result.returncode, 0, result.stderr) + expected = prefix + COMPLETE + self.assertEqual(self.frozen.read_bytes(), expected) + frozen_digest = hashlib.sha256(expected).hexdigest() + with self.receipt.open(encoding="utf-8", newline="") as handle: + row = next(csv.DictReader(handle, delimiter="\t")) + self.assertEqual( + row["schema_version"], "oliphaunt.wasix-postmaster.lifecycle-freeze.v2" + ) + self.assertEqual(row["frozen_size"], str(len(expected))) + self.assertEqual(row["sha256"], frozen_digest) + self.assertEqual(row["commit_ack"], str(self.ack)) + self.assertEqual(row["commit_ack_sha256"], hashlib.sha256(ack).hexdigest()) + self.assertEqual(row["fence_end_offset"], str(len(prefix))) + + with self.raw.open("ab") as handle: + handle.write(b"live-writer-tail\n") + self.assertEqual(self.frozen.read_bytes(), expected) + self.assertEqual(hashlib.sha256(self.frozen.read_bytes()).hexdigest(), frozen_digest) + + def test_missing_or_malformed_committed_ack_fails_closed(self) -> None: + self.raw.write_bytes(FENCE) + result = self.run_freezer() + self.assertEqual(result.returncode, 1) + self.assertIn("No such file", result.stderr) + self.ack.write_bytes(b"wasix-runtime-fence-commit-v1\ttruncated\n") + result = self.run_freezer() + self.assertEqual(result.returncode, 1) + self.assertIn("malformed committed ACK", result.stderr) + self.assertFalse(self.frozen.exists()) + + def test_ack_mismatch_or_nonterminal_offset_fails_closed(self) -> None: + self.raw.write_bytes(FENCE + b"tail\n") + for ack, detail in ( + (commit_ack(len(FENCE), sequence=18), "does not end at the matching"), + (commit_ack(len(FENCE) - 1), "does not end at the matching"), + (commit_ack(len(FENCE) + 5), "does not end at the matching"), + ): + with self.subTest(detail=detail, ack=ack): + self.ack.write_bytes(ack) + result = self.run_freezer() + self.assertEqual(result.returncode, 1) + self.assertIn(detail, result.stderr) + self.assertFalse(self.frozen.exists()) + + def test_duplicate_matching_fence_in_committed_prefix_fails_closed(self) -> None: + raw = FENCE + FENCE + self.raw.write_bytes(raw) + self.ack.write_bytes(commit_ack(len(raw))) + result = self.run_freezer() + self.assertEqual(result.returncode, 1) + self.assertIn("not unique", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-policy.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-policy.py new file mode 100755 index 00000000..7d0f2c42 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-policy.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import hashlib +import importlib.util +import os +from pathlib import Path +import re +import stat +import sys +import tempfile + + +def load_validator(): + path = Path(__file__).with_name("validate-wasix-lifecycle-plateau.py") + spec = importlib.util.spec_from_file_location("wasix_lifecycle_validator", path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot load lifecycle validator: {path}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +VALIDATOR = load_validator() +COUNT_FIELDS: tuple[str, ...] = VALIDATOR.COUNT_FIELDS +OUTPUT_FIELDS: tuple[str, ...] = VALIDATOR.OUTPUT_FIELDS +CLAIM_SCOPE: str = VALIDATOR.CLAIM_SCOPE +BASELINE_ASSUMPTION: str = VALIDATOR.BASELINE_ASSUMPTION +POLICY_FIELDS: tuple[str, ...] = VALIDATOR.BASELINE_POLICY_FIELDS +U64_MAX: int = VALIDATOR.U64_MAX + + +class FreezeError(ValueError): + pass + + +def require_sha256(row: dict[str, str], field: str) -> None: + if not re.fullmatch(r"[0-9a-f]{64}", row[field]): + raise FreezeError(f"exploratory result {field} is not a SHA-256") + + +def parse_u64(value: str, field: str) -> int: + if not value.isascii() or not value.isdecimal(): + raise FreezeError(f"exploratory result {field} is not an unsigned integer") + parsed = int(value) + if parsed > U64_MAX: + raise FreezeError(f"exploratory result {field} exceeds u64") + return parsed + + +def read_exploratory_result(path: Path) -> tuple[dict[str, str], str]: + raw = VALIDATOR.read_regular_file(path) + if not raw.endswith(b"\n") or raw.count(b"\n") != 2: + raise FreezeError("exploratory result must contain exactly two newline-terminated rows") + header, record, _ = raw.decode("utf-8", errors="strict").split("\n") + if tuple(header.split("\t")) != OUTPUT_FIELDS: + raise FreezeError("exploratory result has an unexpected ordered schema") + values = record.split("\t") + if len(values) != len(OUTPUT_FIELDS): + raise FreezeError("exploratory result row does not match its schema") + row = dict(zip(OUTPUT_FIELDS, values, strict=True)) + for field, value in row.items(): + if any(ord(character) < 0x20 or ord(character) == 0x7F for character in value): + raise FreezeError(f"exploratory result {field} contains a control character") + + expected = { + "schema_version": "6", + "target": "wasix", + "status": "passed", + "claim_scope": CLAIM_SCOPE, + "baseline_assumption": BASELINE_ASSUMPTION, + "baseline_policy_status": "exploratory-unbounded", + } + for field, wanted in expected.items(): + if row[field] != wanted: + raise FreezeError(f"exploratory result {field} must be {wanted}") + if not re.fullmatch( + r"pg18-idle-postmaster-stabilized-exploratory-v[1-9][0-9]*", + row["baseline_policy_id"], + ): + raise FreezeError("exploratory result does not name an idle-postmaster policy") + if row["wait_kind"] not in VALIDATOR.WAIT_KINDS: + raise FreezeError("exploratory result has an unsupported wait kind") + for field in ( + "baseline_policy_sha256", + "baseline_binding_sha256", + "freeze_receipt_sha256", + "evidence_sha256", + "commit_ack_sha256", + ): + require_sha256(row, field) + + return row, hashlib.sha256(raw).hexdigest() + + +def revalidate_exploratory_result( + row: dict[str, str], + *, + log: Path, + freeze_receipt: Path, + baseline_policy: Path, + baseline_binding: Path, +) -> dict[str, int]: + nonce = row["nonce"] + if not VALIDATOR.NONCE_RE.fullmatch(nonce): + raise FreezeError("exploratory result nonce is malformed") + observer_pid = parse_u64(row["observer_pid"], "observer_pid") + min_samples = parse_u64(row["min_samples"], "min_samples") + min_span_ns = parse_u64(row["min_span_ns"], "min_span_ns") + expected_interval_ms = parse_u64( + row["expected_interval_ms"], "expected_interval_ms" + ) + max_sample_gap_ns = parse_u64(row["max_sample_gap_ns"], "max_sample_gap_ns") + if observer_pid == 0: + raise FreezeError("exploratory result observer_pid must be positive") + if min_samples < 3: + raise FreezeError("exploratory result min_samples must be at least 3") + if min_span_ns < 1_000_000_000: + raise FreezeError("exploratory result min_span_ns must span at least one second") + if min_span_ns % 1_000_000 != 0: + raise FreezeError("exploratory result min_span_ns is not whole milliseconds") + if expected_interval_ms == 0: + raise FreezeError("exploratory result expected_interval_ms must be positive") + if expected_interval_ms > U64_MAX // 3_000_000: + raise FreezeError("exploratory result expected_interval_ms exceeds u64") + if max_sample_gap_ns != expected_interval_ms * 3_000_000: + raise FreezeError( + "exploratory result max_sample_gap_ns does not match its sampling interval" + ) + + outcome = VALIDATOR.validate_lifecycle_bundle( + log=log, + freeze_receipt=freeze_receipt, + baseline_policy_path=baseline_policy, + baseline_binding_path=baseline_binding, + target=row["target"], + nonce=nonce, + observer_pid=observer_pid, + min_samples=min_samples, + min_span_ns=min_span_ns, + expected_interval_ms=expected_interval_ms, + max_sample_gap_ns=max_sample_gap_ns, + ) + if not outcome.passed: + raise FreezeError( + f"exploratory bundle failed revalidation: {outcome.error}" + ) + revalidated = {field: str(outcome.row[field]) for field in OUTPUT_FIELDS} + for field in OUTPUT_FIELDS: + if row[field] != revalidated[field]: + raise FreezeError( + f"exploratory result does not match revalidated bundle field {field}" + ) + + counts: dict[str, int] = {} + for field in COUNT_FIELDS: + readiness_field = f"readiness_{field}" + final_field = f"post_quiescence_{field}" + readiness = parse_u64(revalidated[readiness_field], readiness_field) + final = parse_u64(revalidated[final_field], final_field) + if readiness != final: + raise FreezeError( + f"revalidated exploratory bundle {field} does not return to readiness" + ) + counts[field] = readiness + return counts + + +def render_policy(policy_id: str, counts: dict[str, int]) -> bytes: + lines = ["\t".join(POLICY_FIELDS)] + metadata = ( + "oliphaunt.wasix-postmaster.lifecycle-baseline-policy.v1", + policy_id, + "qualification-bounded", + CLAIM_SCOPE, + BASELINE_ASSUMPTION, + ) + for field in COUNT_FIELDS: + value = str(counts[field]) + lines.append("\t".join((*metadata, field, "exact", value, value))) + payload = ("\n".join(lines) + "\n").encode() + return payload + + +def publish_new_regular(path: Path, payload: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + fd, pending_name = tempfile.mkstemp( + prefix=f".{path.name}.pending.", dir=path.parent + ) + pending = Path(pending_name) + try: + os.fchmod(fd, 0o644) + with os.fdopen(fd, "wb", closefd=False) as handle: + handle.write(payload) + handle.flush() + os.fsync(handle.fileno()) + os.close(fd) + fd = -1 + parsed = VALIDATOR.parse_baseline_policy(pending) + intended_sha256 = hashlib.sha256(payload).hexdigest() + if parsed.sha256 != intended_sha256: + raise FreezeError("pending lifecycle policy differs from rendered payload") + try: + os.link(pending, path, follow_symlinks=False) + except FileExistsError as error: + try: + existing = os.lstat(path) + kind = "symlink" if stat.S_ISLNK(existing.st_mode) else "existing path" + except FileNotFoundError: + kind = "path created concurrently" + raise FreezeError(f"refusing to replace {kind}: {path}") from error + pending.unlink() + directory_fd = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + finally: + if fd >= 0: + os.close(fd) + try: + pending.unlink() + except FileNotFoundError: + pass + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Freeze one passed exploratory lifecycle tuple into an exact qualification policy" + ) + parser.add_argument("--exploratory-result", required=True, type=Path) + parser.add_argument("--log", required=True, type=Path) + parser.add_argument("--freeze-receipt", required=True, type=Path) + parser.add_argument("--baseline-policy", required=True, type=Path) + parser.add_argument("--baseline-binding", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--policy-id", required=True) + args = parser.parse_args() + if not re.fullmatch( + r"pg18-idle-postmaster-stabilized-qualified-v[1-9][0-9]*", args.policy_id + ): + parser.error("--policy-id must be pg18-idle-postmaster-stabilized-qualified-vN") + try: + row, source_sha256 = read_exploratory_result(args.exploratory_result) + counts = revalidate_exploratory_result( + row, + log=args.log, + freeze_receipt=args.freeze_receipt, + baseline_policy=args.baseline_policy, + baseline_binding=args.baseline_binding, + ) + payload = render_policy(args.policy_id, counts) + publish_new_regular(args.output, payload) + except (FreezeError, VALIDATOR.EvidenceError, OSError, UnicodeError) as error: + print(f"freeze lifecycle policy: {error}", file=sys.stderr) + return 1 + print(f"wrote exact lifecycle policy: {args.output}") + print(f"exploratory result sha256: {source_sha256}") + print(f"policy sha256: {hashlib.sha256(payload).hexdigest()}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-policy.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-policy.test.py new file mode 100755 index 00000000..93f6dffe --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/freeze-wasix-lifecycle-policy.test.py @@ -0,0 +1,443 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import hashlib +import importlib.util +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + + +ROOT = Path(__file__).resolve().parent +FREEZER_PATH = ROOT / "freeze-wasix-lifecycle-policy.py" +EVIDENCE_FREEZER = ROOT / "freeze-wasix-lifecycle-evidence.py" +VALIDATOR_PATH = ROOT / "validate-wasix-lifecycle-plateau.py" +BASELINE_POLICY = ( + ROOT.parent + / "profiles/lifecycle-baselines/relative-stabilized-idle-postmaster-candidate-v1.tsv" +) + + +def load_module(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + if spec is None or spec.loader is None: + raise RuntimeError(f"could not load {path}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +VALIDATOR = load_module("wasix_lifecycle_policy_test_validator", VALIDATOR_PATH) +FREEZER = load_module("wasix_lifecycle_policy_test_freezer", FREEZER_PATH) +NONCE = "0123456789abcdef0123456789abcdef" +OBSERVER_PID = 42 +WAIT_KIND = VALIDATOR.WAIT_KINDS[0] +EXPECTED_INTERVAL_MS = 300 +COUNTS = {field: 0 for field in VALIDATOR.COUNT_FIELDS} +COUNTS.update( + registered_processes=6, + active_tasks=6, + process_topology_nodes=6, + process_child_edges=5, + process_thread_entries=6, + process_live_threads=6, + process_execution_leases=11, + runtime_state_active=6, + runtime_state_slots=6, + runtime_state_observer_registered=1, + private_futexes=3, + private_futex_waiters=3, + private_futex_wakers=2, + shared_futexes=4, + shared_futex_waiters=2, + shared_futex_wakers=2, + epoll_states=6, + epoll_subscriptions=6, + epoll_join_guards=6, + epoll_close_registrations=6, + shared_registry_active=2, + shared_registry_slots=2, + shared_mappings=8, + guest_fd_entries=71, +) +COUNT_TUPLE = tuple(COUNTS[field] for field in VALIDATOR.COUNT_FIELDS) +COLD_COUNTS = COUNTS | {"guest_fd_entries": 70} +COLD_COUNT_TUPLE = tuple(COLD_COUNTS[field] for field in VALIDATOR.COUNT_FIELDS) +OBSERVER_SET = (42, 43, 44, 45, 46, 47, 42) + + +def context(sequence: int, pid: int = OBSERVER_PID) -> str: + return ( + "wasix-runtime-context-v1" + f"\tseq={sequence}\twait_kind={WAIT_KIND}" + f"\tobserver_pid={pid}\tobserver_tid=1\n" + ) + + +def state( + sequence: int, mono_ns: int, counts: tuple[int, ...] = COUNT_TUPLE +) -> str: + fields = "".join( + f"\t{field}={value}" + for field, value in zip(VALIDATOR.COUNT_FIELDS, counts, strict=True) + ) + return f"wasix-runtime-state-v1\tseq={sequence}\tmono_ns={mono_ns}{fields}\n" + + +def fence(sequence: int, mono_ns: int, phase: str, request_sequence: int) -> str: + return ( + "wasix-runtime-fence-v1" + f"\tnonce={NONCE}\tseq={sequence}\tmono_ns={mono_ns}\tphase={phase}" + f"\tobserver_pid={OBSERVER_PID}\tobserver_tid=1" + f"\trequest_seq={request_sequence}\n" + ) + + +def phase(sequence: int, mono_ns: int, name: str) -> str: + return ( + "wasix-runtime-phase-v1" + f"\tnonce={NONCE}\tseq={sequence}\tmono_ns={mono_ns}" + f"\tphase={name}\tobserver_pid={OBSERVER_PID}\n" + ) + + +def stabilization() -> str: + return ( + "wasix-runtime-stabilization-v1" + f"\tnonce={NONCE}\tmethod=pg_log_standby_snapshot" + "\tbefore_writes=10\tafter_writes=12" + "\tbefore_write_bytes=81920\tafter_write_bytes=147456" + "\tbefore_stats_reset=1786320000000000" + "\tafter_stats_reset=1786320000000000" + "\ttarget_lsn=0/1000000\tobserved_flush_lsn=0/1008000" + "\twal_writer_delay_ms=200\tstart_mono_ns=25\tend_mono_ns=35" + f"\tstatus=passed\tobserver_pid={OBSERVER_PID}\n" + ) + + +def reconnect_churn() -> str: + return ( + "wasix-runtime-reconnect-churn-v1" + f"\tnonce={NONCE}\trequested=2000\tcompleted=2000" + f"\tcommand_sha256={VALIDATOR.RECONNECT_COMMAND_SHA256}" + f"\tclient_sha256={'1' * 64}\tconnection_sha256={'2' * 64}" + "\tstart_mono_ns=61\tend_mono_ns=69\tstatus=passed" + f"\tobserver_pid={OBSERVER_PID}\n" + ) + + +def raw_lifecycle_log() -> bytes: + lines = [phase(1, 10, "cold-readiness")] + for sequence, mono_ns in enumerate( + (100_000_000, 500_000_000, 900_000_000, 1_300_000_000), 1 + ): + lines.extend((context(sequence), state(sequence, mono_ns, COLD_COUNT_TUPLE))) + lines.extend( + ( + phase(2, 20, "maintenance-stabilization"), + stabilization(), + phase(3, 50, "readiness"), + ) + ) + for sequence, (mono_ns, pid) in enumerate( + zip( + ( + 1_500_000_000, + 1_700_000_000, + 1_900_000_000, + 2_100_000_000, + 2_300_000_000, + 2_500_000_000, + 2_700_000_000, + ), + OBSERVER_SET, + strict=True, + ), + 5, + ): + lines.extend((context(sequence, pid), state(sequence, mono_ns))) + lines.extend( + ( + fence(11, 2_700_000_000, "readiness", 1), + phase(4, 60, "reconnect-churn"), + context(12, 99), + state(12, 2_800_000_000), + reconnect_churn(), + phase(5, 70, "post-quiescence"), + ) + ) + for sequence, (mono_ns, pid) in enumerate( + zip( + ( + 3_000_000_000, + 3_200_000_000, + 3_400_000_000, + 3_600_000_000, + 3_800_000_000, + 4_000_000_000, + 4_200_000_000, + ), + OBSERVER_SET, + strict=True, + ), + 13, + ): + lines.extend((context(sequence, pid), state(sequence, mono_ns))) + lines.append(fence(19, 4_200_000_000, "post-quiescence", 2)) + return "".join(lines).encode("ascii") + + +def write_tsv(path: Path, row: dict[str, str]) -> None: + with path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter( + handle, + delimiter="\t", + fieldnames=VALIDATOR.OUTPUT_FIELDS, + lineterminator="\n", + ) + writer.writeheader() + writer.writerow(row) + + +class FreezeLifecyclePolicyTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.raw_log = self.root / "runtime.raw.log" + self.ack = self.root / "runtime-fence.ack" + self.log = self.root / "runtime.log" + self.receipt = self.root / "runtime.freeze.tsv" + self.baseline_policy = self.root / "baseline-policy.tsv" + self.baseline_binding = self.root / "baseline-binding.tsv" + self.result = self.root / "plateau.tsv" + self.output = self.root / "qualified.tsv" + + def tearDown(self) -> None: + self.temporary.cleanup() + + def prepare_real_bundle(self) -> dict[str, str]: + raw = raw_lifecycle_log() + self.raw_log.write_bytes(raw) + ack = ( + "wasix-runtime-fence-commit-v1" + f"\tnonce={NONCE}\tseq=19\tmono_ns=4200000000" + f"\tphase=post-quiescence\tobserver_pid={OBSERVER_PID}" + "\tobserver_tid=1\trequest_seq=2" + f"\tfence_end_offset={len(raw)}\n" + ).encode("ascii") + self.ack.write_bytes(ack) + frozen = subprocess.run( + [ + sys.executable, + str(EVIDENCE_FREEZER), + "--raw-log", + str(self.raw_log), + "--commit-ack", + str(self.ack), + "--output", + str(self.log), + "--receipt", + str(self.receipt), + "--nonce", + NONCE, + "--observer-pid", + str(OBSERVER_PID), + "--complete-phase-sequence", + "6", + "--complete-phase-mono-ns", + "80", + ], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + self.assertEqual(frozen.returncode, 0, frozen.stderr) + + policy_payload = BASELINE_POLICY.read_bytes() + self.baseline_policy.write_bytes(policy_payload) + with self.baseline_policy.open(encoding="utf-8", newline="") as handle: + policy = next(csv.DictReader(handle, delimiter="\t")) + self.baseline_binding.write_text( + "schema_version\tpolicy_id\tpolicy_sha256\tpolicy_status\tclaim_scope" + "\tbaseline_assumption\tpostgres_major\truntime_footprint" + "\truntime_footprint_sha256\tdurability_profile" + "\tdurability_profile_sha256\tpostgres_profile_resolution_identity" + "\truntime_mode\twasmer_bin_sha256\tpostgres_module_sha256" + "\tcarrier_manifest_sha256\tcarrier_receipt_sha256" + "\tcarrier_payload_inventory_sha256\n" + "oliphaunt.wasix-postmaster.lifecycle-baseline-binding.v1" + f"\t{policy['policy_id']}\t{hashlib.sha256(policy_payload).hexdigest()}" + f"\t{policy['policy_status']}\t{policy['claim_scope']}" + f"\t{policy['baseline_assumption']}" + "\t18\tnone\tnone\tnone\tnone\tnone\tcompiler" + f"\t{'1' * 64}\t{'2' * 64}\tnone\tnone\tnone\n", + encoding="utf-8", + ) + validated = subprocess.run( + [ + sys.executable, + str(VALIDATOR_PATH), + "--log", + str(self.log), + "--freeze-receipt", + str(self.receipt), + "--baseline-policy", + str(self.baseline_policy), + "--baseline-binding", + str(self.baseline_binding), + "--output", + str(self.result), + "--target", + "wasix", + "--nonce", + NONCE, + "--observer-pid", + str(OBSERVER_PID), + "--min-samples", + "3", + "--min-span-ms", + "1000", + "--expected-interval-ms", + str(EXPECTED_INTERVAL_MS), + ], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + self.assertEqual(validated.returncode, 0, validated.stderr) + with self.result.open(encoding="utf-8", newline="") as handle: + return next(csv.DictReader(handle, delimiter="\t")) + + def run_freezer( + self, *, policy_id: str = "pg18-idle-postmaster-stabilized-qualified-v1" + ) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [ + sys.executable, + str(FREEZER_PATH), + "--exploratory-result", + str(self.result), + "--log", + str(self.log), + "--freeze-receipt", + str(self.receipt), + "--baseline-policy", + str(self.baseline_policy), + "--baseline-binding", + str(self.baseline_binding), + "--output", + str(self.output), + "--policy-id", + policy_id, + ], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + def test_real_passed_bundle_becomes_exact_policy(self) -> None: + self.prepare_real_bundle() + result = self.run_freezer() + self.assertEqual(result.returncode, 0, result.stderr) + policy = VALIDATOR.parse_baseline_policy(self.output) + self.assertEqual(policy.policy_status, "qualification-bounded") + self.assertEqual(policy.policy_id, "pg18-idle-postmaster-stabilized-qualified-v1") + observed = {constraint.field: constraint.minimum for constraint in policy.constraints} + self.assertEqual(observed, COUNTS) + self.assertTrue(all(constraint.rule == "exact" for constraint in policy.constraints)) + + def test_result_must_equal_revalidated_bundle(self) -> None: + row = self.prepare_real_bundle() + row["readiness_guest_fd_entries"] = "72" + row["post_quiescence_guest_fd_entries"] = "72" + write_tsv(self.result, row) + result = self.run_freezer() + self.assertEqual(result.returncode, 1) + self.assertIn( + "does not match revalidated bundle field readiness_guest_fd_entries", + result.stderr, + ) + self.assertFalse(self.output.exists()) + + def test_mutated_bundle_is_rejected_even_when_result_claims_passed(self) -> None: + self.prepare_real_bundle() + original = self.log.read_bytes() + mutated = original.replace(b"guest_fd_entries=70", b"guest_fd_entries=71", 1) + self.assertEqual(len(mutated), len(original)) + self.assertNotEqual(mutated, original) + self.log.write_bytes(mutated) + result = self.run_freezer() + self.assertEqual(result.returncode, 1) + self.assertIn("exploratory bundle failed revalidation", result.stderr) + self.assertFalse(self.output.exists()) + + def test_failed_or_already_qualified_source_is_rejected(self) -> None: + row = self.prepare_real_bundle() + row["status"] = "failed" + write_tsv(self.result, row) + result = self.run_freezer() + self.assertEqual(result.returncode, 1) + self.assertIn("status must be passed", result.stderr) + + row["status"] = "passed" + row["baseline_policy_status"] = "qualification-bounded" + write_tsv(self.result, row) + result = self.run_freezer() + self.assertEqual(result.returncode, 1) + self.assertIn("must be exploratory-unbounded", result.stderr) + + def test_existing_output_and_symlink_are_never_replaced(self) -> None: + self.prepare_real_bundle() + self.output.write_text("owned\n", encoding="utf-8") + result = self.run_freezer() + self.assertEqual(result.returncode, 1) + self.assertEqual(self.output.read_text(encoding="utf-8"), "owned\n") + + self.output.unlink() + target = self.root / "target" + target.write_text("owned\n", encoding="utf-8") + self.output.symlink_to(target) + result = self.run_freezer() + self.assertEqual(result.returncode, 1) + self.assertIn("refusing to replace symlink", result.stderr) + self.assertEqual(target.read_text(encoding="utf-8"), "owned\n") + + def test_link_race_preserves_competing_output(self) -> None: + payload = FREEZER.render_policy( + "pg18-idle-postmaster-stabilized-qualified-v1", COUNTS + ) + real_link = os.link + + def create_competitor_then_link(source, destination, **kwargs): + Path(destination).write_text("competitor\n", encoding="utf-8") + return real_link(source, destination, **kwargs) + + with mock.patch.object( + FREEZER.os, "link", side_effect=create_competitor_then_link + ): + with self.assertRaises(FREEZER.FreezeError): + FREEZER.publish_new_regular(self.output, payload) + self.assertEqual(self.output.read_text(encoding="utf-8"), "competitor\n") + self.assertEqual(list(self.root.glob(".qualified.tsv.pending.*")), []) + + def test_policy_id_must_use_distinct_qualified_namespace(self) -> None: + self.prepare_real_bundle() + result = self.run_freezer( + policy_id="pg18-idle-postmaster-stabilized-exploratory-v2" + ) + self.assertEqual(result.returncode, 2) + self.assertIn("must be pg18-idle-postmaster-stabilized-qualified-vN", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/lifecycle-memory-checkpoint.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/lifecycle-memory-checkpoint.test.sh new file mode 100755 index 00000000..249fa4f8 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/lifecycle-memory-checkpoint.test.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +bench="$root/bin/bench-wasix-concurrent-query-suite.sh" +validator="$root/bin/validate-wasix-lifecycle-memory-plateau.py" +tmp="$(mktemp -d "${TMPDIR:-/tmp}/oliphaunt-lifecycle-memory.XXXXXX")" +trap 'rm -rf -- "$tmp"' EXIT + +expect_usage_failure() { + local label="$1" + shift + set +e + "$bench" "$@" >"$tmp/$label.log" 2>&1 + local status=$? + set -e + if [ "$status" -ne 2 ]; then + printf '%s: expected usage exit 2, got %s\n' "$label" "$status" >&2 + sed -n '1,100p' "$tmp/$label.log" >&2 + exit 1 + fi +} + +help_output="$("$bench" --help)" +grep -Fq -- '--wasix-lifecycle-memory-checkpoint-every N' <<<"$help_output" +grep -Fq -- '--max-lifecycle-pss-anon-growth-kib N' <<<"$help_output" +grep -Fq -- '--max-lifecycle-late-pss-slope-kib-per-1000 N' <<<"$help_output" + +common_memory_args=( + --wasix-lifecycle-memory-checkpoint-every 16 + --max-lifecycle-pss-growth-kib 1024 + --max-lifecycle-pss-anon-growth-kib 1024 + --max-lifecycle-heap-growth-kib 1024 + --max-lifecycle-late-pss-slope-kib-per-1000 1024 + --max-lifecycle-late-pss-anon-slope-kib-per-1000 1024 + --max-lifecycle-late-heap-slope-kib-per-1000 1024 +) +expect_usage_failure without-lifecycle "${common_memory_args[@]}" +expect_usage_failure incomplete-budgets \ + --wasix-lifecycle-plateau \ + --wasix-lifecycle-memory-checkpoint-every 16 \ + --max-lifecycle-pss-growth-kib 1024 +expect_usage_failure interval-not-smaller \ + --wasix-lifecycle-plateau \ + --wasix-lifecycle-reconnects 16 \ + "${common_memory_args[@]}" +expect_usage_failure budgets-without-checkpoints \ + --max-lifecycle-pss-growth-kib 1024 \ + --max-lifecycle-pss-anon-growth-kib 1024 \ + --max-lifecycle-heap-growth-kib 1024 \ + --max-lifecycle-late-pss-slope-kib-per-1000 1024 \ + --max-lifecycle-late-pss-anon-slope-kib-per-1000 1024 \ + --max-lifecycle-late-heap-slope-kib-per-1000 1024 + +sed -n '/^initialize_lifecycle_memory_checkpoints()/,/^run_wasix_lifecycle_plateau()/p' \ + "$bench" | sed '$d' >"$tmp/checkpoint-functions.sh" +# shellcheck source=/dev/null +source "$root/lib/process-supervision.sh" +# shellcheck source=/dev/null +source "$tmp/checkpoint-functions.sh" + +now_ns() { + perl -MTime::HiRes=clock_gettime,CLOCK_MONOTONIC \ + -e 'printf "%.0f\n", clock_gettime(CLOCK_MONOTONIC) * 1000000000' +} + +# Referenced by the dynamically extracted capture function. +# shellcheck disable=SC2034 +wasix_lifecycle_reconnects=100 +# shellcheck disable=SC2034 +wasix_lifecycle_memory_checkpoint_every=25 +raw="$tmp/checkpoints.tsv" +result="$tmp/result.tsv" +runtime_plateau="$tmp/runtime-plateau.tsv" +nonce=0123456789abcdef0123456789abcdef +server_pid="$$" +server_birth_identity="$(fresh_process_birth_identity "$server_pid")" +initialize_lifecycle_memory_checkpoints "$raw" + +sequence=0 +for completed in 0 25 50 75 100; do + if [ "$completed" -eq 0 ]; then + stage=baseline-fenced + elif [ "$completed" -eq 100 ]; then + stage=final-fenced + else + stage=wave-quiescent + fi + quiescence_start_ns="$(now_ns)" + sleep 0.01 + quiescence_end_ns="$(now_ns)" + capture_lifecycle_memory_checkpoint "$raw" "$nonce" "$sequence" "$stage" \ + "$completed" "$server_pid" "$server_birth_identity" 0.01 \ + "$quiescence_start_ns" "$quiescence_end_ns" + sequence=$((sequence + 1)) +done + +python3 - "$raw" "$runtime_plateau" "$nonce" <<'PY' +import csv +from pathlib import Path +import sys + +with Path(sys.argv[1]).open(encoding="utf-8", newline="") as stream: + rows = list(csv.DictReader(stream, delimiter="\t")) +header = ( + "schema_version\ttarget\tstatus\tnonce\tevidence_sha256\t" + "freeze_receipt_sha256\treconnect_requested\treconnect_completed\t" + "reconnect_start_mono_ns\treconnect_end_mono_ns\t" + "readiness_fence_mono_ns\tpost_quiescence_fence_mono_ns\n" +) +row = ( + f"6\twasix\tpassed\t{sys.argv[3]}\t{'1' * 64}\t{'2' * 64}\t100\t100\t" + f"{int(rows[0]['monotonic_after_ns']) + 1}\t" + f"{int(rows[-1]['quiescence_start_ns']) - 1}\t" + f"{rows[0]['quiescence_end_ns']}\t{rows[-1]['quiescence_end_ns']}\n" +) +Path(sys.argv[2]).write_text(header + row, encoding="utf-8") +PY + +python3 "$validator" \ + --input "$raw" \ + --runtime-plateau "$runtime_plateau" \ + --output "$result" \ + --nonce "$nonce" \ + --server-pid "$server_pid" \ + --requested-reconnects 100 \ + --checkpoint-every 25 \ + --min-quiescence-seconds 0.005 \ + --max-pss-growth-kib 1048576 \ + --max-pss-anon-growth-kib 1048576 \ + --max-heap-growth-kib 1048576 \ + --max-late-pss-slope-kib-per-1000-reconnects 1048576 \ + --max-late-pss-anon-slope-kib-per-1000-reconnects 1048576 \ + --max-late-heap-slope-kib-per-1000-reconnects 1048576 + +awk -F '\t' ' + NR == 1 { + for (column = 1; column <= NF; column++) field[$column] = column + next + } + NR == 2 { + if ($(field["status"]) != "passed" || + $(field["checkpoint_count"]) != 5 || + $(field["server_birth_identity"]) !~ /^linux-starttime:[1-9][0-9]*$/ || + $(field["input_sha256"]) !~ /^[0-9a-f]{64}$/ || + $(field["validator_sha256"]) !~ /^[0-9a-f]{64}$/) exit 1 + valid = 1 + } + END { exit !valid } +' "$result" + +printf 'passed: opt-in quiescent lifecycle PSS/anonymous/heap checkpoints\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/pin-runtime-artifacts.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/pin-runtime-artifacts.sh new file mode 100755 index 00000000..a2f14b73 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/pin-runtime-artifacts.sh @@ -0,0 +1,285 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +usage() { + cat <<'USAGE' +Usage: pin-runtime-artifacts.sh [options] + +Create a named experiment snapshot from a WASIX PostgreSQL install, matched +Wasmer binary/build receipt, and compiled Wasmer cache. The snapshot lives under +the ignored work tree so measurements can be replayed without recompiling. It is +not the signed, self-contained host carrier required for release promotion. + +Options: + --name NAME Pin name. Defaults to profile plus Wasmer hash/config. + --profile PROFILE WASIX profile for the install tree. Defaults to current profile. + --wasmer-bin PATH Wasmer binary to pin. Defaults to fresh_wasmer_bin. + --install-dir PATH WASIX PostgreSQL install tree to pin. + --cache-dir PATH Compiled Wasmer cache tree to pin. + --force Replace an existing pin with the same name. + -h, --help Show this help. +USAGE +} + +profile="$WASIX_CORE_PROFILE" +pin_name="" +wasmer_bin="" +install_dir="" +cache_dir="" +force=0 + +while [ "$#" -gt 0 ]; do + case "$1" in + --name) + shift + if [ "$#" -eq 0 ]; then + echo "--name requires a value" >&2 + exit 2 + fi + pin_name="$1" + ;; + --profile) + shift + if [ "$#" -eq 0 ]; then + echo "--profile requires a WASIX_CORE_PROFILE value" >&2 + exit 2 + fi + profile="$(fresh_normalize_wasix_core_profile "$1")" + ;; + --wasmer-bin) + shift + if [ "$#" -eq 0 ]; then + echo "--wasmer-bin requires a path" >&2 + exit 2 + fi + wasmer_bin="$1" + ;; + --install-dir) + shift + if [ "$#" -eq 0 ]; then + echo "--install-dir requires a path" >&2 + exit 2 + fi + install_dir="$1" + ;; + --cache-dir) + shift + if [ "$#" -eq 0 ]; then + echo "--cache-dir requires a path" >&2 + exit 2 + fi + cache_dir="$1" + ;; + --force) + force=1 + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +if [ -z "$wasmer_bin" ]; then + wasmer_bin="$(fresh_wasmer_bin)" +fi +if [ -z "$install_dir" ]; then + install_dir="$(fresh_wasix_core_install_dir_for "$profile")" +fi +if [ -z "$cache_dir" ]; then + cache_dir="$(fresh_wasmer_cache_dir "$wasmer_bin")" +fi + +if [ ! -x "$wasmer_bin" ]; then + printf 'missing executable Wasmer binary: %s\n' "$wasmer_bin" >&2 + exit 2 +fi +fresh_require_patched_wasmer "$wasmer_bin" +wasmer_build_receipt="${WASMER_BUILD_RECEIPT:-$FRESH_WASMER_BUILD_RECEIPT}" +if [ ! -x "$install_dir/bin/postgres" ] || [ ! -x "$install_dir/bin/initdb" ]; then + printf 'missing WASIX PostgreSQL install tree: %s\n' "$install_dir" >&2 + exit 2 +fi +if [ ! -d "$cache_dir" ]; then + printf 'missing compiled Wasmer cache tree: %s\n' "$cache_dir" >&2 + exit 2 +fi + +wasmer_hash="$(fresh_wasmer_bin_hash "$wasmer_bin")" +if [ -z "$pin_name" ]; then + pin_name="$profile-$wasmer_hash" + if fresh_wasmer_llvm_native_cpu_enabled; then + pin_name="${pin_name}-nativecpu" + fi + if fresh_wasmer_llvm_full_o3_enabled; then + pin_name="${pin_name}-fullo3" + fi + if fresh_wasmer_llvm_indirect_call_cache_enabled; then + pin_name="${pin_name}-indirectcache" + fi +fi +case "$pin_name" in + ""|*[!A-Za-z0-9._-]*) + printf 'pin name may only contain letters, numbers, ".", "_", and "-": %s\n' "$pin_name" >&2 + exit 2 + ;; +esac + +pin_parent="$FRESH_WORK_ROOT/tools/pinned-runtimes" +pin_root="$pin_parent/$pin_name" +tmp_root="$pin_parent/.${pin_name}.tmp.$$" +old_root="$pin_parent/.${pin_name}.old.$$" + +cleanup_pin() { + if [ -e "$old_root" ] && [ ! -e "$pin_root" ]; then + mv "$old_root" "$pin_root" 2>/dev/null || true + fi + rm -rf "$tmp_root" "$old_root" +} +trap cleanup_pin EXIT + +case "$wasmer_bin" in "$pin_root"|"$pin_root"/*) echo "refusing to pin from destination Wasmer binary" >&2; exit 2 ;; esac +case "$install_dir" in "$pin_root"|"$pin_root"/*) echo "refusing to pin from destination install tree" >&2; exit 2 ;; esac +case "$cache_dir" in "$pin_root"|"$pin_root"/*) echo "refusing to pin from destination cache tree" >&2; exit 2 ;; esac + +if [ -L "$pin_root" ]; then + printf 'refusing symlinked pin destination: %s\n' "$pin_root" >&2 + exit 2 +fi +if [ -e "$pin_root" ] && [ "$force" -ne 1 ]; then + printf 'pin already exists: %s\n' "$pin_root" >&2 + printf 'Use --force to replace it.\n' >&2 + exit 2 +fi + +copy_tree() { + local src="$1" + local dst="$2" + mkdir -p "$dst" + if command -v rsync >/dev/null 2>&1; then + rsync -a --delete "$src"/ "$dst"/ + else + (cd "$src" && tar -cf - .) | (cd "$dst" && tar -xf -) + fi +} + +shell_quote() { + printf '%q' "$1" +} + +write_tree_manifest() { + local root="$1" + local prefix="$2" + local out="$3" + ( + cd "$root" + find . -type f | LC_ALL=C sort | while IFS= read -r rel; do + hash="$(fresh_wasmer_bin_hash "$rel")" + printf '%s\t%s/%s\n' "$hash" "$prefix" "${rel#./}" + done + find . -type l | LC_ALL=C sort | while IFS= read -r rel; do + target="$(readlink "$rel")" + printf 'SYMLINK\t%s/%s\t%s\n' "$prefix" "${rel#./}" "$target" + done + ) >"$out" +} + +mkdir -p "$pin_parent" +rm -rf "$tmp_root" +mkdir -p "$tmp_root" + +cp -p "$wasmer_bin" "$tmp_root/wasmer" +chmod +x "$tmp_root/wasmer" +cp -p "$wasmer_build_receipt" "$tmp_root/wasmer-build.receipt" +WASMER_BUILD_RECEIPT="$tmp_root/wasmer-build.receipt" \ + fresh_require_patched_wasmer "$tmp_root/wasmer" +copy_tree "$install_dir" "$tmp_root/install" +copy_tree "$cache_dir" "$tmp_root/wasmer-cache" + +write_tree_manifest "$tmp_root/install" install "$tmp_root/install.files.tsv" +write_tree_manifest "$tmp_root/wasmer-cache" wasmer-cache "$tmp_root/wasmer-cache.files.tsv" +{ + printf 'HASH\tPATH\n' + printf '%s\twasmer\n' "$(fresh_wasmer_bin_hash "$tmp_root/wasmer")" + printf '%s\twasmer-build.receipt\n' "$(fresh_wasmer_bin_hash "$tmp_root/wasmer-build.receipt")" + cat "$tmp_root/install.files.tsv" "$tmp_root/wasmer-cache.files.tsv" +} >"$tmp_root/manifest.files.tsv" + +install_manifest_hash="$(fresh_wasmer_bin_hash "$tmp_root/install.files.tsv")" +cache_manifest_hash="$(fresh_wasmer_bin_hash "$tmp_root/wasmer-cache.files.tsv")" +bundle_manifest_hash="$(fresh_wasmer_bin_hash "$tmp_root/manifest.files.tsv")" +wasmer_version="$(fresh_wasmer_version "$wasmer_bin" 2>/dev/null || true)" +created_utc="$(fresh_timestamp)" + +{ + printf 'PINNED_RUNTIME_NAME=%s\n' "$(shell_quote "$pin_name")" + printf 'PINNED_CREATED_UTC=%s\n' "$(shell_quote "$created_utc")" + printf 'PINNED_PROFILE=%s\n' "$(shell_quote "$profile")" + printf 'PINNED_ROOT=%s\n' "$(shell_quote "$pin_root")" + printf 'PINNED_WASMER_BIN_SOURCE=%s\n' "$(shell_quote "$wasmer_bin")" + printf 'PINNED_WASMER_SHA256=%s\n' "$(shell_quote "$wasmer_hash")" + printf 'PINNED_WASMER_BUILD_RECEIPT_SHA256=%s\n' "$(shell_quote "$(fresh_wasmer_bin_hash "$tmp_root/wasmer-build.receipt")")" + printf 'PINNED_WASMER_VERSION=%s\n' "$(shell_quote "$wasmer_version")" + printf 'PINNED_INSTALL_SOURCE_DIR=%s\n' "$(shell_quote "$install_dir")" + printf 'PINNED_INSTALL_MANIFEST_SHA256=%s\n' "$(shell_quote "$install_manifest_hash")" + printf 'PINNED_WASMER_CACHE_SOURCE_DIR=%s\n' "$(shell_quote "$cache_dir")" + printf 'PINNED_WASMER_CACHE_MANIFEST_SHA256=%s\n' "$(shell_quote "$cache_manifest_hash")" + printf 'PINNED_BUNDLE_MANIFEST_SHA256=%s\n' "$(shell_quote "$bundle_manifest_hash")" + printf 'WASIX_CORE_PROFILE=%s\n' "$(shell_quote "$profile")" + printf 'WASMER_LLVM_NATIVE_CPU=%s\n' "$(shell_quote "${WASMER_LLVM_NATIVE_CPU:-0}")" + printf 'WASMER_LLVM_FULL_O3_PIPELINE=%s\n' "$(shell_quote "${WASMER_LLVM_FULL_O3_PIPELINE:-0}")" + printf 'WASMER_LLVM_INDIRECT_CALL_CACHE=%s\n' "$(shell_quote "${WASMER_LLVM_INDIRECT_CALL_CACHE:-0}")" + printf 'WASMER_LLVM_VOLATILE_MEMOPS=%s\n' "$(shell_quote "${WASMER_LLVM_VOLATILE_MEMOPS:-0}")" + printf 'WASMER_COMPILER=%s\n' "$(shell_quote "${WASMER_COMPILER:-llvm}")" + printf 'WASMER_LLVM_OPT_LEVEL=%s\n' "$(shell_quote "${WASMER_LLVM_OPT_LEVEL:-aggressive}")" + printf 'WASMER_CACHE_ARTIFACT_VERSION=%s\n' "$(shell_quote "${WASMER_CACHE_ARTIFACT_VERSION:-21}")" + printf 'WASMER_STACK_SIZE=%s\n' "$(shell_quote "${WASMER_STACK_SIZE:-33554432}")" +} >"$tmp_root/manifest.env" + +{ + printf '# Source this file to replay benchmarks against the experiment snapshot.\n' + printf 'export FRESH_PINNED_RUNTIME_NAME=%s\n' "$(shell_quote "$pin_name")" + printf 'export FRESH_PINNED_RUNTIME_ROOT=%s\n' "$(shell_quote "$pin_root")" + printf 'export FRESH_PINNED_WASIX_CORE_PROFILE=%s\n' "$(shell_quote "$profile")" + printf 'export FRESH_PINNED_WASIX_INSTALL_DIR=%s\n' "$(shell_quote "$pin_root/install")" + printf 'export FRESH_PINNED_WASMER_CACHE_DIR=%s\n' "$(shell_quote "$pin_root/wasmer-cache")" + printf 'export WASIX_CORE_PROFILE=%s\n' "$(shell_quote "$profile")" + printf 'export WASIX_INSTALL_DIR=%s\n' "$(shell_quote "$pin_root/install")" + printf 'export WASMER_BIN=%s\n' "$(shell_quote "$pin_root/wasmer")" + printf 'export WASMER_BUILD_RECEIPT=%s\n' "$(shell_quote "$pin_root/wasmer-build.receipt")" + printf 'export WASMER_CACHE_DIR=%s\n' "$(shell_quote "$pin_root/wasmer-cache")" + printf 'export WASMER_LLVM_NATIVE_CPU=%s\n' "$(shell_quote "${WASMER_LLVM_NATIVE_CPU:-0}")" + printf 'export WASMER_LLVM_FULL_O3_PIPELINE=%s\n' "$(shell_quote "${WASMER_LLVM_FULL_O3_PIPELINE:-0}")" + printf 'export WASMER_LLVM_INDIRECT_CALL_CACHE=%s\n' "$(shell_quote "${WASMER_LLVM_INDIRECT_CALL_CACHE:-0}")" + printf 'export WASMER_LLVM_VOLATILE_MEMOPS=%s\n' "$(shell_quote "${WASMER_LLVM_VOLATILE_MEMOPS:-0}")" + printf 'export WASMER_COMPILER=%s\n' "$(shell_quote "${WASMER_COMPILER:-llvm}")" + printf 'export WASMER_LLVM_OPT_LEVEL=%s\n' "$(shell_quote "${WASMER_LLVM_OPT_LEVEL:-aggressive}")" + printf 'export WASMER_CACHE_ARTIFACT_VERSION=%s\n' "$(shell_quote "${WASMER_CACHE_ARTIFACT_VERSION:-21}")" + printf 'export WASMER_STACK_SIZE=%s\n' "$(shell_quote "${WASMER_STACK_SIZE:-33554432}")" +} >"$tmp_root/env.sh" + +if [ -e "$pin_root" ]; then + mv "$pin_root" "$old_root" +fi +if ! mv "$tmp_root" "$pin_root"; then + [ ! -e "$old_root" ] || mv "$old_root" "$pin_root" + exit 1 +fi +rm -rf "$old_root" +trap - EXIT + +printf 'pinned runtime experiment snapshot: %s\n' "$pin_root" +printf ' wasmer: %s (%s)\n' "$pin_root/wasmer" "$wasmer_hash" +printf ' install: %s\n' "$pin_root/install" +printf ' wasmer cache: %s\n' "$pin_root/wasmer-cache" +printf ' manifest: %s\n' "$pin_root/manifest.env" +printf ' env: %s\n' "$pin_root/env.sh" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/precompile-wasix-core.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/precompile-wasix-core.sh new file mode 100755 index 00000000..2ae9e9c4 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/precompile-wasix-core.sh @@ -0,0 +1,255 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +force=0 +modules=() +while [ "$#" -gt 0 ]; do + case "$1" in + --force) + force=1 + ;; + --module) + shift + if [ "$#" -eq 0 ]; then + echo "--module requires a path relative to the WASIX install root or an absolute path" >&2 + exit 2 + fi + modules+=("$1") + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + exit 2 + ;; + esac + shift +done + +fresh_ensure_dirs + +wasmer_receipt="${WASMER_BUILD_RECEIPT:-$FRESH_WASMER_BUILD_RECEIPT}" +product_receipt="$FRESH_POSTMASTER_EXECUTOR_BUILD_RECEIPT" +compiler_bin="$FRESH_POSTMASTER_COMPILER_BIN" +fresh_require_patched_postmaster_compiler \ + "$compiler_bin" "$product_receipt" "$wasmer_receipt" \ + "$FRESH_POSTMASTER_EXECUTOR_BIN" +compiler_bin_hash="$(fresh_wasmer_bin_hash "$compiler_bin")" +compiler_cache_dir="$(fresh_wasmer_cache_dir "$compiler_bin")" +wasmer_compiler=llvm +llvm_opt_level="${WASMER_LLVM_OPT_LEVEL:-aggressive}" +artifact_version="${WASMER_CACHE_ARTIFACT_VERSION:-21}" +compiler_threads="${WASMER_COMPILER_THREADS:-$(fresh_jobs)}" +precompile_timeout="${WASIX_PRECOMPILE_TIMEOUT:-0}" +precompile_scope="${WASIX_PRECOMPILE_SCOPE:-runtime}" + +if [ -n "${FRESH_PINNED_WASMER_CACHE_DIR:-}" ]; then + { + printf 'postmaster product AOT refuses pinned or foreign cache roots: %s\n' "$FRESH_PINNED_WASMER_CACHE_DIR" + printf 'Unset FRESH_PINNED_WASMER_CACHE_DIR; the product compiler hash must own the cache namespace.\n' + } >&2 + exit 2 +fi +if fresh_wasmer_llvm_native_cpu_enabled; then + echo 'the postmaster product compiler refuses host-native CPU AOT' >&2 + exit 2 +fi + +append_module_if_present() { + local module="$1" + if [ -f "$WASIX_INSTALL_DIR/$module" ]; then + modules+=("$module") + fi +} + +append_installed_modules_matching() { + local dir="$1" + shift + if [ ! -d "$dir" ]; then + return + fi + while IFS= read -r path; do + modules+=("${path#$WASIX_INSTALL_DIR/}") + done < <(find "$dir" "$@" -type f -print | sort) +} + +if [ "${#modules[@]}" -eq 0 ]; then + case "$precompile_scope" in + minimal) + modules=( + bin/initdb + lib/libpq.so.5.18 + bin/postgres + ) + ;; + runtime) + append_module_if_present bin/initdb + append_module_if_present bin/postgres + append_module_if_present bin/pg_dump + append_installed_modules_matching "$WASIX_INSTALL_DIR/lib" -maxdepth 1 -name 'libpq.so*' + append_installed_modules_matching "$WASIX_INSTALL_DIR/lib/postgresql" -name '*.so' + ;; + all) + append_installed_modules_matching "$WASIX_INSTALL_DIR/bin" -maxdepth 1 -perm -111 + append_installed_modules_matching "$WASIX_INSTALL_DIR/lib" -maxdepth 1 -name 'libpq.so*' + append_installed_modules_matching "$WASIX_INSTALL_DIR/lib/postgresql" -name '*.so' + append_module_if_present lib/postgresql/regress.dylib + ;; + *) + echo "unknown WASIX_PRECOMPILE_SCOPE=$precompile_scope; expected minimal, runtime, or all" >&2 + exit 2 + ;; + esac +fi +if [ "${#modules[@]}" -eq 0 ]; then + echo "no WASIX modules found to precompile under $WASIX_INSTALL_DIR" >&2 + exit 2 +fi + +cache_bucket="$compiler_cache_dir/compiled/$(fresh_wasmer_compiler_cache_bucket "$wasmer_compiler" "$llvm_opt_level" "$artifact_version")" +wasmer_dir="$FRESH_WORK_ROOT/tools/wasmer-home" +report="$REPORT_DIR/wasix-core-precompile.md" +log="$REPORT_DIR/wasix-core-precompile.log" +fresh_write_report_header "$report" "WASIX Core Precompile" +mkdir -p "$wasmer_dir" "$cache_bucket" "$(dirname "$log")" +: >"$log" + +{ + printf '## Runtime\n\n' + printf -- '- Product compiler: `%s`\n' "$compiler_bin" + printf -- '- Product compiler hash: `%s`\n' "$compiler_bin_hash" + printf -- '- Product compiler version: `%s`\n' "$("$compiler_bin" --version)" + printf -- '- Product build receipt: `%s`\n' "$product_receipt" + printf -- '- Cache dir: `%s`\n' "$compiler_cache_dir" + printf -- '- Cache bucket: `%s`\n' "$cache_bucket" + printf -- '- WASIX core profile: `%s`\n' "$WASIX_CORE_PROFILE" + printf -- '- WASIX install dir: `%s`\n' "$WASIX_INSTALL_DIR" + printf -- '- Wasmer compiler: `%s`\n' "$wasmer_compiler" + printf -- '- LLVM opt level: `%s`\n' "$llvm_opt_level" + printf -- '- WASMER_LLVM_FULL_O3_PIPELINE: `%s`\n' "${WASMER_LLVM_FULL_O3_PIPELINE:-0}" + printf -- '- WASMER_LLVM_INDIRECT_CALL_CACHE: `%s`\n' "${WASMER_LLVM_INDIRECT_CALL_CACHE:-0}" + printf -- '- WASMER_LLVM_VOLATILE_MEMOPS: `%s`\n' "${WASMER_LLVM_VOLATILE_MEMOPS:-0}" + printf -- '- Compiler threads: `%s`\n' "$compiler_threads" + printf -- '- Module scope: `%s`\n' "$precompile_scope" + printf -- '- Timeout per module: `%s`\n' "$precompile_timeout" + printf -- '- Log: `%s`\n\n' "$log" + printf '## Modules\n\n' +} >>"$report" + +compile_one() { + local module="$1" + local wasm_path + case "$module" in + /*) wasm_path="$module" ;; + *) wasm_path="$WASIX_INSTALL_DIR/$module" ;; + esac + + if [ ! -f "$wasm_path" ]; then + printf 'missing WASIX module: %s\n' "$wasm_path" >&2 + return 2 + fi + + local module_hash cache_path tmp_path status + module_hash="$(fresh_wasmer_module_hash "$wasm_path")" + cache_path="$cache_bucket/$module_hash.bin" + + printf -- '- `%s` -> `%s`' "$wasm_path" "$cache_path" >>"$report" + if [ "$force" -eq 0 ] && [ -s "$cache_path" ]; then + if "$compiler_bin" verify-aot "$wasm_path" "$cache_path" >>"$log" 2>&1; then + printf ' (cached, attested)\n' >>"$report" + return 0 + fi + printf ' (cached artifact rejected; rebuilding)\n' >>"$report" + fi + printf '\n' >>"$report" + + tmp_path="$cache_path.tmp.$$" + rm -f "$tmp_path" + { + printf '\n## %s\n\n' "$wasm_path" + printf 'hash=%s\ncache=%s\n' "$module_hash" "$cache_path" + } >>"$log" + + local compiler_args=( + --llvm + --llvm-opt-level "$llvm_opt_level" + --compiler-threads "$compiler_threads" + ) + if fresh_wasmer_llvm_full_o3_enabled; then + compiler_args+=(--llvm-full-o3-pipeline) + fi + if fresh_wasmer_llvm_indirect_call_cache_enabled; then + compiler_args+=(--llvm-indirect-call-cache) + fi + if fresh_wasmer_llvm_volatile_memops_enabled; then + compiler_args+=(--disable-non-volatile-memops) + fi + + set +e + if [ "$precompile_timeout" != "0" ] && command -v timeout >/dev/null 2>&1; then + timeout "$precompile_timeout" \ + env \ + WASMER_DIR="$wasmer_dir" \ + WASMER_CACHE_DIR="$compiler_cache_dir" \ + "$compiler_bin" \ + "${compiler_args[@]}" \ + --enable-exceptions \ + --enable-threads \ + -o "$tmp_path" \ + "$wasm_path" >>"$log" 2>&1 + status=$? + else + env \ + WASMER_DIR="$wasmer_dir" \ + WASMER_CACHE_DIR="$compiler_cache_dir" \ + "$compiler_bin" \ + "${compiler_args[@]}" \ + --enable-exceptions \ + --enable-threads \ + -o "$tmp_path" \ + "$wasm_path" >>"$log" 2>&1 + status=$? + fi + set -e + + if [ "$status" -ne 0 ]; then + rm -f "$tmp_path" + printf 'precompile failed for %s; see %s\n' "$wasm_path" "$log" >&2 + return "$status" + fi + + if ! "$compiler_bin" verify-aot "$wasm_path" "$tmp_path" >>"$log" 2>&1; then + rm -f "$tmp_path" + printf 'compiled AOT admission failed for %s; see %s\n' "$wasm_path" "$log" >&2 + return 2 + fi + + mv "$tmp_path" "$cache_path" +} + +status=0 +for module in "${modules[@]}"; do + set +e + compile_one "$module" + status=$? + set -e + if [ "$status" -ne 0 ]; then + break + fi +done + +{ + printf '\n## Result\n\n' + if [ "$status" -eq 0 ]; then + printf -- '- Status: `pass`\n' + else + printf -- '- Status: `fail`\n' + fi + printf -- '- Exit code: `%s`\n' "$status" +} >>"$report" + +if [ "$status" -eq 0 ]; then + printf 'precompiled WASIX core modules into %s\n' "$cache_bucket" +fi +exit "$status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/prepare-baseline.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/prepare-baseline.sh new file mode 100755 index 00000000..ccc6f039 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/prepare-baseline.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" +source "$REPO_ROOT/src/postgres/versions/18/fetch-source.sh" + +print_path=0 +refresh=0 +while [ "$#" -gt 0 ]; do + case "$1" in + --print-path) print_path=1 ;; + --refresh) refresh=1 ;; + *) echo "usage: $0 [--print-path] [--refresh]" >&2; exit 2 ;; + esac + shift +done + +read_toml_value() { + local key="$1" + awk -F'=' -v key="$key" ' + $1 ~ "^[[:space:]]*" key "[[:space:]]*$" { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2) + gsub(/^"|"$/, "", $2) + print $2 + exit + } + ' "$POSTGRES_SOURCE_TOML" +} + +sha256_file() { + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + sha256sum "$1" | awk '{print $1}' + fi +} + +fresh_ensure_dirs +fresh_require_command git +fresh_require_command tar + +manifest_version="$(read_toml_value version)" +manifest_url="$(read_toml_value url)" +manifest_sha256="$(read_toml_value sha256)" +[ "$manifest_version" = "$POSTGRES_VERSION" ] || { + echo "PostgreSQL version mismatch: common=$POSTGRES_VERSION manifest=$manifest_version" >&2 + exit 1 +} +expected_tag="REL_${POSTGRES_VERSION//./_}" +[ "$POSTGRES_TAG" = "$expected_tag" ] || { + echo "PostgreSQL tag mismatch: expected $expected_tag, got $POSTGRES_TAG" >&2 + exit 1 +} + +source_cache="$REPO_ROOT/target/liboliphaunt-pg18/source" +tarball="$source_cache/postgresql-$POSTGRES_VERSION.tar.bz2" +fingerprint_file="$FRESH_WORK_ROOT/.postgres-baseline-fingerprint" +fingerprint="$POSTGRES_VERSION:$manifest_sha256" + +if [ "$refresh" -eq 1 ]; then + rm -f "$tarball" +fi + +oliphaunt_fetch_postgresql_source_archive \ + "$tarball" \ + "$POSTGRES_VERSION" \ + "$manifest_sha256" \ + "$manifest_url" + +[ "$(sha256_file "$tarball")" = "$manifest_sha256" ] || { + echo "verified PostgreSQL source cache changed unexpectedly: $tarball" >&2 + exit 1 +} + +reuse=0 +if [ "$refresh" -eq 0 ] && [ -d "$BASELINE_DIR/.git" ] && [ -f "$fingerprint_file" ]; then + if [ "$(cat "$fingerprint_file")" = "$fingerprint" ] && + [ -z "$(git -C "$BASELINE_DIR" status --porcelain)" ]; then + reuse=1 + fi +fi + +if [ "$reuse" -eq 0 ]; then + case "$BASELINE_DIR" in + "$FRESH_WORK_ROOT"/*) ;; + *) echo "baseline directory escaped the generated work root: $BASELINE_DIR" >&2; exit 2 ;; + esac + rm -rf "$BASELINE_DIR" + extracted="$FRESH_WORK_ROOT/sources/postgresql-$POSTGRES_VERSION" + tar -xjf "$tarball" -C "$FRESH_WORK_ROOT/sources" + [ "$extracted" = "$BASELINE_DIR" ] || mv "$extracted" "$BASELINE_DIR" + + git init --quiet "$BASELINE_DIR" + git -C "$BASELINE_DIR" config core.autocrlf false + git -C "$BASELINE_DIR" config core.eol lf + git -C "$BASELINE_DIR" add -A + GIT_AUTHOR_NAME="Oliphaunt Source Prep" \ + GIT_AUTHOR_EMAIL="dev@oliphaunt.dev" \ + GIT_AUTHOR_DATE="2000-01-01T00:00:00Z" \ + GIT_COMMITTER_NAME="Oliphaunt Source Prep" \ + GIT_COMMITTER_EMAIL="dev@oliphaunt.dev" \ + GIT_COMMITTER_DATE="2000-01-01T00:00:00Z" \ + git -C "$BASELINE_DIR" commit --quiet -m "source: PostgreSQL $POSTGRES_VERSION" + printf '%s' "$fingerprint" >"$fingerprint_file" +fi + +baseline_head="$(git -C "$BASELINE_DIR" rev-parse HEAD)" +report="$REPORT_DIR/baseline.md" +fresh_write_report_header "$report" "Clean PostgreSQL Baseline" +{ + printf '## Result\n\n' + printf -- '- Source directory: `%s`\n' "$BASELINE_DIR" + printf -- '- Pinned archive: `%s`\n' "$manifest_url" + printf -- '- Archive SHA-256: `%s`\n' "$manifest_sha256" + printf -- '- Local deterministic baseline commit: `%s`\n\n' "$baseline_head" + printf '## Lineage Rule\n\n' + printf 'This checkout is the clean canonical PostgreSQL 18.4 archive oracle. Do not apply runtime patches here.\n' +} >>"$report" + +if [ "$print_path" -eq 1 ]; then + printf '%s\n' "$BASELINE_DIR" +else + printf 'prepared clean PostgreSQL %s at %s\n' "$POSTGRES_VERSION" "$BASELINE_DIR" +fi diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/probe-wasix-runtime.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/probe-wasix-runtime.sh new file mode 100755 index 00000000..1263905d --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/probe-wasix-runtime.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +fresh_ensure_dirs +docker_bin="$(fresh_docker_bin)" +wasmer_bin="$(fresh_wasmer_bin)" + +fresh_ensure_docker_image >/dev/null + +probe_source_dir="$FRESH_ROOT/probes/bootstrap" +probe_build_dir="$FRESH_WORK_ROOT/builds/runtime-probes" +report="$REPORT_DIR/wasix-runtime-capabilities.md" +compile_log="$REPORT_DIR/wasix-runtime-probes.compile.log" +mmap_log="$REPORT_DIR/wasix-runtime-probe.mmap.log" +fork_pic_log="$REPORT_DIR/wasix-runtime-probe.fork-pic.log" +fork_shm_log="$REPORT_DIR/wasix-runtime-probe.fork-shm.log" +mkdir -p "$probe_build_dir" + +fresh_write_report_header "$report" "WASIX Runtime Capability Probes" + +{ + printf '## Scope\n\n' + printf -- '- Wasmer: `%s`\n' "$(fresh_wasmer_version "$wasmer_bin" 2>/dev/null || true)" + printf -- '- wasixcc image: `%s`\n' "$FRESH_WASIX_DOCKER_IMAGE" + printf -- '- Probe source directory: `%s`\n' "$probe_source_dir" + printf -- '- Probe build directory: `%s`\n' "$probe_build_dir" + printf -- '- Policy: record runtime/toolchain blockers as blockers; do not hide them with PostgreSQL success shims.\n\n' + printf '## Official Capability Surface Checked\n\n' + printf -- '- WASIX documents sockets, threads/futexes, `proc_fork`, `proc_spawn`, `proc_join`, pipes/events, polling, and filesystem calls: https://wasix.org/docs/explanation/features\n' + printf -- '- Wasmer CLI documents LLVM/Cranelift/Singlepass, profiling, networking, threads, module linking, snapshots/journals, and cache controls: https://docs.wasmer.io/runtime/cli/\n' + printf -- '- Wasmer dynamic-linking guidance requires PIC/EH for dynamic main modules and side modules: https://wasmer.io/es/posts/dynamic-linking-in-wasm-wasix\n\n' +} >>"$report" + +: >"$compile_log" +"$docker_bin" run --rm \ + -v "$REPO_ROOT:/work" \ + -w /work \ + -e PROBE_SOURCE_DIR="${probe_source_dir#$REPO_ROOT/}" \ + -e PROBE_BUILD_DIR="${probe_build_dir#$REPO_ROOT/}" \ + "$FRESH_WASIX_DOCKER_IMAGE" \ + bash -lc ' + set -euo pipefail + source ./src/runtimes/liboliphaunt/wasix/assets/build/docker_wasix_env.sh + mkdir -p "$PROBE_BUILD_DIR" + WASIXCC_WASM_EXCEPTIONS=yes wasixcc -O0 -g3 -fPIC -pthread \ + "$PROBE_SOURCE_DIR/mmap_probe.c" \ + -o "$PROBE_BUILD_DIR/mmap_probe.wasm" + WASIXCC_WASM_EXCEPTIONS=yes wasixcc -O0 -g3 -fPIC -pthread \ + "$PROBE_SOURCE_DIR/fork_shm_probe.c" \ + -o "$PROBE_BUILD_DIR/fork_shm_probe.pic.wasm" + WASIXCC_WASM_EXCEPTIONS=no wasixcc -O0 -g3 -pthread \ + "$PROBE_SOURCE_DIR/libc_fork_shm_probe.c" \ + -o "$PROBE_BUILD_DIR/libc_fork_shm_probe.asyncify.wasm" + ' >"$compile_log" 2>&1 + +run_probe() { + local name="$1" + local log="$2" + shift 2 + + set +e + env WASMER_DIR="$FRESH_WORK_ROOT/tools/wasmer-home" \ + WASMER_CACHE_DIR="$FRESH_WORK_ROOT/tools/wasmer-cache" \ + "$wasmer_bin" run --quiet "$@" >"$log" 2>&1 + local status=$? + set -e + + { + printf '## %s\n\n' "$name" + printf -- '- Exit code: `%s`\n' "$status" + printf -- '- Log: `%s`\n\n' "$log" + printf '```text\n' + sed -n '1,80p' "$log" + printf '```\n\n' + } >>"$report" +} + +run_probe "PIC mmap MAP_FIXED" "$mmap_log" \ + --enable-exceptions --enable-threads \ + --volume "$FRESH_WORK_ROOT:$FRESH_WORK_ROOT" \ + "$probe_build_dir/mmap_probe.wasm" + +run_probe "PIC proc_fork" "$fork_pic_log" \ + --enable-exceptions --enable-threads \ + --volume "$FRESH_WORK_ROOT:$FRESH_WORK_ROOT" \ + "$probe_build_dir/fork_shm_probe.pic.wasm" + +run_probe "Asyncify fork with MAP_SHARED shm" "$fork_shm_log" \ + --enable-threads \ + --volume "$FRESH_WORK_ROOT:$FRESH_WORK_ROOT" \ + "$probe_build_dir/libc_fork_shm_probe.asyncify.wasm" + +{ + printf '## Interpretation\n\n' + printf -- '- `MAP_FIXED` failure blocks EXEC_BACKEND shared-memory reattach, because PostgreSQL needs shared pointers to remain valid in child processes.\n' + printf -- '- PIC `proc_fork` failure blocks the dynamic-linking lane from using fork directly.\n' + printf -- '- Asyncify `fork()` succeeds as process control flow, but `MAP_SHARED` writes do not propagate back to the parent in this runtime, so it is not sufficient for PostgreSQL shared memory semantics.\n' + printf -- '- A production-quality fix belongs in the WASIX runtime/toolchain shared-memory/process implementation, or in a PostgreSQL architecture explicitly based on a real shared-address primitive. Thread-backed backends would be a separate design, not the native process model.\n' +} >>"$report" + +printf 'wrote WASIX runtime capability report to %s\n' "$report" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/process-tree-collector.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/process-tree-collector.test.sh new file mode 100755 index 00000000..7aaf9154 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/process-tree-collector.test.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +bench="$root/bin/bench-wasix-concurrent-query-suite.sh" +tmp="$(mktemp -d)" +trap 'rm -rf -- "$tmp"' EXIT + +sed -n '/^collect_linux_process_tree()/,/^collect_process_tree_snapshot()/p' "$bench" | + sed '$d' >"$tmp/collector.sh" +# shellcheck source=/dev/null +source "$tmp/collector.sh" + +proc_root="$tmp/proc" +for pid in 10 11 12 13; do + mkdir -p "$proc_root/$pid/task/$pid" + : >"$proc_root/$pid/task/$pid/children" +done +printf '11 12\n' >"$proc_root/10/task/10/children" +printf '13\n' >"$proc_root/11/task/11/children" + +expected=$'10\n11\n12\n13' +[ "$(collect_linux_process_tree 10 "$proc_root")" = "$expected" ] + +# Children created by a non-leader thread are part of the same process tree. +mkdir -p "$proc_root/10/task/99" +printf '13\n' >"$proc_root/10/task/99/children" +[ "$(collect_linux_process_tree 10 "$proc_root")" = "$expected" ] + +# A disappeared descendant is harmless; it is no longer in the observed tree. +printf '11 12 14\n' >"$proc_root/10/task/10/children" +[ "$(collect_linux_process_tree 10 "$proc_root")" = "$expected" ] + +printf '11 invalid\n' >"$proc_root/10/task/10/children" +if collect_linux_process_tree 10 "$proc_root" >/dev/null 2>&1; then + echo 'Linux process-tree collector accepted a malformed child PID' >&2 + exit 1 +fi + +printf '11 12\n' >"$proc_root/10/task/10/children" +rm -f "$proc_root/11/task/11/children" +if collect_linux_process_tree 10 "$proc_root" >/dev/null 2>&1; then + echo 'Linux process-tree collector accepted an unreadable live task' >&2 + exit 1 +fi + +if collect_linux_process_tree 10 relative/proc >/dev/null 2>&1; then + echo 'Linux process-tree collector accepted a relative proc root' >&2 + exit 1 +fi +if collect_linux_process_tree 0 "$proc_root" >/dev/null 2>&1; then + echo 'Linux process-tree collector accepted PID zero' >&2 + exit 1 +fi + +cgroup_dir="$tmp/scope" +mkdir -p "$cgroup_dir" +printf '10\n11\n12\n13\n' >"$cgroup_dir/cgroup.procs" +[ "$(collect_cgroup_process_set 10 "$cgroup_dir")" = "$expected" ] +printf '11\n12\n13\n' >"$cgroup_dir/cgroup.procs" +if collect_cgroup_process_set 10 "$cgroup_dir" >/dev/null 2>&1; then + echo 'cgroup process collector accepted a scope without its measured root' >&2 + exit 1 +fi +printf '10\n11\n11\n' >"$cgroup_dir/cgroup.procs" +if collect_cgroup_process_set 10 "$cgroup_dir" >/dev/null 2>&1; then + echo 'cgroup process collector accepted duplicate membership' >&2 + exit 1 +fi + +echo 'passed: Linux process-tree collection is bounded to the measured tree' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/profile-native-query.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/profile-native-query.sh new file mode 100755 index 00000000..b407148d --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/profile-native-query.sh @@ -0,0 +1,356 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +workload="unlogged-constant-insert" +setup_workload="${NATIVE_PROFILE_SETUP_WORKLOAD:-}" +profile_rows="${NATIVE_PROFILE_ROWS:-1000000}" +transaction_rows="${NATIVE_PROFILE_TRANSACTION_ROWS:-$profile_rows}" +transaction_rows_explicit=0 +if [ "${NATIVE_PROFILE_TRANSACTION_ROWS+x}" = x ]; then + transaction_rows_explicit=1 +fi +sample_seconds="${NATIVE_PROFILE_SAMPLE_SECONDS:-10}" +sample_delay="${NATIVE_PROFILE_SAMPLE_DELAY:-0.2}" +start_port="${PGPORT:-59600}" +skip_build=0 +run_label="${NATIVE_PROFILE_LABEL:-}" +postgres_gucs=() + +usage() { + cat <&2; exit 2; } + workload="$1" + ;; + --setup-workload) + shift + [ "$#" -gt 0 ] || { echo "--setup-workload requires a value" >&2; exit 2; } + setup_workload="$1" + ;; + --rows) + shift + [ "$#" -gt 0 ] || { echo "--rows requires a value" >&2; exit 2; } + profile_rows="$1" + if [ "$transaction_rows_explicit" -eq 0 ]; then + transaction_rows="$profile_rows" + fi + ;; + --transaction-rows) + shift + [ "$#" -gt 0 ] || { echo "--transaction-rows requires a value" >&2; exit 2; } + transaction_rows="$1" + transaction_rows_explicit=1 + ;; + --sample-seconds) + shift + [ "$#" -gt 0 ] || { echo "--sample-seconds requires a value" >&2; exit 2; } + sample_seconds="$1" + ;; + --sample-delay) + shift + [ "$#" -gt 0 ] || { echo "--sample-delay requires a value" >&2; exit 2; } + sample_delay="$1" + ;; + --start-port) + shift + [ "$#" -gt 0 ] || { echo "--start-port requires a value" >&2; exit 2; } + start_port="$1" + ;; + --skip-build) + skip_build=1 + ;; + --label) + shift + [ "$#" -gt 0 ] || { echo "--label requires a value" >&2; exit 2; } + case "$1" in + *[!A-Za-z0-9._-]*|"") + echo "--label may only contain letters, numbers, '.', '_', and '-'" >&2 + exit 2 + ;; + esac + run_label="$1" + ;; + --postgres-guc) + shift + [ "$#" -gt 0 ] || { echo "--postgres-guc requires a name=value setting" >&2; exit 2; } + postgres_gucs+=("$1") + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +if [ -n "${POSTGRES_GUCS:-}" ]; then + for guc in $POSTGRES_GUCS; do + postgres_gucs+=("$guc") + done +fi + +fresh_ensure_dirs + +workload_dir="$FRESH_ROOT/bench/sql/query-perf" +workload_path_for() { + case "$1" in + */*) printf '%s\n' "$1" ;; + *.sql) printf '%s/%s\n' "$workload_dir" "$1" ;; + *) printf '%s/%s.sql\n' "$workload_dir" "$1" ;; + esac +} +workload_name_for() { + basename "${1%.sql}" +} +now_ms() { + if command -v perl >/dev/null 2>&1; then + perl -MTime::HiRes=time -e 'printf "%.0f\n", time() * 1000' + else + printf '%s000\n' "$(date +%s)" + fi +} +extract_psql_time_sum_ms() { + awk ' + /^Time: [0-9.]+ ms/ { + sum += $2 + count += 1 + } + END { + if (count == 0) { + printf "" + } else { + printf "%.3f", sum + } + } + ' "$1" +} +stop_pid() { + local pid="${1:-}" + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi +} + +if [ -z "$run_label" ]; then + run_label="$(date -u +%Y%m%dT%H%M%SZ)-native-$(workload_name_for "$workload")" +fi + +run_root="$FRESH_WORK_ROOT/run/query-profiles/$run_label" +report_root="$FRESH_WORK_ROOT/reports/query-profiles/$run_label" +pgdata="$run_root/pgdata" +sql_path="$(workload_path_for "$workload")" +workload_name="$(workload_name_for "$workload")" +setup_sql_path="" +setup_workload_name="" +if [ -n "$setup_workload" ]; then + setup_sql_path="$(workload_path_for "$setup_workload")" + setup_workload_name="$(workload_name_for "$setup_workload")" +fi +app_name="native-profile-$run_label" + +mkdir -p "$run_root" "$report_root" +rm -rf "$pgdata" + +if [ ! -f "$sql_path" ]; then + printf 'missing workload SQL: %s\n' "$sql_path" >&2 + exit 2 +fi +if [ -n "$setup_sql_path" ] && [ ! -f "$setup_sql_path" ]; then + printf 'missing setup workload SQL: %s\n' "$setup_sql_path" >&2 + exit 2 +fi + +if [ "$skip_build" -ne 1 ]; then + "$FRESH_ROOT/bin/build-native-oracle.sh" >"$report_root/build.log" 2>&1 +elif [ ! -x "$NATIVE_INSTALL_DIR/bin/postgres" ]; then + printf 'missing %s/bin/postgres with --skip-build\n' "$NATIVE_INSTALL_DIR" >&2 + exit 2 +fi + +summary="$report_root/summary.md" +initdb_log="$report_root/initdb.log" +server_log="$report_root/server.log" +wait_log="$report_root/wait.log" +psql_log="$report_root/$workload_name.log" +setup_log="$report_root/${setup_workload_name:-setup}.setup.log" +backend_pid_log="$report_root/backend-pid.log" +sample_log="$report_root/sample.txt" +sample_stderr="$report_root/sample.stderr.log" + +{ + printf '# Native Query Profile\n\n' + printf -- '- Generated: `%s`\n' "$(fresh_timestamp)" + printf -- '- Workload: `%s`\n' "$workload_name" + printf -- '- Workload SQL: `%s`\n' "$sql_path" + printf -- '- Setup workload: `%s`\n' "${setup_workload_name:-}" + printf -- '- Setup workload SQL: `%s`\n' "${setup_sql_path:-}" + printf -- '- Rows: `%s`\n' "$profile_rows" + printf -- '- Transaction rows: `%s`\n' "$transaction_rows" + printf -- '- Port: `%s`\n' "$start_port" + printf -- '- Sample seconds: `%s`\n' "$sample_seconds" + printf -- '- Sample delay: `%s`\n' "$sample_delay" + printf -- '- PostgreSQL GUCs: `%s`\n\n' "${postgres_gucs[*]:-}" +} >"$summary" + +"$NATIVE_INSTALL_DIR/bin/initdb" \ + -D "$pgdata" \ + -A trust \ + --no-locale \ + --encoding=UTF8 \ + --no-instructions \ + >"$initdb_log" 2>&1 + +postgres_args=( + -D "$pgdata" + -h 127.0.0.1 + -p "$start_port" + -c unix_socket_directories= +) +if [ "${#postgres_gucs[@]}" -gt 0 ]; then + for guc in "${postgres_gucs[@]}"; do + postgres_args+=(-c "$guc") + done +fi + +set +e +"$NATIVE_INSTALL_DIR/bin/postgres" "${postgres_args[@]}" >"$server_log" 2>&1 & +server_pid=$! +set -e + +cleanup() { + stop_pid "$server_pid" +} +trap cleanup EXIT + +: >"$wait_log" +ready=0 +conn="postgresql://$(id -un)@127.0.0.1:$start_port/postgres" +for _ in $(seq 1 300); do + if "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -X -q -c 'select 1' >>"$wait_log" 2>&1; then + ready=1 + break + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + echo "native server exited before readiness" >>"$wait_log" + break + fi + sleep 0.1 +done +if [ "$ready" -ne 1 ]; then + printf 'native server did not become ready; see %s\n' "$wait_log" >&2 + exit 1 +fi + +if [ -n "$setup_sql_path" ]; then + "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" \ + -X -q \ + -v "perf_rows=$profile_rows" \ + -v "update_rows=$profile_rows" \ + -v "transaction_rows=$transaction_rows" \ + -f "$setup_sql_path" >"$setup_log" 2>&1 +fi + +started_ms="$(now_ms)" +set +e +PGAPPNAME="$app_name" \ +"$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" \ + -X -q \ + -v "perf_rows=$profile_rows" \ + -v "update_rows=$profile_rows" \ + -v "transaction_rows=$transaction_rows" \ + -f "$sql_path" >"$psql_log" 2>&1 & +psql_pid=$! +set -e + +backend_pid="" +: >"$backend_pid_log" +for _ in $(seq 1 200); do + if ! kill -0 "$psql_pid" 2>/dev/null; then + break + fi + backend_pid="$("$NATIVE_INSTALL_DIR/bin/psql" "$conn" -XAtq \ + -c "select pid from pg_stat_activity where application_name = '$app_name' order by backend_start desc limit 1" \ + 2>>"$backend_pid_log" | head -n 1 | tr -d '[:space:]')" + case "$backend_pid" in + ''|*[!0-9]*) + sleep 0.05 + ;; + *) + printf '%s\n' "$backend_pid" >"$backend_pid_log" + break + ;; + esac +done + +sleep "$sample_delay" +sample_status=0 +if [ -n "$backend_pid" ] && command -v sample >/dev/null 2>&1 && kill -0 "$psql_pid" 2>/dev/null && kill -0 "$backend_pid" 2>/dev/null; then + set +e + sample "$backend_pid" "$sample_seconds" -file "$sample_log" >"$sample_stderr" 2>&1 + sample_status=$? + set -e +else + sample_status=127 + printf 'sample command unavailable, backend missing, or workload finished before sampling\n' >"$sample_stderr" +fi + +set +e +wait "$psql_pid" +psql_status=$? +set -e +ended_ms="$(now_ms)" +wall_ms=$((ended_ms - started_ms)) +psql_time_ms="$(extract_psql_time_sum_ms "$psql_log")" + +{ + printf '\n## Result\n\n' + printf -- '- Postmaster PID: `%s`\n' "$server_pid" + printf -- '- Workload backend PID: `%s`\n' "${backend_pid:-missing}" + printf -- '- PSQL exit code: `%s`\n' "$psql_status" + printf -- '- Sample exit code: `%s`\n' "$sample_status" + printf -- '- Workload wall ms: `%s`\n' "$wall_ms" + printf -- '- PSQL timed ms: `%s`\n' "$psql_time_ms" + if [ -n "$setup_sql_path" ]; then + printf -- '- Setup log: `%s`\n' "$setup_log" + fi + printf -- '- PSQL log: `%s`\n' "$psql_log" + printf -- '- Backend PID log: `%s`\n' "$backend_pid_log" + printf -- '- Sample log: `%s`\n' "$sample_log" + printf -- '- Sample stderr: `%s`\n' "$sample_stderr" +} >>"$summary" + +printf 'native profile written to %s\n' "$summary" +exit "$psql_status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/profile-wasix-query.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/profile-wasix-query.sh new file mode 100755 index 00000000..42aaa7eb --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/profile-wasix-query.sh @@ -0,0 +1,486 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +profile="$WASIX_CORE_PROFILE" +initdb_profile="" +workload="unlogged-constant-insert" +setup_workload="${WASIX_PROFILE_SETUP_WORKLOAD:-}" +profile_rows="${WASIX_PROFILE_ROWS:-1000000}" +transaction_rows="${WASIX_PROFILE_TRANSACTION_ROWS:-$profile_rows}" +transaction_rows_explicit=0 +if [ "${WASIX_PROFILE_TRANSACTION_ROWS+x}" = x ]; then + transaction_rows_explicit=1 +fi +sql_timeout="${WASIX_PROFILE_SQL_TIMEOUT:-0}" +sample_seconds="${WASIX_PROFILE_SAMPLE_SECONDS:-10}" +sample_delay="${WASIX_PROFILE_SAMPLE_DELAY:-0.2}" +start_port="${PGPORT:-59500}" +precompile_scope="${WASIX_PRECOMPILE_SCOPE:-minimal}" +skip_build=0 +skip_precompile=0 +run_label="${WASIX_PROFILE_LABEL:-}" +postgres_gucs=() +wasmer_extra_args=() + +usage() { + cat <.map when available. +EOF +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --profile) + shift + [ "$#" -gt 0 ] || { echo "--profile requires a value" >&2; exit 2; } + profile="$(fresh_normalize_wasix_core_profile "$1")" + ;; + --initdb-profile) + shift + [ "$#" -gt 0 ] || { echo "--initdb-profile requires a value" >&2; exit 2; } + initdb_profile="$(fresh_normalize_wasix_core_profile "$1")" + ;; + --workload) + shift + [ "$#" -gt 0 ] || { echo "--workload requires a value" >&2; exit 2; } + workload="$1" + ;; + --setup-workload) + shift + [ "$#" -gt 0 ] || { echo "--setup-workload requires a value" >&2; exit 2; } + setup_workload="$1" + ;; + --rows) + shift + [ "$#" -gt 0 ] || { echo "--rows requires a value" >&2; exit 2; } + profile_rows="$1" + if [ "$transaction_rows_explicit" -eq 0 ]; then + transaction_rows="$profile_rows" + fi + ;; + --transaction-rows) + shift + [ "$#" -gt 0 ] || { echo "--transaction-rows requires a value" >&2; exit 2; } + transaction_rows="$1" + transaction_rows_explicit=1 + ;; + --sql-timeout) + shift + [ "$#" -gt 0 ] || { echo "--sql-timeout requires a value" >&2; exit 2; } + sql_timeout="$1" + ;; + --sample-seconds) + shift + [ "$#" -gt 0 ] || { echo "--sample-seconds requires a value" >&2; exit 2; } + sample_seconds="$1" + ;; + --sample-delay) + shift + [ "$#" -gt 0 ] || { echo "--sample-delay requires a value" >&2; exit 2; } + sample_delay="$1" + ;; + --start-port) + shift + [ "$#" -gt 0 ] || { echo "--start-port requires a value" >&2; exit 2; } + start_port="$1" + ;; + --precompile-scope) + shift + [ "$#" -gt 0 ] || { echo "--precompile-scope requires a value" >&2; exit 2; } + precompile_scope="$1" + ;; + --skip-build) + skip_build=1 + ;; + --skip-precompile) + skip_precompile=1 + ;; + --label) + shift + [ "$#" -gt 0 ] || { echo "--label requires a value" >&2; exit 2; } + case "$1" in + *[!A-Za-z0-9._-]*|"") + echo "--label may only contain letters, numbers, '.', '_', and '-'" >&2 + exit 2 + ;; + esac + run_label="$1" + ;; + --postgres-guc) + shift + [ "$#" -gt 0 ] || { echo "--postgres-guc requires a name=value setting" >&2; exit 2; } + postgres_gucs+=("$1") + ;; + --wasmer-arg) + shift + [ "$#" -gt 0 ] || { echo "--wasmer-arg requires one wasmer argument" >&2; exit 2; } + wasmer_extra_args+=("$1") + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +if [ -n "${POSTGRES_GUCS:-}" ]; then + for guc in $POSTGRES_GUCS; do + postgres_gucs+=("$guc") + done +fi +if [ -n "${WASMER_RUN_EXTRA_ARGS:-}" ]; then + for arg in $WASMER_RUN_EXTRA_ARGS; do + wasmer_extra_args+=("$arg") + done +fi + +fresh_ensure_dirs + +workload_dir="$FRESH_ROOT/bench/sql/query-perf" +workload_path_for() { + case "$1" in + */*) printf '%s\n' "$1" ;; + *.sql) printf '%s/%s\n' "$workload_dir" "$1" ;; + *) printf '%s/%s.sql\n' "$workload_dir" "$1" ;; + esac +} +workload_name_for() { + basename "${1%.sql}" +} +now_ms() { + if command -v perl >/dev/null 2>&1; then + perl -MTime::HiRes=time -e 'printf "%.0f\n", time() * 1000' + else + printf '%s000\n' "$(date +%s)" + fi +} +extract_psql_time_sum_ms() { + awk ' + /^Time: [0-9.]+ ms/ { + sum += $2 + count += 1 + } + END { + if (count == 0) { + printf "" + } else { + printf "%.3f", sum + } + } + ' "$1" +} + +if [ -z "$run_label" ]; then + run_label="$(date -u +%Y%m%dT%H%M%SZ)-$profile-$(workload_name_for "$workload")" +fi +if [ -z "$initdb_profile" ]; then + initdb_profile="$profile" +fi + +install_dir="$(fresh_wasix_core_install_dir_for "$profile")" +initdb_install_dir="$(fresh_wasix_core_install_dir_for "$initdb_profile")" +run_root="$FRESH_WORK_ROOT/run/query-profiles/$run_label" +report_root="$FRESH_WORK_ROOT/reports/query-profiles/$run_label" +pgdata="$run_root/pgdata" +dev_shm="$run_root/dev-shm" +sql_path="$(workload_path_for "$workload")" +workload_name="$(workload_name_for "$workload")" +setup_sql_path="" +setup_workload_name="" +if [ -n "$setup_workload" ]; then + setup_sql_path="$(workload_path_for "$setup_workload")" + setup_workload_name="$(workload_name_for "$setup_workload")" +fi + +mkdir -p "$run_root" "$report_root" +rm -rf "$pgdata" "$dev_shm" +mkdir -p "$dev_shm" + +if [ ! -f "$sql_path" ]; then + printf 'missing workload SQL: %s\n' "$sql_path" >&2 + exit 2 +fi +if [ -n "$setup_sql_path" ] && [ ! -f "$setup_sql_path" ]; then + printf 'missing setup workload SQL: %s\n' "$setup_sql_path" >&2 + exit 2 +fi + +if [ "$skip_build" -ne 1 ]; then + WASIX_CORE_PROFILE="$profile" \ + WASIX_BUILD_DIR="$(fresh_wasix_core_build_dir_for "$profile")" \ + WASIX_INSTALL_DIR="$install_dir" \ + REPORT_DIR="$(fresh_wasix_core_report_dir_for "$profile")" \ + RUN_DIR="$(fresh_wasix_core_run_dir_for "$profile")" \ + "$FRESH_ROOT/bin/build-wasix-core.sh" >"$report_root/build.log" 2>&1 + if [ "$initdb_profile" != "$profile" ]; then + WASIX_CORE_PROFILE="$initdb_profile" \ + WASIX_BUILD_DIR="$(fresh_wasix_core_build_dir_for "$initdb_profile")" \ + WASIX_INSTALL_DIR="$initdb_install_dir" \ + REPORT_DIR="$(fresh_wasix_core_report_dir_for "$initdb_profile")" \ + RUN_DIR="$(fresh_wasix_core_run_dir_for "$initdb_profile")" \ + "$FRESH_ROOT/bin/build-wasix-core.sh" >"$report_root/initdb-build.log" 2>&1 + fi +elif [ ! -x "$install_dir/bin/postgres" ]; then + printf 'missing %s/bin/postgres with --skip-build\n' "$install_dir" >&2 + exit 2 +fi +if [ ! -x "$initdb_install_dir/bin/initdb" ]; then + printf 'missing %s/bin/initdb\n' "$initdb_install_dir" >&2 + exit 2 +fi + +if [ "$skip_precompile" -ne 1 ]; then + WASIX_CORE_PROFILE="$profile" \ + WASIX_BUILD_DIR="$(fresh_wasix_core_build_dir_for "$profile")" \ + WASIX_INSTALL_DIR="$install_dir" \ + REPORT_DIR="$(fresh_wasix_core_report_dir_for "$profile")" \ + RUN_DIR="$(fresh_wasix_core_run_dir_for "$profile")" \ + WASIX_PRECOMPILE_SCOPE="$precompile_scope" \ + "$FRESH_ROOT/bin/precompile-wasix-core.sh" >"$report_root/precompile.log" 2>&1 +else + printf 'precompile skipped\n' >"$report_root/precompile.log" +fi + +wasmer_bin="$(fresh_wasmer_bin)" +wasmer_bin_hash="$(fresh_wasmer_bin_hash "$wasmer_bin")" +wasmer_cache_dir="$(fresh_wasmer_cache_dir "$wasmer_bin")" +wasmer_compiler="$(fresh_wasmer_compiler)" +wasmer_llvm_opt_level="${WASMER_LLVM_OPT_LEVEL:-aggressive}" +wasmer_stack_size="${WASMER_STACK_SIZE:-33554432}" +wasmer_compiler_threads="${WASMER_COMPILER_THREADS:-$(fresh_jobs)}" +fresh_require_wasmer_compiler_cli "$wasmer_bin" "$wasmer_compiler" run + +wasmer_env=( + "WASMER_DIR=$FRESH_WORK_ROOT/tools/wasmer-home" + "WASMER_CACHE_DIR=$wasmer_cache_dir" +) +wasmer_args=( + run + --quiet +) +while IFS= read -r arg; do + wasmer_args+=("$arg") +done < <(fresh_wasmer_compiler_args_for "$wasmer_bin" run "$wasmer_compiler" "$wasmer_llvm_opt_level" "$wasmer_compiler_threads") +wasmer_args+=( + --profiler perfmap + --stack-size "$wasmer_stack_size" + --enable-exceptions + --enable-threads + --net + --volume "$REPO_ROOT:$REPO_ROOT" + --volume "$dev_shm:/dev/shm" +) +if [ "${#wasmer_extra_args[@]}" -gt 0 ]; then + wasmer_args+=("${wasmer_extra_args[@]}") +fi + +summary="$report_root/summary.md" +initdb_log="$report_root/initdb.log" +server_log="$report_root/server.log" +wait_log="$report_root/wait.log" +psql_log="$report_root/$workload_name.log" +setup_log="$report_root/${setup_workload_name:-setup}.setup.log" +sample_log="$report_root/sample.txt" +sample_stderr="$report_root/sample.stderr.log" +perfmap_copy="$report_root/perf.map" +symbolized_sample="$report_root/symbolized-sample.txt" +symbolized_top="$report_root/symbolized-sample.top.tsv" + +{ + printf '# WASIX Query Profile\n\n' + printf -- '- Generated: `%s`\n' "$(fresh_timestamp)" + printf -- '- Profile: `%s`\n' "$profile" + printf -- '- Initdb profile: `%s`\n' "$initdb_profile" + printf -- '- Workload: `%s`\n' "$workload_name" + printf -- '- Workload SQL: `%s`\n' "$sql_path" + printf -- '- Setup workload: `%s`\n' "${setup_workload_name:-}" + printf -- '- Setup workload SQL: `%s`\n' "${setup_sql_path:-}" + printf -- '- Rows: `%s`\n' "$profile_rows" + printf -- '- Transaction rows: `%s`\n' "$transaction_rows" + printf -- '- Port: `%s`\n' "$start_port" + printf -- '- Sample seconds: `%s`\n' "$sample_seconds" + printf -- '- Sample delay: `%s`\n' "$sample_delay" + printf -- '- Wasmer binary: `%s`\n' "$wasmer_bin" + printf -- '- Wasmer binary hash: `%s`\n' "$wasmer_bin_hash" + printf -- '- Wasmer compiler: `%s`\n' "$wasmer_compiler" + printf -- '- Wasmer LLVM opt level: `%s`\n' "$wasmer_llvm_opt_level" + printf -- '- WASMER_LLVM_NATIVE_CPU: `%s`\n' "${WASMER_LLVM_NATIVE_CPU:-0}" + printf -- '- WASMER_LLVM_FULL_O3_PIPELINE: `%s`\n' "${WASMER_LLVM_FULL_O3_PIPELINE:-0}" + printf -- '- WASMER_LLVM_INDIRECT_CALL_CACHE: `%s`\n' "${WASMER_LLVM_INDIRECT_CALL_CACHE:-0}" + printf -- '- Wasmer cache dir: `%s`\n' "$wasmer_cache_dir" + printf -- '- PostgreSQL GUCs: `%s`\n' "${postgres_gucs[*]:-}" + printf -- '- Extra Wasmer args: `%s`\n\n' "${wasmer_extra_args[*]:-}" +} >"$summary" + +env "${wasmer_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" \ + --volume "$initdb_install_dir/lib:/lib" \ + "$initdb_install_dir/bin/initdb" -- \ + -D "$pgdata" \ + -A trust \ + --no-locale \ + --encoding=UTF8 \ + --no-instructions \ + >"$initdb_log" 2>&1 + +postgres_args=( + -D "$pgdata" + -h 127.0.0.1 + -p "$start_port" + -c unix_socket_directories= +) +if [ "${#postgres_gucs[@]}" -gt 0 ]; then + for guc in "${postgres_gucs[@]}"; do + postgres_args+=(-c "$guc") + done +fi + +set +e +env "${wasmer_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" \ + --volume "$install_dir/lib:/lib" \ + "$install_dir/bin/postgres" -- \ + "${postgres_args[@]}" >"$server_log" 2>&1 & +server_pid=$! +set -e + +cleanup() { + if kill -0 "$server_pid" 2>/dev/null; then + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi +} +trap cleanup EXIT + +: >"$wait_log" +ready=0 +conn="postgresql://wasix@127.0.0.1:$start_port/postgres" +for _ in $(seq 1 300); do + if "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -X -q -c 'select 1' >>"$wait_log" 2>&1; then + ready=1 + break + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + echo "WASIX server exited before readiness" >>"$wait_log" + break + fi + sleep 0.1 +done +if [ "$ready" -ne 1 ]; then + printf 'WASIX server did not become ready; see %s\n' "$wait_log" >&2 + exit 1 +fi + +if [ -n "$setup_sql_path" ]; then + "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" \ + -X -q \ + -v "perf_rows=$profile_rows" \ + -v "update_rows=$profile_rows" \ + -v "transaction_rows=$transaction_rows" \ + -f "$setup_sql_path" >"$setup_log" 2>&1 +fi + +started_ms="$(now_ms)" +set +e +"$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" \ + -X -q \ + -v "perf_rows=$profile_rows" \ + -v "update_rows=$profile_rows" \ + -v "transaction_rows=$transaction_rows" \ + -f "$sql_path" >"$psql_log" 2>&1 & +psql_pid=$! +set -e + +sleep "$sample_delay" +sample_status=0 +if command -v sample >/dev/null 2>&1 && kill -0 "$psql_pid" 2>/dev/null; then + set +e + sample "$server_pid" "$sample_seconds" -file "$sample_log" >"$sample_stderr" 2>&1 + sample_status=$? + set -e +else + sample_status=127 + printf 'sample command unavailable or workload finished before sampling\n' >"$sample_stderr" +fi + +set +e +wait "$psql_pid" +psql_status=$? +set -e +ended_ms="$(now_ms)" +wall_ms=$((ended_ms - started_ms)) +psql_time_ms="$(extract_psql_time_sum_ms "$psql_log")" + +perfmap="/tmp/perf-$server_pid.map" +if [ -s "$perfmap" ]; then + cp "$perfmap" "$perfmap_copy" +fi + +if [ -s "$sample_log" ] && [ -s "$perfmap_copy" ]; then + "$FRESH_ROOT/bin/symbolize-wasmer-sample.sh" \ + "$sample_log" \ + "$perfmap_copy" \ + "$report_root/symbolized-sample" \ + >"$report_root/symbolize.log" 2>&1 || true +fi + +{ + printf '\n## Result\n\n' + printf -- '- Server host PID: `%s`\n' "$server_pid" + printf -- '- PSQL exit code: `%s`\n' "$psql_status" + printf -- '- Sample exit code: `%s`\n' "$sample_status" + printf -- '- Workload wall ms: `%s`\n' "$wall_ms" + printf -- '- PSQL timed ms: `%s`\n' "$psql_time_ms" + if [ -n "$setup_sql_path" ]; then + printf -- '- Setup log: `%s`\n' "$setup_log" + fi + printf -- '- PSQL log: `%s`\n' "$psql_log" + printf -- '- Sample log: `%s`\n' "$sample_log" + printf -- '- Sample stderr: `%s`\n' "$sample_stderr" + if [ -s "$perfmap_copy" ]; then + printf -- '- Perf map: `%s`\n' "$perfmap_copy" + else + printf -- '- Perf map: `missing`\n' + fi + if [ -s "$symbolized_sample" ]; then + printf -- '- Symbolized sample: `%s`\n' "$symbolized_sample" + printf -- '- Symbolized top TSV: `%s`\n' "$symbolized_top" + else + printf -- '- Symbolized sample: `missing`\n' + fi +} >>"$summary" + +printf 'profile written to %s\n' "$summary" +exit "$psql_status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/prove-linux-cold-residency.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/prove-linux-cold-residency.py new file mode 100755 index 00000000..5e74e061 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/prove-linux-cold-residency.py @@ -0,0 +1,1087 @@ +#!/usr/bin/env python3 +"""Evict and prove cold regular-file pages for an exact set of directory roots. + +This is deliberately a targeted operation. It never uses drop_caches: every +regular file is content-addressed, synchronized, advised DONTNEED, and then +checked with mincore(2). The caller must launch the measured process without +reading either root after this program returns. +""" + +from __future__ import annotations + +import argparse +import contextlib +import ctypes +import datetime as dt +import hashlib +import json +import os +import platform +import stat +import sys +import time +from collections.abc import Iterator +from dataclasses import dataclass, field +from pathlib import Path +from typing import NoReturn + + +SCHEMA = "oliphaunt.wasix-postmaster.cold-residency.v1" +MAP_SHARED = 0x01 +PROT_NONE = 0x0 +O_CLOEXEC = getattr(os, "O_CLOEXEC", 0) +O_NOFOLLOW = getattr(os, "O_NOFOLLOW", 0) +O_DIRECTORY = getattr(os, "O_DIRECTORY", 0) +DIRECTORY_OPEN_FLAGS = os.O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_DIRECTORY +REGULAR_OPEN_FLAGS = os.O_RDONLY | O_CLOEXEC | O_NOFOLLOW + + +class ProofError(RuntimeError): + pass + + +def fail(message: str) -> NoReturn: + raise ProofError(message) + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb", buffering=0) as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def utc_now() -> str: + return dt.datetime.now(dt.timezone.utc).isoformat(timespec="microseconds") + + +def parse_assignment(value: str, label: str) -> tuple[str, str]: + if "=" not in value: + fail(f"{label} requires NAME=VALUE: {value}") + name, assigned = value.split("=", 1) + if not name or any(character not in "abcdefghijklmnopqrstuvwxyz0123456789_-" for character in name): + fail(f"invalid {label} name: {name}") + if not assigned or any(character in assigned for character in "\t\r\n\0"): + fail(f"invalid {label} value for {name}") + return name, assigned + + +@dataclass +class FileObject: + device: int + inode: int + size: int + mode: int + mtime_ns: int + ctime_ns: int + link_count: int + sha256: str = "" + resident_before_pages: int = 0 + resident_after_pages: int = 0 + paths: list[tuple[str, str]] = field(default_factory=list) + + @property + def page_count(self) -> int: + if self.size == 0: + return 0 + return (self.size + PAGE_SIZE - 1) // PAGE_SIZE + + +@dataclass +class RootRecord: + role: str + path: str + require_read_only: bool + fd: int + identity: "InventoryEntry" + inventory: tuple["InventoryEntry", ...] = () + inventory_by_path: dict[str, "InventoryEntry"] = field(default_factory=dict) + directories: list[tuple[str, int]] = field(default_factory=list) + regular_paths: list[tuple[str, FileObject]] = field(default_factory=list) + + +@dataclass(frozen=True, order=True) +class InventoryEntry: + relative: str + kind: str + device: int + inode: int + mode: int + size: int + mtime_ns: int + ctime_ns: int + link_count: int + + @classmethod + def from_stat( + cls, relative: str, kind: str, current: os.stat_result + ) -> "InventoryEntry": + return cls( + relative=relative, + kind=kind, + device=current.st_dev, + inode=current.st_ino, + mode=stat.S_IMODE(current.st_mode), + size=current.st_size, + mtime_ns=current.st_mtime_ns, + ctime_ns=current.st_ctime_ns, + link_count=current.st_nlink, + ) + + def diagnostic(self) -> dict[str, object]: + return { + "relative": self.relative, + "kind": self.kind, + "device": self.device, + "inode": self.inode, + "mode": f"{self.mode:o}", + "size": self.size, + "mtime_ns": self.mtime_ns, + "ctime_ns": self.ctime_ns, + "link_count": self.link_count, + } + + +@dataclass +class OutputParent: + path: str + name: str + fd: int + identity: InventoryEntry + original_entry: InventoryEntry | None + + +@dataclass +class ColdFileDescriptorBudget: + """Make the proof's O(1) cold-file descriptor bound executable evidence.""" + + limit: int = 1 + current: int = 0 + peak: int = 0 + total_opens: int = 0 + + def acquired(self) -> None: + if self.current >= self.limit: + fail( + "cold-file descriptor bound exceeded: " + f"current={self.current + 1} limit={self.limit}" + ) + self.current += 1 + self.total_opens += 1 + self.peak = max(self.peak, self.current) + + def released(self) -> None: + if self.current <= 0: + fail("cold-file descriptor accounting underflow") + self.current -= 1 + + +libc = ctypes.CDLL(None, use_errno=True) +libc.mmap.argtypes = [ + ctypes.c_void_p, + ctypes.c_size_t, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ctypes.c_longlong, +] +libc.mmap.restype = ctypes.c_void_p +libc.mincore.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_ubyte)] +libc.mincore.restype = ctypes.c_int +libc.munmap.argtypes = [ctypes.c_void_p, ctypes.c_size_t] +libc.munmap.restype = ctypes.c_int + +PAGE_SIZE = os.sysconf("SC_PAGE_SIZE") +MAP_FAILED = ctypes.c_void_p(-1).value + + +def mincore_resident_pages(fd: int, size: int) -> int: + if size == 0: + return 0 + pages = (size + PAGE_SIZE - 1) // PAGE_SIZE + address = libc.mmap(None, size, PROT_NONE, MAP_SHARED, fd, 0) + if address == MAP_FAILED: + error = ctypes.get_errno() + raise OSError(error, os.strerror(error)) + vector = (ctypes.c_ubyte * pages)() + try: + if libc.mincore(address, size, vector) != 0: + error = ctypes.get_errno() + raise OSError(error, os.strerror(error)) + return sum(1 for value in vector if value & 1) + finally: + if libc.munmap(address, size) != 0: + error = ctypes.get_errno() + raise OSError(error, os.strerror(error)) + + +def stat_matches_entry(current: os.stat_result, expected: InventoryEntry) -> bool: + kind_matches = ( + expected.kind == "directory" and stat.S_ISDIR(current.st_mode) + ) or (expected.kind == "file" and stat.S_ISREG(current.st_mode)) + return ( + kind_matches + and current.st_dev == expected.device + and current.st_ino == expected.inode + and stat.S_IMODE(current.st_mode) == expected.mode + and current.st_size == expected.size + and current.st_mtime_ns == expected.mtime_ns + and current.st_ctime_ns == expected.ctime_ns + and current.st_nlink == expected.link_count + ) + + +def stat_matches_object(current: os.stat_result, expected: InventoryEntry) -> bool: + return ( + ((expected.kind == "directory" and stat.S_ISDIR(current.st_mode)) + or (expected.kind == "file" and stat.S_ISREG(current.st_mode))) + and current.st_dev == expected.device + and current.st_ino == expected.inode + ) + + +def open_canonical_directory(path: Path) -> int: + """Open every canonical absolute component with O_NOFOLLOW.""" + + if not path.is_absolute(): + fail(f"directory path must be absolute: {path}") + canonical = Path(os.path.realpath(path)) + if canonical != path: + fail(f"directory path must already be canonical: supplied={path} canonical={canonical}") + current_fd = os.open("/", DIRECTORY_OPEN_FLAGS) + try: + for component in path.parts[1:]: + if component in ("", ".", "..") or "/" in component: + fail(f"invalid canonical directory component in {path}: {component}") + next_fd = os.open(component, DIRECTORY_OPEN_FLAGS, dir_fd=current_fd) + os.close(current_fd) + current_fd = next_fd + return current_fd + except BaseException: + os.close(current_fd) + raise + + +def pin_root(root: Path, role: str, require_read_only: bool) -> RootRecord: + if not root.is_absolute(): + fail(f"root must be absolute: {root}") + before = os.lstat(root) + if stat.S_ISLNK(before.st_mode) or not stat.S_ISDIR(before.st_mode): + fail(f"root must be a non-symlink directory: {root}") + fd = open_canonical_directory(root) + opened = os.fstat(fd) + identity = InventoryEntry.from_stat(".", "directory", opened) + if not stat_matches_entry(before, identity): + os.close(fd) + fail(f"root changed while it was pinned: {root}") + if require_read_only and identity.mode != 0o555: + os.close(fd) + fail(f"read-only root mode must be 0555: {root}") + return RootRecord( + role=role, + path=str(root), + require_read_only=require_read_only, + fd=fd, + identity=identity, + ) + + +def pin_output_parent(output: Path) -> OutputParent: + if not output.is_absolute(): + fail(f"receipt output must be absolute: {output}") + if output.name in ("", ".", ".."): + fail(f"invalid receipt output name: {output}") + parent = output.parent + fd = open_canonical_directory(parent) + identity = InventoryEntry.from_stat(".", "directory", os.fstat(fd)) + try: + current = os.stat(output.name, dir_fd=fd, follow_symlinks=False) + except FileNotFoundError: + original_entry = None + else: + if stat.S_ISLNK(current.st_mode): + os.close(fd) + fail(f"receipt output must not be a symlink: {output}") + if not stat.S_ISREG(current.st_mode): + os.close(fd) + fail(f"existing receipt output must be a regular file: {output}") + os.close(fd) + fail(f"receipt output must not already exist: {output}") + return OutputParent( + path=str(parent), + name=output.name, + fd=fd, + identity=identity, + original_entry=original_entry, + ) + + +def require_pinned_directory( + path: str, + fd: int, + expected: InventoryEntry, + label: str, + *, + exact_metadata: bool = True, +) -> None: + comparator = stat_matches_entry if exact_metadata else stat_matches_object + if not comparator(os.fstat(fd), expected): + fail(f"pinned {label} descriptor changed identity: {path}") + reopened = open_canonical_directory(Path(path)) + try: + if not comparator(os.fstat(reopened), expected): + fail(f"{label} pathname no longer resolves to its pinned identity: {path}") + finally: + os.close(reopened) + + +def require_output_entry_stable(output_parent: OutputParent) -> None: + try: + current = os.stat( + output_parent.name, + dir_fd=output_parent.fd, + follow_symlinks=False, + ) + except FileNotFoundError: + if output_parent.original_entry is not None: + fail("existing receipt output disappeared before publication") + return + if output_parent.original_entry is None: + fail("receipt output appeared concurrently before publication") + if not stat_matches_entry(current, output_parent.original_entry): + fail("existing receipt output changed before publication") + + +@contextlib.contextmanager +def open_cold_file( + name: str, + parent_fd: int, + descriptor_budget: ColdFileDescriptorBudget, +) -> Iterator[int]: + descriptor_budget.acquired() + try: + fd = os.open(name, REGULAR_OPEN_FLAGS, dir_fd=parent_fd) + except BaseException: + descriptor_budget.released() + raise + try: + yield fd + finally: + try: + os.close(fd) + finally: + descriptor_budget.released() + + +def metadata_matches(file_object: FileObject, current: os.stat_result) -> bool: + return ( + stat.S_ISREG(current.st_mode) + and current.st_dev == file_object.device + and current.st_ino == file_object.inode + and current.st_size == file_object.size + and stat.S_IMODE(current.st_mode) == file_object.mode + and current.st_mtime_ns == file_object.mtime_ns + and current.st_ctime_ns == file_object.ctime_ns + and current.st_nlink == file_object.link_count + ) + + +@contextlib.contextmanager +def open_parent_beneath( + root: RootRecord, relative: str +) -> Iterator[tuple[int, str]]: + components = relative.split("/") + if ( + relative.startswith("/") + or len(components) == 0 + or any(component in ("", ".", "..") or "/" in component for component in components) + ): + fail(f"invalid recorded relative path below {root.path}: {relative}") + current_fd = root.fd + owned_fd = False + traversed: list[str] = [] + try: + for component in components[:-1]: + traversed.append(component) + directory_relative = "/".join(traversed) + expected = root.inventory_by_path.get(directory_relative) + if expected is None or expected.kind != "directory": + fail(f"recorded parent directory disappeared: {root.path}/{directory_relative}") + next_fd = os.open(component, DIRECTORY_OPEN_FLAGS, dir_fd=current_fd) + if not stat_matches_entry(os.fstat(next_fd), expected): + os.close(next_fd) + fail(f"parent directory changed while resolving: {root.path}/{directory_relative}") + if owned_fd: + os.close(current_fd) + current_fd = next_fd + owned_fd = True + yield current_fd, components[-1] + finally: + if owned_fd: + os.close(current_fd) + + +def require_path_matches( + root: RootRecord, + relative: str, + file_object: FileObject, + phase: str, +) -> None: + with open_parent_beneath(root, relative) as (parent_fd, name): + current = os.stat(name, dir_fd=parent_fd, follow_symlinks=False) + if stat.S_ISLNK(current.st_mode) or not metadata_matches(file_object, current): + fail(f"file path changed during {phase}: {root.path}/{relative}") + + +@contextlib.contextmanager +def reopen_stable_file( + file_object: FileObject, + roots_by_role: dict[str, RootRecord], + descriptor_budget: ColdFileDescriptorBudget, + phase: str, +) -> Iterator[int]: + """Reopen one recorded inode and reject pathname or in-operation races.""" + + if not file_object.paths: + fail( + "recorded file object has no path: " + f"dev={file_object.device} ino={file_object.inode}" + ) + role, relative = min(file_object.paths) + root = roots_by_role[role] + require_path_matches(root, relative, file_object, phase) + with open_parent_beneath(root, relative) as (parent_fd, name): + with open_cold_file(name, parent_fd, descriptor_budget) as fd: + if not metadata_matches(file_object, os.fstat(fd)): + fail(f"file changed while reopening for {phase}: {root.path}/{relative}") + yield fd + if not metadata_matches(file_object, os.fstat(fd)): + fail(f"file changed during {phase}: {root.path}/{relative}") + require_path_matches(root, relative, file_object, phase) + + +def scan_directory( + root: RootRecord, + directory_fd: int, + relative_directory: str, + objects: dict[tuple[int, int], FileObject] | None, + descriptor_budget: ColdFileDescriptorBudget, + inventory: list[InventoryEntry], +) -> None: + directory_before = os.fstat(directory_fd) + directory_entry = InventoryEntry.from_stat( + relative_directory, "directory", directory_before + ) + if root.require_read_only and directory_entry.mode != 0o555: + fail( + "carrier directory mode must remain 0555: " + f"{root.path}/{relative_directory}" + ) + inventory.append(directory_entry) + if objects is not None: + root.directories.append((relative_directory, directory_entry.mode)) + + names = sorted(os.listdir(directory_fd)) + for name in names: + if name in ("", ".", "..") or "/" in name: + fail(f"invalid directory entry below {root.path}: {name}") + relative = name if relative_directory == "." else f"{relative_directory}/{name}" + before = os.stat(name, dir_fd=directory_fd, follow_symlinks=False) + if stat.S_ISDIR(before.st_mode): + child_fd = os.open(name, DIRECTORY_OPEN_FLAGS, dir_fd=directory_fd) + try: + child_opened = os.fstat(child_fd) + child_entry = InventoryEntry.from_stat(relative, "directory", child_opened) + if not stat_matches_entry(before, child_entry): + fail(f"directory changed while it was opened: {root.path}/{relative}") + scan_directory( + root, + child_fd, + relative, + objects, + descriptor_budget, + inventory, + ) + finally: + os.close(child_fd) + elif stat.S_ISREG(before.st_mode): + mode = stat.S_IMODE(before.st_mode) + if root.require_read_only and mode not in (0o444, 0o555): + fail(f"carrier file mode must remain 0444 or 0555: {root.path}/{relative}") + with open_cold_file(name, directory_fd, descriptor_budget) as fd: + opened = os.fstat(fd) + file_entry = InventoryEntry.from_stat(relative, "file", opened) + if not stat_matches_entry(before, file_entry): + fail(f"file changed while it was opened: {root.path}/{relative}") + inventory.append(file_entry) + if objects is not None: + identity = (opened.st_dev, opened.st_ino) + existing = objects.get(identity) + if existing is None: + existing = FileObject( + device=opened.st_dev, + inode=opened.st_ino, + size=opened.st_size, + mode=mode, + mtime_ns=opened.st_mtime_ns, + ctime_ns=opened.st_ctime_ns, + link_count=opened.st_nlink, + ) + objects[identity] = existing + elif not metadata_matches(existing, opened): + fail(f"hard-linked file metadata is inconsistent: {root.path}/{relative}") + existing.paths.append((root.role, relative)) + root.regular_paths.append((relative, existing)) + else: + fail(f"tree contains a symlink or special entry: {root.path}/{relative}") + + directory_after = os.fstat(directory_fd) + if not stat_matches_entry(directory_after, directory_entry): + fail(f"directory changed while inventorying: {root.path}/{relative_directory}") + + +def capture_root_inventory( + root: RootRecord, + objects: dict[tuple[int, int], FileObject] | None, + descriptor_budget: ColdFileDescriptorBudget, +) -> tuple[InventoryEntry, ...]: + require_pinned_directory(root.path, root.fd, root.identity, f"root {root.role}") + if objects is not None: + root.directories.clear() + root.regular_paths.clear() + inventory: list[InventoryEntry] = [] + scan_directory(root, root.fd, ".", objects, descriptor_budget, inventory) + require_pinned_directory(root.path, root.fd, root.identity, f"root {root.role}") + return tuple(sorted(inventory)) + + +def inventory_difference( + expected: tuple[InventoryEntry, ...], actual: tuple[InventoryEntry, ...] +) -> dict[str, object]: + for index in range(max(len(expected), len(actual))): + left = expected[index] if index < len(expected) else None + right = actual[index] if index < len(actual) else None + if left != right: + return { + "index": index, + "expected": left.diagnostic() if left is not None else None, + "actual": right.diagnostic() if right is not None else None, + } + return {"index": -1, "expected": None, "actual": None} + + +def require_exact_inventories( + roots: list[RootRecord], descriptor_budget: ColdFileDescriptorBudget, phase: str +) -> None: + for root in roots: + actual = capture_root_inventory(root, None, descriptor_budget) + if actual != root.inventory: + fail( + f"exact root inventory changed during {phase}: role={root.role} " + + json.dumps(inventory_difference(root.inventory, actual), sort_keys=True) + ) + + +def require_output_isolated( + output: Path, + output_parent: OutputParent, + roots: list[RootRecord], + objects: dict[tuple[int, int], FileObject], +) -> None: + parent_identity = (output_parent.identity.device, output_parent.identity.inode) + for root in roots: + root_path = Path(root.path) + if output == root_path or root_path in output.parents: + fail(f"receipt output must be outside every cold root: {output}") + directory_identities = { + (entry.device, entry.inode) + for entry in root.inventory + if entry.kind == "directory" + } + if parent_identity in directory_identities: + fail( + "receipt output parent aliases a directory inside a cold root: " + f"output_parent={output_parent.path} role={root.role}" + ) + if output_parent.original_entry is not None: + output_identity = ( + output_parent.original_entry.device, + output_parent.original_entry.inode, + ) + if output_identity in objects: + fail("existing receipt output hard-links a cold-root file") + + +def require_root_isolation(roots: list[RootRecord]) -> None: + for left_index, left in enumerate(roots): + left_path = Path(left.path) + left_identity = (left.identity.device, left.identity.inode) + for right in roots[left_index + 1 :]: + right_path = Path(right.path) + if ( + left_path == right_path + or left_path in right_path.parents + or right_path in left_path.parents + ): + fail(f"cold roots must be disjoint: {left_path} and {right_path}") + right_directory_identities = { + (entry.device, entry.inode) + for entry in right.inventory + if entry.kind == "directory" + } + left_directory_identities = { + (entry.device, entry.inode) + for entry in left.inventory + if entry.kind == "directory" + } + right_identity = (right.identity.device, right.identity.inode) + if ( + left_identity in right_directory_identities + or right_identity in left_directory_identities + ): + fail( + "cold roots alias or overlap by directory identity: " + f"{left_path} and {right_path}" + ) + + +def require_complete_hardlink_topology( + unique_objects: list[FileObject], +) -> list[dict[str, object]]: + internal_hardlinks: list[dict[str, object]] = [] + for file_object in unique_objects: + roles = {role for role, _ in file_object.paths} + if len(roles) != 1: + fail( + "cold roots share one hard-linked inode, violating immutable/mutable isolation: " + + json.dumps(file_object.paths, sort_keys=True) + ) + observed_aliases = len(file_object.paths) + if file_object.link_count != observed_aliases: + fail( + "cold file has external or otherwise unobserved hard-link aliases: " + + json.dumps( + { + "device": file_object.device, + "inode": file_object.inode, + "st_nlink": file_object.link_count, + "observed_aliases": observed_aliases, + "paths": file_object.paths, + }, + sort_keys=True, + ) + ) + if observed_aliases > 1: + internal_hardlinks.append( + { + "device": file_object.device, + "inode": file_object.inode, + "link_count": file_object.link_count, + "paths": sorted(file_object.paths), + } + ) + return internal_hardlinks + + +def publish_receipt(receipt: dict[str, object], output_parent: OutputParent) -> None: + require_pinned_directory( + output_parent.path, + output_parent.fd, + output_parent.identity, + "receipt output parent", + exact_metadata=False, + ) + require_output_entry_stable(output_parent) + pending_name = f".{output_parent.name}.pending.{os.getpid()}" + pending_created = False + try: + pending_fd = os.open( + pending_name, + os.O_WRONLY | os.O_CREAT | os.O_EXCL | O_CLOEXEC | O_NOFOLLOW, + 0o666, + dir_fd=output_parent.fd, + ) + pending_created = True + with os.fdopen(pending_fd, "w", encoding="utf-8", newline="\n") as stream: + json.dump(receipt, stream, indent=2, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + written_entry = InventoryEntry.from_stat( + output_parent.name, "file", os.fstat(stream.fileno()) + ) + require_output_entry_stable(output_parent) + os.link( + pending_name, + output_parent.name, + src_dir_fd=output_parent.fd, + dst_dir_fd=output_parent.fd, + follow_symlinks=False, + ) + os.unlink(pending_name, dir_fd=output_parent.fd) + pending_created = False + published = os.stat( + output_parent.name, + dir_fd=output_parent.fd, + follow_symlinks=False, + ) + if ( + not stat_matches_object(published, written_entry) + or stat.S_IMODE(published.st_mode) != written_entry.mode + or published.st_size != written_entry.size + or published.st_mtime_ns != written_entry.mtime_ns + or published.st_nlink != 1 + ): + fail("published receipt identity differs from the fsynced pending file") + os.fsync(output_parent.fd) + require_pinned_directory( + output_parent.path, + output_parent.fd, + output_parent.identity, + "receipt output parent", + exact_metadata=False, + ) + finally: + if pending_created: + try: + os.unlink(pending_name, dir_fd=output_parent.fd) + except FileNotFoundError: + pass + + +def hash_open_file(fd: int, file_object: FileObject) -> str: + digest = hashlib.sha256() + offset = 0 + while offset < file_object.size: + chunk = os.pread(fd, min(1024 * 1024, file_object.size - offset), offset) + if not chunk: + fail(f"short read while hashing dev={file_object.device} ino={file_object.inode}") + digest.update(chunk) + offset += len(chunk) + return digest.hexdigest() + + +def root_receipt(root: RootRecord) -> dict[str, object]: + content_digest = hashlib.sha256() + metadata_digest = hashlib.sha256() + inventory_digest = hashlib.sha256() + object_keys: set[tuple[int, int]] = set() + for relative, mode in sorted(root.directories): + content_digest.update(f"d\0{relative}\0{mode:o}\0".encode()) + metadata_digest.update(f"d\0{relative}\0{mode:o}\0".encode()) + for relative, file_object in sorted(root.regular_paths, key=lambda item: item[0]): + object_keys.add((file_object.device, file_object.inode)) + content_digest.update( + f"f\0{relative}\0{file_object.mode:o}\0{file_object.size}\0{file_object.sha256}\0".encode() + ) + metadata_digest.update( + ( + f"f\0{relative}\0{file_object.device}\0{file_object.inode}\0" + f"{file_object.mode:o}\0{file_object.size}\0{file_object.mtime_ns}\0" + f"{file_object.ctime_ns}\0{file_object.link_count}\0{file_object.sha256}\0" + ).encode() + ) + for entry in root.inventory: + inventory_digest.update( + ( + f"{entry.kind}\0{entry.relative}\0{entry.device}\0{entry.inode}\0" + f"{entry.mode:o}\0{entry.size}\0{entry.mtime_ns}\0{entry.ctime_ns}\0" + f"{entry.link_count}\0" + ).encode() + ) + root_objects = { + (entry.device, entry.inode): entry for _, entry in root.regular_paths + } + unique = [entry for _, entry in sorted(root_objects.items())] + return { + "role": root.role, + "path": root.path, + "require_read_only": root.require_read_only, + "directory_count": len(root.directories), + "regular_path_count": len(root.regular_paths), + "unique_file_count": len(object_keys), + "root_device": root.identity.device, + "root_inode": root.identity.inode, + "hard_link_alias_count": sum( + max(0, file_object.link_count - 1) for file_object in unique + ), + "hard_linked_unique_file_count": sum( + 1 for file_object in unique if file_object.link_count > 1 + ), + "logical_bytes": sum(file_object.size for file_object in unique), + "page_count": sum(file_object.page_count for file_object in unique), + "resident_before_pages": sum(file_object.resident_before_pages for file_object in unique), + "resident_after_pages": sum(file_object.resident_after_pages for file_object in unique), + "content_sha256": content_digest.hexdigest(), + "metadata_sha256": metadata_digest.hexdigest(), + "exact_inventory_sha256": inventory_digest.hexdigest(), + } + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--root", + action="append", + required=True, + metavar="ROLE=ABSOLUTE_PATH", + help="directory root to hash, evict, and prove; may repeat", + ) + parser.add_argument( + "--read-only-root", + action="append", + default=[], + metavar="ROLE", + help="role whose complete mode surface must be immutable (0555/0444)", + ) + parser.add_argument( + "--binding", + action="append", + default=[], + metavar="NAME=VALUE", + help="precomputed evidence identity to bind into the receipt", + ) + parser.add_argument("--output", required=True, type=Path) + return parser.parse_args() + + +def execute_proof( + roots: list[RootRecord], + output: Path, + output_parent: OutputParent, + bindings: dict[str, str], + started_utc: str, + started_monotonic_ns: int, +) -> int: + tool_sha256 = sha256_file(Path(__file__).resolve()) + objects: dict[tuple[int, int], FileObject] = {} + descriptor_budget = ColdFileDescriptorBudget() + roots_by_role = {root.role: root for root in roots} + + inventory_started_monotonic_ns = time.monotonic_ns() + for root in roots: + root.inventory = capture_root_inventory(root, objects, descriptor_budget) + root.inventory_by_path = {entry.relative: entry for entry in root.inventory} + inventory_completed_monotonic_ns = time.monotonic_ns() + if not objects: + fail("cold-residency proof requires at least one regular file") + unique_objects = [objects[identity] for identity in sorted(objects)] + require_root_isolation(roots) + require_output_isolated(output, output_parent, roots, objects) + internal_hardlinks = require_complete_hardlink_topology(unique_objects) + + for file_object in unique_objects: + with reopen_stable_file( + file_object, roots_by_role, descriptor_budget, "content hashing" + ) as fd: + file_object.sha256 = hash_open_file(fd, file_object) + + for file_object in unique_objects: + with reopen_stable_file( + file_object, roots_by_role, descriptor_budget, "data synchronization" + ) as fd: + os.fdatasync(fd) + synchronized_monotonic_ns = time.monotonic_ns() + + for file_object in unique_objects: + with reopen_stable_file( + file_object, roots_by_role, descriptor_budget, "pre-eviction mincore" + ) as fd: + file_object.resident_before_pages = mincore_resident_pages(fd, file_object.size) + resident_before_pages = sum( + file_object.resident_before_pages for file_object in unique_objects + ) + + require_exact_inventories(roots, descriptor_budget, "pre-eviction boundary") + pre_eviction_inventory_verified_monotonic_ns = time.monotonic_ns() + eviction_started_monotonic_ns = time.monotonic_ns() + for file_object in unique_objects: + with reopen_stable_file( + file_object, roots_by_role, descriptor_budget, "targeted eviction" + ) as fd: + os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED) + eviction_completed_monotonic_ns = time.monotonic_ns() + + proof_started_monotonic_ns = time.monotonic_ns() + proof_pass_resident_pages: list[int] = [] + for proof_pass in range(1, 3): + for file_object in unique_objects: + with reopen_stable_file( + file_object, + roots_by_role, + descriptor_budget, + f"mincore proof pass {proof_pass}", + ) as fd: + file_object.resident_after_pages = mincore_resident_pages( + fd, file_object.size + ) + resident_after_pages = sum( + file_object.resident_after_pages for file_object in unique_objects + ) + proof_pass_resident_pages.append(resident_after_pages) + if resident_after_pages != 0: + resident_paths = [ + { + "paths": file_object.paths, + "resident_pages": file_object.resident_after_pages, + "page_count": file_object.page_count, + } + for file_object in unique_objects + if file_object.resident_after_pages + ] + fail( + f"targeted eviction proof pass {proof_pass} found resident pages; " + "a process may still map or read the file(s): " + + json.dumps(resident_paths, sort_keys=True) + ) + require_exact_inventories(roots, descriptor_budget, "post-proof boundary") + post_proof_inventory_verified_monotonic_ns = time.monotonic_ns() + proof_completed_monotonic_ns = time.monotonic_ns() + if descriptor_budget.current != 0: + fail( + "cold-file descriptors remain open after proof: " + f"current={descriptor_budget.current}" + ) + + root_records = [root_receipt(root) for root in roots] + page_count = sum(file_object.page_count for file_object in unique_objects) + logical_bytes = sum(file_object.size for file_object in unique_objects) + receipt = { + "schema_version": SCHEMA, + "status": "passed", + "method": ( + "pinned-fd-relative-exact-inventory+content-sha256+fdatasync+" + "posix-fadvise-dontneed+bounded-streaming-reopen+" + "two-consecutive-sequential-point-in-time-mincore-sweeps" + ), + "claim": ( + "under caller-enforced root quiescence beginning before inventory, every " + "unique regular file was observed nonresident at its sequential point in " + "each of two final sweeps" + ), + "host": { + "system": platform.system(), + "release": platform.release(), + "machine": platform.machine(), + "page_size": PAGE_SIZE, + "pid": os.getpid(), + }, + "resource_bounds": { + "cold_file_descriptor_strategy": "bounded-streaming-reopen", + "cold_file_descriptor_limit": descriptor_budget.limit, + "cold_file_descriptor_peak": descriptor_budget.peak, + "cold_file_descriptor_total_opens": descriptor_budget.total_opens, + "persistent_cold_file_descriptors": 0, + "pinned_root_descriptors": len(roots), + "pinned_output_parent_descriptors": 1, + "final_mincore_sweeps": len(proof_pass_resident_pages), + "final_mincore_resident_pages": proof_pass_resident_pages, + }, + "proof_scope": { + "root_activity_requirement": ( + "quiescent before initial inventory through measured spawn" + ), + "quiescence_required_from_monotonic_ns": started_monotonic_ns, + "path_resolution": "pinned-directory-fd-relative-o_nofollow", + "inventory_revalidation": "exact-before-eviction-and-after-mincore", + "observation_semantics": "sequential-point-in-time", + "payload_reads_after_eviction_started": 0, + }, + "hard_links": { + "policy": "all st_nlink aliases must be inventoried in exactly one root", + "external_alias_count": 0, + "internal_hard_link_object_count": len(internal_hardlinks), + "internal_hard_links": internal_hardlinks, + }, + "tool_sha256": tool_sha256, + "bindings": dict(sorted(bindings.items())), + "roots": root_records, + "totals": { + "root_count": len(roots), + "directory_count": sum(len(root.directories) for root in roots), + "regular_path_count": sum(len(root.regular_paths) for root in roots), + "unique_file_count": len(unique_objects), + "logical_bytes": logical_bytes, + "page_count": page_count, + "resident_before_pages": resident_before_pages, + "resident_after_pages": resident_after_pages, + "resident_after_bytes_upper_bound": resident_after_pages * PAGE_SIZE, + }, + "timestamps": { + "started_utc": started_utc, + "completed_utc": utc_now(), + "started_monotonic_ns": started_monotonic_ns, + "inventory_started_monotonic_ns": inventory_started_monotonic_ns, + "inventory_completed_monotonic_ns": inventory_completed_monotonic_ns, + "synchronized_monotonic_ns": synchronized_monotonic_ns, + "pre_eviction_inventory_verified_monotonic_ns": ( + pre_eviction_inventory_verified_monotonic_ns + ), + "eviction_started_monotonic_ns": eviction_started_monotonic_ns, + "eviction_completed_monotonic_ns": eviction_completed_monotonic_ns, + "proof_started_monotonic_ns": proof_started_monotonic_ns, + "post_proof_inventory_verified_monotonic_ns": ( + post_proof_inventory_verified_monotonic_ns + ), + "proof_completed_monotonic_ns": proof_completed_monotonic_ns, + }, + } + publish_receipt(receipt, output_parent) + return 0 + + +def main() -> int: + started_utc = utc_now() + started_monotonic_ns = time.monotonic_ns() + args = parse_args() + if platform.system() != "Linux": + fail("cold-residency proof requires Linux mincore and POSIX_FADV_DONTNEED") + if not hasattr(os, "posix_fadvise") or not hasattr(os, "POSIX_FADV_DONTNEED"): + fail("Python runtime lacks posix_fadvise(POSIX_FADV_DONTNEED)") + if PAGE_SIZE <= 0: + fail(f"invalid host page size: {PAGE_SIZE}") + + role_paths: dict[str, str] = {} + for item in args.root: + role, path = parse_assignment(item, "--root") + if role in role_paths: + fail(f"duplicate root role: {role}") + role_paths[role] = path + read_only_roles = set(args.read_only_root) + if read_only_roles - set(role_paths): + fail(f"unknown --read-only-root roles: {sorted(read_only_roles - set(role_paths))}") + bindings: dict[str, str] = {} + for item in args.binding: + name, value = parse_assignment(item, "--binding") + if name in bindings: + fail(f"duplicate binding: {name}") + bindings[name] = value + if not bindings: + fail("at least one --binding is required") + + output = args.output.absolute() + with contextlib.ExitStack() as descriptors: + roots: list[RootRecord] = [] + for role, path in sorted(role_paths.items()): + root = pin_root(Path(path), role, role in read_only_roles) + descriptors.callback(os.close, root.fd) + roots.append(root) + output_parent = pin_output_parent(output) + descriptors.callback(os.close, output_parent.fd) + return execute_proof( + roots, + output, + output_parent, + bindings, + started_utc, + started_monotonic_ns, + ) + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (ProofError, OSError, ValueError) as error: + print(f"cold-residency proof failed: {error}", file=sys.stderr) + raise SystemExit(1) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-checkpoint-recycle.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-checkpoint-recycle.sh new file mode 100755 index 00000000..68f22586 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-checkpoint-recycle.sh @@ -0,0 +1,1643 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" +source "$FRESH_ROOT/lib/sealed-carrier.sh" +source "$FRESH_ROOT/lib/postgres-profiles.sh" +source "$FRESH_ROOT/lib/process-supervision.sh" +source "$FRESH_ROOT/lib/server-lifecycle.sh" +source "$FRESH_ROOT/lib/qualification-identities.sh" + +usage() { + cat <<'USAGE' +Usage: qualify-wasix-checkpoint-recycle.sh --sealed-carrier DIR --immutable-carrier-receipt FILE [options] + +Run fixed-offer periodic-checkpoint, WAL recycle, clean postmaster recycle, and +standalone embedded-memory evidence. This lane is always candidate-unqualified +and cannot by itself promote a product or release. + + --sealed-carrier DIR Exact compiler-free carrier. Required. + --immutable-carrier-receipt FILE + External Linux immutable-deployment receipt. + Required for every executable qualification. + --mode MODE smoke, diagnostic, promotional. Default: smoke. + --blocks N Balanced ABBA/BAAB blocks. Defaults: 1/3/10. + --duration-seconds N Per-sample steady epoch. Defaults: 40/240/240. + --connections N Persistent clients. Default: 4. + --tps-per-client N Fixed offers/client/second. Default: 15. + --stagger-us N Per-client start stagger. Default: 12500. + --start-port PORT Default: 55980. + --timeout SECONDS Per-operation deadline. Default: 240. + --cgroup-memory-max SIZE Standalone WASIX MemoryMax. Default: 256M. + --cgroup-memory-high SIZE Standalone WASIX MemoryHigh. Default: 224M. + --cgroup-swap-max SIZE Standalone WASIX MemorySwapMax. Default: 0. + --skip-memory-sample Skip standalone WASIX memory/recycle evidence; + aggregate status then fails closed. + --keep-pgdata Retain successful sample PGDATA. + --label NAME Evidence label. + --print-plan Print the bounded resolved plan and exit. + -h, --help Show help. + +Smoke enforces structural/correctness gates and observes latency gates. +Diagnostic and promotional modes enforce absolute and paired latency gates. +Promotional mode requires at least ten blocks but remains non-release. +USAGE +} + +sealed_carrier="" +immutable_carrier_receipt="" +mode="${WASIX_CHECKPOINT_MODE:-smoke}" +blocks="${WASIX_CHECKPOINT_BLOCKS:-}" +duration_seconds="${WASIX_CHECKPOINT_DURATION_SECONDS:-}" +blocks_explicit=0 +duration_explicit=0 +connections="${WASIX_CHECKPOINT_CONNECTIONS:-4}" +tps_per_client="${WASIX_CHECKPOINT_TPS_PER_CLIENT:-15}" +stagger_us="${WASIX_CHECKPOINT_STAGGER_US:-12500}" +start_port="${WASIX_CHECKPOINT_PORT:-55980}" +timeout_seconds="${WASIX_CHECKPOINT_TIMEOUT:-240}" +cgroup_memory_max="${WASIX_CHECKPOINT_CGROUP_MEMORY_MAX:-256M}" +cgroup_memory_high="${WASIX_CHECKPOINT_CGROUP_MEMORY_HIGH:-224M}" +cgroup_swap_max="${WASIX_CHECKPOINT_CGROUP_SWAP_MAX:-0}" +run_label="${WASIX_CHECKPOINT_LABEL:-$(date -u +%Y%m%dT%H%M%SZ)}" +skip_memory_sample=0 +keep_pgdata=0 +print_plan=0 + +while [ "$#" -gt 0 ]; do + case "$1" in + --sealed-carrier) shift; [ "$#" -gt 0 ] || exit 2; sealed_carrier="$1" ;; + --immutable-carrier-receipt) shift; [ "$#" -gt 0 ] || exit 2; [ -z "$immutable_carrier_receipt" ] || { echo '--immutable-carrier-receipt may only be specified once' >&2; exit 2; }; immutable_carrier_receipt="$1" ;; + --mode) shift; [ "$#" -gt 0 ] || exit 2; mode="$1" ;; + --blocks) shift; [ "$#" -gt 0 ] || exit 2; blocks="$1"; blocks_explicit=1 ;; + --duration-seconds) shift; [ "$#" -gt 0 ] || exit 2; duration_seconds="$1"; duration_explicit=1 ;; + --connections) shift; [ "$#" -gt 0 ] || exit 2; connections="$1" ;; + --tps-per-client) shift; [ "$#" -gt 0 ] || exit 2; tps_per_client="$1" ;; + --stagger-us) shift; [ "$#" -gt 0 ] || exit 2; stagger_us="$1" ;; + --start-port) shift; [ "$#" -gt 0 ] || exit 2; start_port="$1" ;; + --timeout) shift; [ "$#" -gt 0 ] || exit 2; timeout_seconds="$1" ;; + --cgroup-memory-max) shift; [ "$#" -gt 0 ] || exit 2; cgroup_memory_max="$1" ;; + --cgroup-memory-high) shift; [ "$#" -gt 0 ] || exit 2; cgroup_memory_high="$1" ;; + --cgroup-swap-max) shift; [ "$#" -gt 0 ] || exit 2; cgroup_swap_max="$1" ;; + --skip-memory-sample) skip_memory_sample=1 ;; + --keep-pgdata) keep_pgdata=1 ;; + --label) shift; [ "$#" -gt 0 ] || exit 2; run_label="$1" ;; + --print-plan) print_plan=1 ;; + -h|--help) usage; exit 0 ;; + *) printf 'unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +is_positive_integer() { [[ "$1" =~ ^[1-9][0-9]*$ ]]; } +is_nonnegative_integer() { [[ "$1" =~ ^[0-9]+$ ]]; } +validate_cgroup_size() { [[ "$1" =~ ^[0-9]+([KMGTPE]([i]?B)?)?$ ]]; } +cgroup_size_to_bytes() { + python3 - "$1" <<'PY' +import re +import sys + +match = re.fullmatch(r"([0-9]+)([KMGTPE])?(?:i?B)?", sys.argv[1]) +if match is None: + raise SystemExit(2) +value = int(match.group(1)) +suffix = match.group(2) +if suffix is not None: + value *= 1024 ** ("KMGTPE".index(suffix) + 1) +if value > 2**63 - 1: + raise SystemExit(2) +print(value) +PY +} + +case "$mode" in + smoke) + [ "$blocks_explicit" -eq 1 ] || blocks=1 + [ "$duration_explicit" -eq 1 ] || duration_seconds=40 + min_wal_bytes=33554432 + min_checkpoints=1 + min_overlap_samples=10 + post_recycle_quiescence_seconds=5 + performance_enforced=0 + ;; + diagnostic) + [ "$blocks_explicit" -eq 1 ] || blocks=3 + [ "$duration_explicit" -eq 1 ] || duration_seconds=240 + min_wal_bytes=536870912 + min_checkpoints=6 + min_overlap_samples=100 + post_recycle_quiescence_seconds=60 + performance_enforced=1 + ;; + promotional) + [ "$blocks_explicit" -eq 1 ] || blocks=10 + [ "$duration_explicit" -eq 1 ] || duration_seconds=240 + min_wal_bytes=536870912 + min_checkpoints=6 + min_overlap_samples=100 + post_recycle_quiescence_seconds=60 + performance_enforced=1 + ;; + *) echo "--mode requires smoke, diagnostic, or promotional" >&2; exit 2 ;; +esac + +for value in "$blocks" "$duration_seconds" "$connections" "$tps_per_client" "$start_port" "$timeout_seconds"; do + is_positive_integer "$value" || { echo "positive integer option required" >&2; exit 2; } +done +is_nonnegative_integer "$stagger_us" || { echo "--stagger-us requires a nonnegative integer" >&2; exit 2; } +[ "$stagger_us" -le 1000000 ] || { echo "--stagger-us exceeds the probe limit" >&2; exit 2; } +[ "$connections" -le 4 ] || { echo "at most four clients fit the embedded profile control headroom" >&2; exit 2; } +[ "$tps_per_client" -le 10000 ] && [ "$duration_seconds" -le 86400 ] || { echo "probe load exceeds bounded limits" >&2; exit 2; } +[ "$start_port" -le 65535 ] || { echo "--start-port exceeds 65535" >&2; exit 2; } +for cgroup_size in "$cgroup_memory_max" "$cgroup_memory_high" "$cgroup_swap_max"; do + validate_cgroup_size "$cgroup_size" || { + printf 'invalid cgroup size: %s\n' "$cgroup_size" >&2 + exit 2 + } +done +cgroup_memory_max_bytes="$(cgroup_size_to_bytes "$cgroup_memory_max")" || { + echo "--cgroup-memory-max exceeds the supported finite range" >&2 + exit 2 +} +cgroup_memory_high_bytes="$(cgroup_size_to_bytes "$cgroup_memory_high")" || { + echo "--cgroup-memory-high exceeds the supported finite range" >&2 + exit 2 +} +cgroup_swap_max_bytes="$(cgroup_size_to_bytes "$cgroup_swap_max")" || { + echo "--cgroup-swap-max exceeds the supported finite range" >&2 + exit 2 +} +[ "$cgroup_memory_max_bytes" -gt 0 ] && + [ "$cgroup_memory_high_bytes" -gt 0 ] || { + echo "MemoryMax and MemoryHigh must be finite positive sizes" >&2 + exit 2 +} +[ "$cgroup_memory_high_bytes" -le "$cgroup_memory_max_bytes" ] || { + echo "MemoryHigh may not exceed MemoryMax" >&2 + exit 2 +} +if [ "$mode" = promotional ] && [ "$blocks" -lt 10 ]; then + echo "promotional mode requires at least 10 balanced blocks" >&2 + exit 2 +fi +case "$run_label" in ""|*[!A-Za-z0-9._-]*) echo "--label contains unsafe characters" >&2; exit 2 ;; esac +min_achieved_tps="$( + awk -v clients="$connections" -v tps="$tps_per_client" \ + 'BEGIN { printf "%.6f", clients * tps * 0.99 }' +)" + +if [ "$print_plan" -eq 1 ]; then + printf 'mode\t%s\nblocks\t%s\nduration_seconds\t%s\nconnections\t%s\n' "$mode" "$blocks" "$duration_seconds" "$connections" + printf 'tps_per_client\t%s\nmin_wal_bytes\t%s\nmin_checkpoints\t%s\n' "$tps_per_client" "$min_wal_bytes" "$min_checkpoints" + printf 'min_achieved_tps\t%s\n' "$min_achieved_tps" + printf 'cgroup_memory_max\t%s\ncgroup_memory_max_bytes\t%s\n' \ + "$cgroup_memory_max" "$cgroup_memory_max_bytes" + printf 'cgroup_memory_high\t%s\ncgroup_memory_high_bytes\t%s\n' \ + "$cgroup_memory_high" "$cgroup_memory_high_bytes" + printf 'cgroup_swap_max\t%s\ncgroup_swap_max_bytes\t%s\n' \ + "$cgroup_swap_max" "$cgroup_swap_max_bytes" + printf 'memory_sample\t%s\nclassification\tcandidate-unqualified-non-release\n' "$([ "$skip_memory_sample" -eq 0 ] && printf enabled || printf disabled)" + printf 'require_zero_write_aot\t1\nrequired_snapshot_mode\tdirect-immutable-inode\n' + printf 'immutable_carrier_receipt\t%s\n' "${immutable_carrier_receipt:-required-at-execution}" + exit 0 +fi + +[ "$(uname -s)" = Linux ] || { echo "qualification currently requires Linux cgroup v2" >&2; exit 2; } +[ -n "$sealed_carrier" ] || { echo "--sealed-carrier is required" >&2; exit 2; } +[ -n "$immutable_carrier_receipt" ] || { echo "--immutable-carrier-receipt is required" >&2; exit 2; } +[ -d "$sealed_carrier" ] || { printf 'missing sealed carrier: %s\n' "$sealed_carrier" >&2; exit 2; } +sealed_carrier="$(cd "$sealed_carrier" && pwd -P)" +receipt_parent="$(dirname "$immutable_carrier_receipt")" +[ -d "$receipt_parent" ] && [ ! -L "$receipt_parent" ] || { + printf 'immutable carrier receipt parent must be a non-symlink directory: %s\n' \ + "$receipt_parent" >&2 + exit 2 +} +immutable_carrier_receipt="$(cd "$receipt_parent" && pwd -P)/$(basename "$immutable_carrier_receipt")" +[ "$(id -u)" -ne 0 ] || { + echo 'checkpoint qualification must run unprivileged after deployment' >&2 + exit 2 +} +cap_eff="$(awk '$1 == "CapEff:" { print $2 }' /proc/self/status)" +[[ "$cap_eff" =~ ^[0-9a-fA-F]+$ ]] || { + echo 'could not read exact CapEff for checkpoint immutable qualification' >&2 + exit 2 +} +if (( (16#$cap_eff & (1 << 9)) != 0 )); then + echo 'checkpoint qualification refuses effective CAP_LINUX_IMMUTABLE' >&2 + exit 2 +fi +for command in cc ldd perl python3 systemd-run stat; do fresh_require_command "$command"; done +for binary in pg_config initdb postgres psql pg_checksums; do + [ -x "$NATIVE_INSTALL_DIR/bin/$binary" ] || { printf 'missing native oracle: %s\n' "$NATIVE_INSTALL_DIR/bin/$binary" >&2; exit 2; } +done + +fresh_ensure_dirs +fresh_capture_qualification_carrier_identity "$sealed_carrier" +carrier_identity="$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" +core_profile="$FRESH_QUALIFICATION_CORE_PROFILE" +guest_build_recipe_sha256="$FRESH_QUALIFICATION_GUEST_BUILD_RECIPE_SHA256" +"$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier" \ + --receipt "$immutable_carrier_receipt" --fast +immutable_receipt_sha256="$(fresh_wasmer_bin_hash "$immutable_carrier_receipt")" +read -r immutable_receipt_dev immutable_receipt_ino < <( + stat -c '%d %i' -- "$immutable_carrier_receipt" +) +loader_validator="$FRESH_ROOT/bin/validate-sealed-loader-audit.py" +loader_validator_sha256="$(fresh_wasmer_bin_hash "$loader_validator")" +cache_observe_validator="$FRESH_ROOT/bin/validate-file-cache-telemetry.py" +cache_observe_validator_sha256="$(fresh_wasmer_bin_hash "$cache_observe_validator")" +cache_adaptive_validator="$FRESH_ROOT/bin/validate-adaptive-file-cache-telemetry.py" +cache_adaptive_validator_sha256="$(fresh_wasmer_bin_hash "$cache_adaptive_validator")" +expected_initdb_executions=$((blocks * 2)) +expected_postgres_executions=$((blocks * 2)) +expected_cache_observe_validations=$((blocks * 4)) +expected_cache_adaptive_validations=$((blocks * 2)) +if [ "$skip_memory_sample" -eq 0 ]; then + expected_initdb_executions=$((expected_initdb_executions + 1)) + expected_postgres_executions=$((expected_postgres_executions + 2)) + expected_cache_observe_validations=$((expected_cache_observe_validations + 3)) + expected_cache_adaptive_validations=$((expected_cache_adaptive_validations + 2)) +fi +fresh_capture_native_oracle_identity "$NATIVE_INSTALL_DIR" +native_identity="$FRESH_QUALIFICATION_NATIVE_ORACLE_IDENTITY" +pg_checksums_bin="$NATIVE_INSTALL_DIR/bin/pg_checksums" +pg_checksums_sha256="$(fresh_wasmer_bin_hash "$pg_checksums_bin")" + +checkpoint_gucs="$FRESH_ROOT/profiles/checkpoint-policies/embedded-steady-v1.gucs" +checkpoint_policy="$FRESH_ROOT/profiles/checkpoint-policies/embedded-steady-v1.tsv" +workload_setup_sql="$FRESH_ROOT/bench/sql/checkpoint-workload-setup.sql" +workload_volume_sql="$FRESH_ROOT/bench/sql/checkpoint-volume.sql" +workload_state_sql="$FRESH_ROOT/bench/sql/checkpoint-database-state.sql" +[ -f "$checkpoint_gucs" ] && [ ! -L "$checkpoint_gucs" ] && + [ -f "$checkpoint_policy" ] && [ ! -L "$checkpoint_policy" ] || { + echo "checkpoint policy inputs must be regular non-symlink files" >&2 + exit 2 + } +for sql_input in "$workload_setup_sql" "$workload_volume_sql" "$workload_state_sql"; do + [ -f "$sql_input" ] && [ ! -L "$sql_input" ] || { + printf 'workload SQL input must be regular and non-symlink: %s\n' \ + "$sql_input" >&2 + exit 2 + } +done +checkpoint_gucs_sha256="$(fresh_wasmer_bin_hash "$checkpoint_gucs")" +checkpoint_policy_sha256="$(fresh_wasmer_bin_hash "$checkpoint_policy")" +workload_setup_sha256="$(fresh_wasmer_bin_hash "$workload_setup_sql")" +workload_volume_sha256="$(fresh_wasmer_bin_hash "$workload_volume_sql")" +workload_state_sha256="$(fresh_wasmer_bin_hash "$workload_state_sql")" +workload_sql_identity="$( + printf '%s\n%s\n%s\n' \ + "$workload_setup_sha256" "$workload_volume_sha256" \ + "$workload_state_sha256" | fresh_sha256_stream +)" +mapfile -t checkpoint_guc_values <"$checkpoint_gucs" +fresh_resolve_postgres_profiles embedded-concurrent safe "${checkpoint_guc_values[@]}" +[ "${#FRESH_POSTGRES_PROFILE_OVERLAPPING_EXPLICIT[@]}" -eq 0 ] || exit 2 +effective_gucs=("${FRESH_POSTGRES_PROFILE_GUCS[@]}") + +run_root="$FRESH_WORK_ROOT/run/checkpoint-recycle-$run_label" +report_root="$FRESH_WORK_ROOT/reports/checkpoint-recycle-$run_label" +fresh_require_managed_generated_path "$run_root" "checkpoint run root" +fresh_require_managed_generated_path "$report_root" "checkpoint report root" +[ ! -e "$run_root" ] && [ ! -L "$run_root" ] || { printf 'refusing existing run root: %s\n' "$run_root" >&2; exit 2; } +[ ! -e "$report_root" ] && [ ! -L "$report_root" ] || { printf 'refusing existing report root: %s\n' "$report_root" >&2; exit 2; } +mkdir -p "$run_root" "$report_root/samples" "$report_root/provenance" +sealed_loader_audit="$report_root/sealed-loader-audit.jsonl" +sealed_loader_validation="$report_root/sealed-loader-audit-validation.tsv" +sealed_loader_envelope="$report_root/qualification-evidence-envelope.tsv" +[ ! -e "$sealed_loader_audit" ] && [ ! -L "$sealed_loader_audit" ] || { + echo 'sealed loader audit path exists before checkpoint qualification' >&2 + exit 2 +} + +cp -p "$checkpoint_gucs" "$report_root/provenance/checkpoint-policy.gucs" +cp -p "$checkpoint_policy" "$report_root/provenance/checkpoint-policy.tsv" +cp -p "$workload_setup_sql" "$report_root/provenance/checkpoint-workload-setup.sql" +cp -p "$workload_volume_sql" "$report_root/provenance/checkpoint-volume.sql" +cp -p "$workload_state_sql" "$report_root/provenance/checkpoint-database-state.sql" +fresh_write_native_oracle_manifest "$NATIVE_INSTALL_DIR" "$report_root/provenance/native-oracle-identity.tsv" +cp -p "$sealed_carrier/manifest.json" "$report_root/provenance/manifest.json" +cp -p "$sealed_carrier/wasmer-build.receipt" "$report_root/provenance/wasmer-build.receipt" +cp -p "$sealed_carrier/payload.files" "$report_root/provenance/payload.files" +fresh_write_postgres_profile_evidence "$report_root/provenance/postgres-profile-inputs.tsv" "$report_root/provenance/postgres-profile-resolution.tsv" +cache_telemetry_policy="$report_root/provenance/cache-telemetry-policy.tsv" +printf 'schema_version\tactivation_source\toutput_environment\tadaptive_path_derivation\tobserve_validator_sha256\tadaptive_validator_sha256\texpected_observe_validations\texpected_adaptive_validations\n' \ + >"$cache_telemetry_policy" +printf 'oliphaunt.wasix-postmaster.cache-telemetry-policy.v1\tsealed-manifest-only\tOLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE\tPath::with_extension("adaptive.json")\t%s\t%s\t%s\t%s\n' \ + "$cache_observe_validator_sha256" "$cache_adaptive_validator_sha256" \ + "$expected_cache_observe_validations" "$expected_cache_adaptive_validations" \ + >>"$cache_telemetry_policy" +cache_telemetry_policy_sha256="$(fresh_wasmer_bin_hash "$cache_telemetry_policy")" +standalone_cgroup_policy="$report_root/provenance/standalone-cgroup-policy.tsv" +printf 'schema_version\tenabled\tmemory_max\tmemory_max_bytes\tmemory_high\tmemory_high_bytes\tmemory_swap_max\tmemory_swap_max_bytes\n' \ + >"$standalone_cgroup_policy" +printf 'oliphaunt.wasix-postmaster.standalone-cgroup-policy.v1\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$([ "$skip_memory_sample" -eq 0 ] && printf true || printf false)" \ + "$cgroup_memory_max" "$cgroup_memory_max_bytes" \ + "$cgroup_memory_high" "$cgroup_memory_high_bytes" \ + "$cgroup_swap_max" "$cgroup_swap_max_bytes" \ + >>"$standalone_cgroup_policy" +standalone_cgroup_policy_sha256="$(fresh_wasmer_bin_hash "$standalone_cgroup_policy")" +printf 'schema_version\tpath\tsha256\n' \ + >"$report_root/provenance/checksum-tool.tsv" +printf 'oliphaunt.wasix-postmaster.checksum-tool.v1\t%s\t%s\n' \ + "$pg_checksums_bin" "$pg_checksums_sha256" \ + >>"$report_root/provenance/checksum-tool.tsv" + +policy_receipt="$report_root/qualification-policy.tsv" +printf 'schema_version\tstatus\tmode\tblocks\tduration_seconds\tconnections\ttps_per_client\tmin_achieved_tps\tstagger_us\tmin_wal_bytes\tmin_checkpoints\tmin_overlap_samples\tperformance_enforced\tcheckpoint_gucs_sha256\tcheckpoint_policy_sha256\tworkload_sql_identity\tpg_checksums_sha256\tpostgres_profile_identity\tcarrier_identity\tnative_identity\trequire_zero_write_aot\trequired_snapshot_mode\timmutable_receipt_path\timmutable_receipt_sha256\timmutable_receipt_dev\timmutable_receipt_ino\tloader_validator_sha256\texpected_initdb_executions\texpected_postgres_executions\tcore_profile\tguest_build_recipe_sha256\timmutable_verification_scope\n' >"$policy_receipt" +printf 'oliphaunt.wasix-postmaster.checkpoint-qualification-policy.v3\tcandidate-unqualified\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t1\tdirect-immutable-inode\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\tcampaign-boundary-full-fast-samples\n' \ + "$mode" "$blocks" "$duration_seconds" "$connections" "$tps_per_client" "$min_achieved_tps" "$stagger_us" "$min_wal_bytes" "$min_checkpoints" "$min_overlap_samples" \ + "$performance_enforced" "$checkpoint_gucs_sha256" "$checkpoint_policy_sha256" "$workload_sql_identity" "$pg_checksums_sha256" "$FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY" "$carrier_identity" "$native_identity" \ + "$immutable_carrier_receipt" "$immutable_receipt_sha256" \ + "$immutable_receipt_dev" "$immutable_receipt_ino" \ + "$loader_validator_sha256" "$expected_initdb_executions" \ + "$expected_postgres_executions" "$core_profile" \ + "$guest_build_recipe_sha256" >>"$policy_receipt" +policy_identity="$(fresh_wasmer_bin_hash "$policy_receipt")" +chmod 0444 "$policy_receipt" "$report_root"/provenance/* + +assert_frozen_inputs() { + local receipt_stat observed_receipt_dev observed_receipt_ino + receipt_stat="$(stat -c '%d %i' -- "$immutable_carrier_receipt")" || return + read -r observed_receipt_dev observed_receipt_ino <<<"$receipt_stat" + [ "$(fresh_wasmer_bin_hash "$policy_receipt")" = "$policy_identity" ] && + [ "$(fresh_wasmer_bin_hash "$cache_telemetry_policy")" = "$cache_telemetry_policy_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$standalone_cgroup_policy")" = "$standalone_cgroup_policy_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$checkpoint_gucs")" = "$checkpoint_gucs_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$checkpoint_policy")" = "$checkpoint_policy_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$workload_setup_sql")" = "$workload_setup_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$workload_volume_sql")" = "$workload_volume_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$workload_state_sql")" = "$workload_state_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$pg_checksums_bin")" = "$pg_checksums_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$probe_source")" = "$probe_source_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$probe_bin")" = "$probe_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$loader_validator")" = "$loader_validator_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$cache_observe_validator")" = "$cache_observe_validator_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$cache_adaptive_validator")" = "$cache_adaptive_validator_sha256" ] && + [ "$observed_receipt_dev" = "$immutable_receipt_dev" ] && + [ "$observed_receipt_ino" = "$immutable_receipt_ino" ] && + fresh_assert_postgres_profile_inputs && + "$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier" \ + --receipt "$immutable_carrier_receipt" --fast && + fresh_capture_native_oracle_identity "$NATIVE_INSTALL_DIR" && + [ "$FRESH_QUALIFICATION_NATIVE_ORACLE_IDENTITY" = "$native_identity" ] +} + +probe_source="$FRESH_ROOT/probes/libpq_checkpoint_probe.c" +probe_bin="$report_root/provenance/libpq-checkpoint-probe" +pg_config="$NATIVE_INSTALL_DIR/bin/pg_config" +include_dir="$($pg_config --includedir)" +lib_dir="$($pg_config --libdir)" +probe_pending="$(mktemp "$report_root/provenance/.checkpoint-probe.XXXXXX")" +cc -std=c11 -O2 -g0 -Wall -Wextra -Werror -Wpedantic -Wconversion -Wshadow -pthread \ + -I"$include_dir" "$probe_source" -L"$lib_dir" "-Wl,-rpath,$lib_dir" -lpq -o "$probe_pending" +chmod 0555 "$probe_pending" +mv "$probe_pending" "$probe_bin" +probe_sha256="$(fresh_wasmer_bin_hash "$probe_bin")" +probe_source_sha256="$(fresh_wasmer_bin_hash "$probe_source")" +linked_libpq="$(env -u LD_LIBRARY_PATH -u LD_PRELOAD -u LD_AUDIT ldd "$probe_bin" | awk '$1 ~ /^libpq[.]so/ && $2 == "=>" { print $3 }')" +[ -n "$linked_libpq" ] && [ "$(realpath "$linked_libpq")" = "$(realpath "$lib_dir/libpq.so")" ] || { + echo "checkpoint probe did not resolve exact native-oracle libpq" >&2 + exit 2 +} +printf 'schema_version\tprobe_sha256\tprobe_source_sha256\tlibpq_path\tlibpq_sha256\tcompiler\n' >"$report_root/provenance/probe-build.tsv" +printf 'oliphaunt.wasix-postmaster.checkpoint-probe-build.v1\t%s\t%s\t%s\t%s\t%s\n' \ + "$probe_sha256" "$probe_source_sha256" "$(realpath "$linked_libpq")" "$(fresh_wasmer_bin_hash "$(realpath "$linked_libpq")")" \ + "$(cc --version | awk 'NR==1{print}')" >>"$report_root/provenance/probe-build.tsv" +chmod 0444 "$report_root/provenance/probe-build.tsv" + +wasmer_bin="$sealed_carrier/bin/wasmer-headless" +wasix_initdb="$sealed_carrier/bin/initdb" +wasix_postgres="$sealed_carrier/bin/postgres" +wasix_lib="$sealed_carrier/lib" +wasmer_stack_size="${WASMER_STACK_SIZE:-33554432}" +wasmer_args=( + run --quiet --disable-cache + --sealed-module-manifest "$sealed_carrier/manifest.json" + --stack-size "$wasmer_stack_size" + --enable-exceptions --enable-threads --net + --volume "$REPO_ROOT:$REPO_ROOT" + --volume "$wasix_lib:/lib" +) +case "$FRESH_WORK_ROOT/" in + "$REPO_ROOT/"*) ;; + *) wasmer_args+=(--volume "$FRESH_WORK_ROOT:$FRESH_WORK_ROOT") ;; +esac +case "$sealed_carrier/" in + "$REPO_ROOT/"*|"$FRESH_WORK_ROOT/"*) ;; + *) wasmer_args+=(--volume "$sealed_carrier:$sealed_carrier") ;; +esac + +active_pid="" +active_pgid="" +active_identity="" +active_port="" +active_cgroup_dir="" +active_cgroup_identity="" +active_dev_shm="" +active_log="" +active_target="" +active_epoch_origin_ns="" +memory_sampler_pid="" +memory_stop_file="" +qualification_status=failed +current_stage=setup + +cleanup() { + local status="$?" + trap - EXIT HUP INT TERM + if [ -n "$memory_sampler_pid" ]; then + [ -n "$memory_stop_file" ] && touch "$memory_stop_file" + wait "$memory_sampler_pid" 2>/dev/null || true + fi + if [ -n "$active_pid" ] && fresh_supervision_pid_running "$active_pid"; then + fresh_terminate_owned_process_group \ + "$active_pgid" "$active_pid" "$active_identity" 1000 3000 || true + fi + { + printf 'status\t%s\n' "$qualification_status" + printf 'exit_code\t%s\n' "$status" + printf 'last_stage\t%s\n' "$current_stage" + printf 'report_root\t%s\n' "$report_root" + } >"$report_root/run-result.tsv" + exit "$status" +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +now_ns() { + perl -MTime::HiRes=clock_gettime,CLOCK_MONOTONIC \ + -e 'printf "%.0f\n", clock_gettime(CLOCK_MONOTONIC) * 1000000000' +} +run_timed() { + local log="$1" + shift + fresh_run_process_group_timeout "$timeout_seconds" -- "$@" >"$log" 2>&1 +} +launch_with_nofile() { + ulimit -S -n 1024 + exec "$@" +} + +cache_telemetry_path_for_log() { + local log="$1" + case "$log" in + /*.log) printf '%s.cache-offers.json\n' "${log%.log}" ;; + *) printf 'cache telemetry requires an absolute .log path: %s\n' "$log" >&2; return 125 ;; + esac +} + +validate_observe_cache_telemetry() { + local log="$1" + local expected_workload="$2" + local telemetry validation + telemetry="$(cache_telemetry_path_for_log "$log")" || return + validation="${telemetry%.json}-validation.tsv" + python3 "$cache_observe_validator" \ + --telemetry "$telemetry" \ + --manifest "$sealed_carrier/manifest.json" \ + --output "$validation" \ + --expected-workload "$expected_workload" + chmod 0444 "$telemetry" +} + +validate_postgres_cache_telemetry() { + local log="$1" + local telemetry adaptive validation + validate_observe_cache_telemetry "$log" runtime:postgres + telemetry="$(cache_telemetry_path_for_log "$log")" || return + # Exact sibling convention used by Rust Path::with_extension("adaptive.json"). + adaptive="${telemetry%.json}.adaptive.json" + validation="${telemetry%.json}-adaptive-validation.tsv" + python3 "$cache_adaptive_validator" \ + --telemetry "$adaptive" \ + --manifest "$sealed_carrier/manifest.json" \ + --output "$validation" + chmod 0444 "$adaptive" +} + +server_prefix=() +server_cgroup_unit="" +configure_cgroup() { + local sample_label="$1" + local epoch="$2" + local enable="$3" + server_prefix=() + server_cgroup_unit="" + [ "$enable" -eq 1 ] || return 0 + server_cgroup_unit="oliphaunt-cp-$$-$sample_label-e$epoch" + server_prefix=( + systemd-run --user --scope --quiet --collect + "--unit=$server_cgroup_unit" + --property=MemoryAccounting=yes + "--property=MemoryMax=$cgroup_memory_max" + "--property=MemoryHigh=$cgroup_memory_high" + "--property=MemorySwapMax=$cgroup_swap_max" + ) +} + +capture_cgroup() { + local pid="$1" + local unit="$2" + local deadline relative="" directory observed_max observed_high observed_swap + active_cgroup_dir="" + active_cgroup_identity="" + [ -n "$unit" ] || return 0 + deadline=$(( $(fresh_supervision_now_ms) + 5000 )) + while [ "$(fresh_supervision_now_ms)" -lt "$deadline" ]; do + relative="$(awk -F: '$1=="0"{print $3;exit}' \ + "/proc/$pid/cgroup" 2>/dev/null || true)" + if [ "$(basename "$relative" 2>/dev/null || true)" = "$unit.scope" ]; then + break + fi + sleep 0.05 + done + case "$relative" in /*) ;; *) return 125 ;; esac + [ "$(basename "$relative")" = "$unit.scope" ] || return 125 + directory="/sys/fs/cgroup$relative" + [ -r "$directory/cgroup.procs" ] && + [ -r "$directory/memory.peak" ] && + [ -r "$directory/memory.max" ] && + [ -r "$directory/memory.high" ] && + [ -r "$directory/memory.swap.max" ] || return 125 + observed_max="$(<"$directory/memory.max")" + observed_high="$(<"$directory/memory.high")" + observed_swap="$(<"$directory/memory.swap.max")" + [ "$observed_max" = "$cgroup_memory_max_bytes" ] && + [ "$observed_high" = "$cgroup_memory_high_bytes" ] && + [ "$observed_swap" = "$cgroup_swap_max_bytes" ] || { + printf 'standalone cgroup controls differ: max=%s/%s high=%s/%s swap=%s/%s\n' \ + "$observed_max" "$cgroup_memory_max_bytes" \ + "$observed_high" "$cgroup_memory_high_bytes" \ + "$observed_swap" "$cgroup_swap_max_bytes" >&2 + return 125 + } + active_cgroup_dir="$directory" + active_cgroup_identity="$(fresh_path_identity "$directory")" +} + +start_server() { + local target="$1" + local pgdata="$2" + local dev_shm="$3" + local port="$4" + local server_log="$5" + local initialize="$6" + local sample_label="$7" + local epoch="$8" + local measured="$9" + local initdb_log="${server_log%.log}.initdb.log" + local initdb_cache_telemetry + local postgres_cache_telemetry + local postgres_args=( + -D "$pgdata" -h 127.0.0.1 -p "$port" + -c unix_socket_directories= + ) + local guc + [ -z "$active_pid" ] || return 125 + ! fresh_tcp_port_open 127.0.0.1 "$port" || return 125 + [ -z "$(find "$dev_shm" -mindepth 1 -print -quit)" ] || return 1 + assert_frozen_inputs + if [ "$initialize" -eq 1 ]; then + if [ "$target" = native ]; then + run_timed "$initdb_log" "$NATIVE_INSTALL_DIR/bin/initdb" \ + -D "$pgdata" -A trust --no-locale --encoding=UTF8 \ + --data-checksums --no-instructions + else + local init_args=("${wasmer_args[@]}" --volume "$dev_shm:/dev/shm") + initdb_cache_telemetry="$(cache_telemetry_path_for_log "$initdb_log")" || return + run_timed "$initdb_log" env -u WASMER_DIR -u WASMER_CACHE_DIR \ + -u OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT \ + -u OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE \ + -u OLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE \ + OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1 \ + "OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE=$sealed_loader_audit" \ + "OLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE=$initdb_cache_telemetry" \ + "$wasmer_bin" "${init_args[@]}" "$wasix_initdb" -- \ + -D "$pgdata" -A trust --no-locale --encoding=UTF8 \ + --data-checksums --no-instructions + validate_observe_cache_telemetry "$initdb_log" runtime:initdb + fi + fi + for guc in "${effective_gucs[@]}"; do postgres_args+=(-c "$guc"); done + configure_cgroup "$sample_label" "$epoch" "$measured" + active_epoch_origin_ns="$(now_ns)" + if [ "$target" = native ]; then + fresh_spawn_process_group -- launch_with_nofile \ + "${server_prefix[@]}" "$NATIVE_INSTALL_DIR/bin/postgres" \ + "${postgres_args[@]}" >"$server_log" 2>&1 + else + local run_args=("${wasmer_args[@]}" --volume "$dev_shm:/dev/shm") + postgres_cache_telemetry="$(cache_telemetry_path_for_log "$server_log")" || return + fresh_spawn_process_group -- launch_with_nofile \ + "${server_prefix[@]}" env -u WASMER_DIR -u WASMER_CACHE_DIR \ + -u OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT \ + -u OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE \ + -u OLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE \ + OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1 \ + "OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE=$sealed_loader_audit" \ + "OLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE=$postgres_cache_telemetry" \ + "$wasmer_bin" "${run_args[@]}" "$wasix_postgres" -- \ + "${postgres_args[@]}" >"$server_log" 2>&1 + fi + active_pid="$FRESH_PROCESS_GROUP_PID" + active_pgid="$FRESH_PROCESS_GROUP_PGID" + active_identity="$FRESH_PROCESS_GROUP_IDENTITY" + active_port="$port" + active_dev_shm="$dev_shm" + active_log="$server_log" + active_target="$target" + [ "$active_pid" = "$active_pgid" ] && [ -n "$active_identity" ] || + return 125 + capture_cgroup "$active_pid" "$server_cgroup_unit" +} + +connection_uri() { + if [ "$1" = native ]; then + printf 'postgresql://%s@127.0.0.1:%s/postgres\n' "$(id -un)" "$2" + else + printf 'postgresql://wasix@127.0.0.1:%s/postgres\n' "$2" + fi +} + +wait_ready() { + local conn="$1" + local log="$2" + local deadline status + : >"$log" + deadline=$(( $(fresh_supervision_now_ms) + timeout_seconds * 1000 )) + while [ "$(fresh_supervision_now_ms)" -lt "$deadline" ]; do + set +e + fresh_run_process_group_timeout_ms 1000 -- \ + env PGCONNECT_TIMEOUT=1 "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" -XAtq -v ON_ERROR_STOP=1 -c 'select 1' >>"$log" 2>&1 + status=$? + set -e + [ "$status" -eq 0 ] && return 0 + [ "$status" -ne 125 ] || return 125 + fresh_supervision_pid_running "$active_pid" || return 1 + fresh_pid_matches_birth_identity "$active_pid" "$active_identity" || + return 125 + sleep 0.1 + done + return 124 +} + +psql_file() { + local conn="$1" + local output="$2" + local sql="$3" + fresh_run_process_group_timeout "$timeout_seconds" -- \ + env PGCONNECT_TIMEOUT=5 "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" -XAtq -F $'\t' -v ON_ERROR_STOP=1 -c "$sql" \ + >"$output" 2>"$output.stderr" +} + +psql_script() { + local conn="$1" + local output="$2" + local script="$3" + shift 3 + fresh_run_process_group_timeout "$timeout_seconds" -- \ + env PGCONNECT_TIMEOUT=5 "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" -XAtq -v ON_ERROR_STOP=1 "$@" -f "$script" \ + >"$output" 2>"$output.stderr" +} + +stop_smart() { + local evidence="$1" + local deadline wait_status + fresh_signal_owned_pid TERM "$active_pid" "$active_identity" + deadline=$(( $(fresh_supervision_now_ms) + timeout_seconds * 1000 )) + while fresh_supervision_pid_running "$active_pid"; do + if ! fresh_pid_matches_birth_identity "$active_pid" "$active_identity"; then + fresh_supervision_pid_running "$active_pid" && return 125 + break + fi + [ "$(fresh_supervision_now_ms)" -lt "$deadline" ] || return 124 + sleep 0.05 + done + fresh_reap_process_group_leader "$active_pid" + wait_status="$FRESH_PROCESS_GROUP_WAIT_STATUS" + [ "$wait_status" -eq 0 ] || return 1 + ! fresh_process_group_exists "$active_pgid" || return 1 + fresh_wait_cgroup_empty \ + "$active_cgroup_dir" "$active_cgroup_identity" 5000 || return 1 + fresh_wait_tcp_port_closed 127.0.0.1 "$active_port" 5000 || return 1 + [ -z "$(find "$active_dev_shm" -mindepth 1 -print -quit)" ] || return 1 + grep -Fq 'received smart shutdown request' "$active_log" || return 1 + grep -Fq 'database system is shut down' "$active_log" || return 1 + { + printf 'target\tpid\tpgid\tbirth_identity\twait_status\tprocess_group_empty\tcgroup_empty\tport_closed\tshared_objects_empty\tclean_shutdown\tescalation\n' + printf '%s\t%s\t%s\t%s\t0\ttrue\ttrue\ttrue\ttrue\ttrue\tnone\n' \ + "$active_target" "$active_pid" "$active_pgid" "$active_identity" + } >"$evidence" + active_pid="" + active_pgid="" + active_identity="" + active_port="" + active_cgroup_dir="" + active_cgroup_identity="" + active_dev_shm="" + active_log="" + active_target="" + active_epoch_origin_ns="" +} + +capture_settings() { + local conn="$1" + local output="$2" + psql_file "$conn" "$output" " +COPY ( + SELECT name, setting, COALESCE(unit, '') AS unit, source + FROM pg_settings + WHERE source = 'command line' + AND name NOT IN ( + 'config_file', 'data_directory', 'external_pid_file', + 'hba_file', 'ident_file' + ) + ORDER BY name +) TO STDOUT WITH (FORMAT csv, HEADER true, DELIMITER E'\\t'); +" +} + +capture_checkpoint_state() { + local conn="$1" + local output="$2" + psql_file "$conn" "$output" " +COPY ( + SELECT c.num_timed::bigint AS num_timed, + c.num_requested::bigint AS num_requested, + c.num_done::bigint AS num_done, + w.wal_bytes::numeric::bigint AS wal_bytes + FROM pg_stat_checkpointer AS c + CROSS JOIN pg_stat_wal AS w +) TO STDOUT WITH (FORMAT csv, HEADER true, DELIMITER E'\\t'); +" +} + +verify_online_data_checksums() { + local conn="$1" + local output="$2" + psql_file "$conn" "$output" " +COPY ( + SELECT current_setting('data_checksums') AS data_checksums +) TO STDOUT WITH (FORMAT csv, HEADER true, DELIMITER E'\\t'); +" + [ "$(awk -F '\t' 'NR == 2 { print $1 }' "$output")" = on ] +} + +completed_time_checkpoints() { + local server_log="$1" + awk ' + /checkpoint starting:/ { + active = 1 + timed = index($0, "checkpoint starting: time") > 0 + next + } + active && /checkpoint complete:/ { + if (timed) count++ + active = 0 + timed = 0 + } + END { print count + 0 } + ' "$server_log" +} + +wait_for_periodic_checkpoints() { + local server_log="$1" + local baseline="$2" + local receipt="$3" + local deadline observed + deadline=$(( $(fresh_supervision_now_ms) + timeout_seconds * 1000 )) + while :; do + observed="$(completed_time_checkpoints "$server_log")" + if [ $((observed - baseline)) -ge "$min_checkpoints" ]; then + { + printf 'completed_time_before\tcompleted_time_after\tdelta\n' + printf '%s\t%s\t%s\n' \ + "$baseline" "$observed" "$((observed - baseline))" + } >"$receipt" + return 0 + fi + fresh_supervision_pid_running "$active_pid" || return 1 + fresh_pid_matches_birth_identity "$active_pid" "$active_identity" || + return 125 + [ "$(fresh_supervision_now_ms)" -lt "$deadline" ] || return 124 + sleep 0.2 + done +} + +capture_full_stats() { + local conn="$1" + local output="$2" + psql_file "$conn" "$output" " +COPY ( + SELECT 'checkpointer'::text AS section, to_jsonb(c)::text AS payload + FROM pg_stat_checkpointer AS c + UNION ALL + SELECT 'wal', to_jsonb(w)::text + FROM pg_stat_wal AS w + UNION ALL + SELECT 'io', to_jsonb(i)::text + FROM pg_stat_io AS i +) TO STDOUT WITH (FORMAT csv, HEADER true, DELIMITER E'\\t'); +" +} + +setup_workload() { + local conn="$1" + local output="$2" + psql_script "$conn" "$output" "$workload_setup_sql" \ + -v "connections=$connections" +} + +capture_database_state() { + local conn="$1" + local output="$2" + psql_script "$conn" "$output" "$workload_state_sql" +} + +run_probe() { + local conn="$1" + local directory="$2" + local duration="$3" + local sequence_offset="$4" + mkdir -p "$directory" + fresh_run_process_group_timeout "$((timeout_seconds + duration))" -- \ + "$probe_bin" \ + --conninfo "$conn" \ + --output "$directory/transactions.tsv" \ + --flush-output "$directory/flushes.tsv" \ + --clients "$connections" \ + --duration-seconds "$duration" \ + --tps-per-client "$tps_per_client" \ + --stagger-us "$stagger_us" \ + --sequence-offset "$sequence_offset" \ + >"$directory/probe.stdout" 2>"$directory/probe.stderr" +} + +validate_periodic_probe() { + local target="$1" + local directory="$2" + local server_log="$3" + local duration="$4" + local validation_args=( + --transactions "$directory/transactions.tsv" + --flushes "$directory/flushes.tsv" + --checkpoint-before "$directory/checkpoint-before.tsv" + --checkpoint-after "$directory/checkpoint-after.tsv" + --server-log "$server_log" + --output "$directory/checkpoint-summary.tsv" + --gates-output "$directory/checkpoint-gates.tsv" + --target "$target" + --mode "$mode" + --clients "$connections" + --duration-seconds "$duration" + --tps-per-client "$tps_per_client" + --stagger-us "$stagger_us" + --min-achieved-tps "$min_achieved_tps" + --min-wal-bytes "$min_wal_bytes" + --min-checkpoints "$min_checkpoints" + --min-overlap-samples "$min_overlap_samples" + ) + if [ "$performance_enforced" -eq 1 ]; then + validation_args+=(--enforce-performance) + fi + python3 "$FRESH_ROOT/bin/validate-checkpoint-recycle.py" \ + "${validation_args[@]}" +} + +collect_memory_sample() { + local cgroup_dir="$1" + local cgroup_identity="$2" + local epoch="$3" + local epoch_origin_ns="$4" + local phase="$5" + local output="$6" + local actual_identity pids process_metrics + local process_count pss_kib pss_anon_kib private_kib pagetables_kib + local current peak swap anon file kernel cgroup_pagetables dirty writeback + local event_metrics event_high event_max event_oom event_oom_kill + local pressure_metrics psi_some psi_full monotonic + + [ -d "$cgroup_dir" ] || return 3 + actual_identity="$(fresh_path_identity "$cgroup_dir")" || return 125 + [ "$actual_identity" = "$cgroup_identity" ] || return 125 + pids="$(tr '\n' ' ' <"$cgroup_dir/cgroup.procs")" + [ -n "${pids//[[:space:]]/}" ] || return 3 + process_metrics="$( + { + local pid + for pid in $pids; do + case "$pid" in ""|*[!0-9]*) return 125 ;; esac + [ -r "/proc/$pid/smaps_rollup" ] && + [ -r "/proc/$pid/status" ] || continue + awk ' + FNR == 1 { file_index++ } + file_index == 1 && /^Pss:[[:space:]]/ { + pss = $2 + saw_pss = 1 + } + file_index == 1 && /^Pss_Anon:[[:space:]]/ { pss_anon = $2 } + file_index == 1 && /^Private_Clean:[[:space:]]/ { private += $2 } + file_index == 1 && /^Private_Dirty:[[:space:]]/ { private += $2 } + file_index == 1 && /^Private_Hugetlb:[[:space:]]/ { private += $2 } + file_index == 2 && /^VmPTE:[[:space:]]/ { pagetables = $2 } + END { + if (saw_pss) { + printf "1\t%.0f\t%.0f\t%.0f\t%.0f\n", + pss, pss_anon, private, pagetables + } + } + ' "/proc/$pid/smaps_rollup" "/proc/$pid/status" 2>/dev/null || true + done + } | awk -F '\t' ' + { + process_count += $1 + pss += $2 + pss_anon += $3 + private += $4 + pagetables += $5 + } + END { + printf "%d\t%.0f\t%.0f\t%.0f\t%.0f\n", + process_count, pss, pss_anon, private, pagetables + } + ' + )" + IFS=$'\t' read -r process_count pss_kib pss_anon_kib private_kib pagetables_kib \ + <<<"$process_metrics" + [ "$process_count" -gt 0 ] || return 3 + + current="$(<"$cgroup_dir/memory.current")" + peak="$(<"$cgroup_dir/memory.peak")" + swap="$(<"$cgroup_dir/memory.swap.current")" + IFS=$'\t' read -r anon file kernel cgroup_pagetables dirty writeback < <( + awk ' + $1 == "anon" { anon = $2; seen["anon"]++ } + $1 == "file" { file = $2; seen["file"]++ } + $1 == "kernel" { kernel = $2; seen["kernel"]++ } + $1 == "pagetables" { pagetables = $2; seen["pagetables"]++ } + $1 == "file_dirty" { dirty = $2; seen["file_dirty"]++ } + $1 == "file_writeback" { writeback = $2; seen["file_writeback"]++ } + END { + if (seen["anon"] != 1 || seen["file"] != 1 || + seen["kernel"] != 1 || seen["pagetables"] != 1 || + seen["file_dirty"] != 1 || seen["file_writeback"] != 1 || + anon !~ /^[0-9]+$/ || file !~ /^[0-9]+$/ || + kernel !~ /^[0-9]+$/ || pagetables !~ /^[0-9]+$/ || + dirty !~ /^[0-9]+$/ || writeback !~ /^[0-9]+$/) { + exit 1 + } + printf "%s\t%s\t%s\t%s\t%s\t%s\n", + anon, file, kernel, pagetables, dirty, writeback + } + ' "$cgroup_dir/memory.stat" + ) + event_metrics="$( + awk ' + $1 == "high" { high = $2; seen["high"]++ } + $1 == "max" { max = $2; seen["max"]++ } + $1 == "oom" { oom = $2; seen["oom"]++ } + $1 == "oom_kill" { oom_kill = $2; seen["oom_kill"]++ } + END { + if (seen["high"] != 1 || seen["max"] != 1 || + seen["oom"] != 1 || seen["oom_kill"] != 1 || + high !~ /^[0-9]+$/ || max !~ /^[0-9]+$/ || + oom !~ /^[0-9]+$/ || oom_kill !~ /^[0-9]+$/) { + exit 1 + } + printf "%s\t%s\t%s\t%s\n", high, max, oom, oom_kill + } + ' "$cgroup_dir/memory.events" + )" || return 125 + IFS=$'\t' read -r event_high event_max event_oom event_oom_kill \ + <<<"$event_metrics" + pressure_metrics="$( + awk ' + ($1 == "some" || $1 == "full") { + kind = $1 + seen[kind]++ + for (index = 2; index <= NF; index++) { + split($index, part, "=") + if (part[1] == "total" && part[2] ~ /^[0-9]+$/) { + total[kind] = part[2] + } + } + } + END { + if (seen["some"] != 1 || seen["full"] != 1 || + total["some"] !~ /^[0-9]+$/ || total["full"] !~ /^[0-9]+$/) { + exit 1 + } + printf "%s\t%s\n", total["some"], total["full"] + } + ' "$cgroup_dir/memory.pressure" + )" || return 125 + IFS=$'\t' read -r psi_some psi_full <<<"$pressure_metrics" + for value in \ + "$current" "$peak" "$swap" "$anon" "$file" "$kernel" \ + "$cgroup_pagetables" "$dirty" "$writeback" + do + case "$value" in ""|*[!0-9]*) return 125 ;; esac + done + monotonic="$(now_ns)" + printf '1\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$epoch" "$epoch_origin_ns" "$monotonic" "$phase" "$process_count" \ + "$pss_kib" "$pss_anon_kib" "$private_kib" "$pagetables_kib" \ + "$current" "$peak" "$swap" "$anon" "$file" "$kernel" \ + "$cgroup_pagetables" "$dirty" "$writeback" \ + "$event_high" "$event_max" "$event_oom" "$event_oom_kill" \ + "$psi_some" "$psi_full" >>"$output" +} + +memory_sampler_loop() { + local cgroup_dir="$1" + local cgroup_identity="$2" + local epoch="$3" + local epoch_origin_ns="$4" + local phase_file="$5" + local stop_file="$6" + local output="$7" + local phase sample_status + while [ ! -e "$stop_file" ]; do + [ -d "$cgroup_dir" ] || return 0 + phase="$(<"$phase_file")" + set +e + collect_memory_sample \ + "$cgroup_dir" "$cgroup_identity" "$epoch" "$epoch_origin_ns" \ + "$phase" "$output" + sample_status=$? + set -e + case "$sample_status" in + 0) ;; + 3) [ -d "$cgroup_dir" ] || return 0 ;; + *) return "$sample_status" ;; + esac + sleep 0.1 + done +} + +memory_phase_file="" +start_memory_sampler() { + local epoch="$1" + local phase="$2" + local output="$3" + [ -n "$active_cgroup_dir" ] && [ -n "$active_epoch_origin_ns" ] || + return 125 + if [ ! -e "$output" ]; then + printf 'schema_version\tepoch\tepoch_origin_monotonic_ns\tmonotonic_ns\tphase\tprocess_count\tpss_kib\tpss_anon_kib\tprivate_kib\tpagetables_kib\tcgroup_current_bytes\tcgroup_peak_bytes\tcgroup_swap_bytes\tcgroup_anon_bytes\tcgroup_file_bytes\tcgroup_kernel_bytes\tcgroup_pagetables_bytes\tcgroup_file_dirty_bytes\tcgroup_file_writeback_bytes\tevent_high\tevent_max\tevent_oom\tevent_oom_kill\tpsi_some_total_usec\tpsi_full_total_usec\n' >"$output" + fi + memory_phase_file="$run_root/memory-phase-e$epoch" + memory_stop_file="$run_root/memory-stop-e$epoch" + [ ! -e "$memory_stop_file" ] || return 125 + printf '%s\n' "$phase" >"$memory_phase_file" + memory_sampler_loop \ + "$active_cgroup_dir" "$active_cgroup_identity" "$epoch" \ + "$active_epoch_origin_ns" "$memory_phase_file" "$memory_stop_file" \ + "$output" & + memory_sampler_pid="$!" + sleep 0.3 + kill -0 "$memory_sampler_pid" 2>/dev/null +} + +set_memory_phase() { + local phase="$1" + [ -n "$memory_sampler_pid" ] && [ -n "$memory_phase_file" ] || return 125 + printf '%s\n' "$phase" >"$memory_phase_file" + sleep 0.3 +} + +stop_memory_sampler() { + local status + [ -n "$memory_sampler_pid" ] || return 0 + touch "$memory_stop_file" + set +e + wait "$memory_sampler_pid" + status=$? + set -e + memory_sampler_pid="" + memory_stop_file="" + memory_phase_file="" + [ "$status" -eq 0 ] +} + +snapshot_wal() { + local pgdata="$1" + local label="$2" + local output="$3" + local name path ordinal=0 metrics size device inode + case "$label" in + before-steady|after-steady|after-volume|plateau-1|plateau-2|plateau-3) ;; + *) return 125 ;; + esac + if [ ! -e "$output" ]; then + printf 'schema_version\tsnapshot\tordinal\tname\tsize\tdevice\tinode\n' \ + >"$output" + fi + while IFS= read -r name; do + [ -n "$name" ] || continue + path="$pgdata/pg_wal/$name" + [ -f "$path" ] && [ ! -L "$path" ] || return 125 + metrics="$(stat -c '%s %d %i' "$path")" + read -r size device inode <<<"$metrics" + ordinal=$((ordinal + 1)) + printf '1\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$label" "$ordinal" "$name" "$size" "$device" "$inode" >>"$output" + done < <( + find "$pgdata/pg_wal" -maxdepth 1 -type f -printf '%f\n' | + awk 'length($0) == 24 && $0 ~ /^[0-9A-F]+$/ { print }' | + LC_ALL=C sort + ) + [ "$ordinal" -gt 0 ] +} + +wal_checkpoint_completed() { + local server_log="$1" + local first_line="$2" + awk -v first_line="$first_line" ' + NR < first_line { next } + /checkpoint starting: wal/ { pending = 1 } + pending && /checkpoint complete:/ { + completed = 1 + pending = 0 + } + END { exit !completed } + ' "$server_log" +} + +run_volume_checkpoint() { + local conn="$1" + local output="$2" + local server_log="$3" + local deadline first_line + first_line=$(( $(wc -l <"$server_log") + 1 )) + psql_script "$conn" "$output" "$workload_volume_sql" + deadline=$(( $(fresh_supervision_now_ms) + timeout_seconds * 1000 )) + while ! wal_checkpoint_completed "$server_log" "$first_line"; do + [ "$(fresh_supervision_now_ms)" -lt "$deadline" ] || return 124 + sleep 0.1 + done +} + +run_plateau_checkpoints() { + local conn="$1" + local pgdata="$2" + local snapshots="$3" + local report_dir="$4" + local index + for index in 1 2 3; do + psql_file "$conn" "$report_dir/plateau-$index.checkpoint.tsv" "CHECKPOINT;" + snapshot_wal "$pgdata" "plateau-$index" "$snapshots" + done +} + +run_periodic_sample() { + local target="$1" + local block="$2" + local pair="$3" + local position="$4" + local sample_label="$5" + local port="$6" + local sample_run="$run_root/$sample_label" + local sample_report="$report_root/samples/$sample_label" + local pgdata="$sample_run/pgdata" + local dev_shm="$sample_run/dev-shm" + local server_log="$sample_report/server.log" + local conn settings_sha sample_status p95_ns p99_ns summary_fields + local time_checkpoint_baseline + local sequence_offset + + current_stage="paired:$sample_label:setup" + assert_frozen_inputs + mkdir -p "$sample_run" "$sample_report" "$dev_shm" + start_server \ + "$target" "$pgdata" "$dev_shm" "$port" "$server_log" \ + 1 "$sample_label" 0 0 + conn="$(connection_uri "$target" "$port")" + wait_ready "$conn" "$sample_report/readiness.log" + verify_online_data_checksums \ + "$conn" "$sample_report/data-checksums-online.tsv" + setup_workload "$conn" "$sample_report/workload-setup.tsv" + capture_settings "$conn" "$sample_report/effective-settings.tsv" + fresh_validate_postgres_profile_settings \ + "$sample_report/effective-settings.tsv" \ + "$sample_report/postgres-profile-validation.tsv" + capture_full_stats "$conn" "$sample_report/full-stats-before.tsv" + capture_checkpoint_state "$conn" "$sample_report/checkpoint-before.tsv" + time_checkpoint_baseline="$(completed_time_checkpoints "$server_log")" + + current_stage="paired:$sample_label:open-loop" + sequence_offset=$((block * 1000000000 + position * 10000000)) + run_probe "$conn" "$sample_report" "$duration_seconds" "$sequence_offset" + wait_for_periodic_checkpoints \ + "$server_log" "$time_checkpoint_baseline" \ + "$sample_report/time-checkpoint-log-delta.tsv" + capture_checkpoint_state "$conn" "$sample_report/checkpoint-after.tsv" + capture_full_stats "$conn" "$sample_report/full-stats-after.tsv" + validate_periodic_probe "$target" "$sample_report" "$server_log" \ + "$duration_seconds" + + current_stage="paired:$sample_label:smart-shutdown" + stop_smart "$sample_report/smart-shutdown.tsv" + if [ "$target" = wasix ]; then + validate_postgres_cache_telemetry "$server_log" + fi + summary_fields="$( + python3 "$FRESH_ROOT/bin/extract-checkpoint-summary.py" \ + "$sample_report/checkpoint-summary.tsv" + )" + IFS=$'\t' read -r sample_status p95_ns p99_ns <<<"$summary_fields" + settings_sha="$(fresh_wasmer_bin_hash "$sample_report/effective-settings.tsv")" + printf '%s\t%s\t%s\t%s\t%s\t0\t%s\t%s\t%s\t%s\t%s\n' \ + "$block" "$pair" "$position" "$target" "$sample_label" \ + "$sample_status" "$p95_ns" "$p99_ns" "$sample_report" "$settings_sha" \ + >>"$report_root/samples.tsv" + assert_frozen_inputs + if [ "$keep_pgdata" -eq 0 ]; then + fresh_require_managed_generated_path "$sample_run" \ + "checkpoint paired sample run root" + rm -rf -- "$sample_run" + fi +} + +record_cgroup_epoch() { + local epoch="$1" + local output="$2" + local memory_max memory_high swap_max + memory_max="$(<"$active_cgroup_dir/memory.max")" + memory_high="$(<"$active_cgroup_dir/memory.high")" + swap_max="$(<"$active_cgroup_dir/memory.swap.max")" + if [ ! -e "$output" ]; then + printf 'schema_version\tepoch\tepoch_origin_monotonic_ns\tcgroup_path\tcgroup_identity\tmemory_max_bytes\tmemory_high_bytes\tmemory_swap_max_bytes\n' \ + >"$output" + fi + printf '1\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$epoch" "$active_epoch_origin_ns" "$active_cgroup_dir" \ + "$active_cgroup_identity" "$memory_max" "$memory_high" "$swap_max" \ + >>"$output" +} + +run_standalone_wasix() { + local sample_label="$run_label-standalone-wasix" + local sample_run="$run_root/$sample_label" + local sample_report="$report_root/standalone-wasix" + local pgdata="$sample_run/pgdata" + local dev_shm="$sample_run/dev-shm" + local server_log="$sample_report/server-epoch-1.log" + local restart_log="$sample_report/server-epoch-2.log" + local conn snapshots memory_samples epoch_receipt + local old_pid old_identity old_cgroup_dir old_cgroup_identity + local old_epoch_origin old_state_sha new_state_sha time_checkpoint_baseline + local checksum_status checksum_result checksum_log_sha256 + local sequence_offset=7000000000 + + mkdir -p "$sample_run" "$sample_report/second-steady" "$dev_shm" + snapshots="$sample_report/wal-snapshots.tsv" + memory_samples="$sample_report/memory-samples.tsv" + epoch_receipt="$sample_report/cgroup-epochs.tsv" + + current_stage="standalone:epoch-1-start" + start_server \ + wasix "$pgdata" "$dev_shm" "$start_port" "$server_log" \ + 1 "$sample_label" 1 1 + conn="$(connection_uri wasix "$start_port")" + wait_ready "$conn" "$sample_report/readiness-epoch-1.log" + verify_online_data_checksums \ + "$conn" "$sample_report/data-checksums-online-epoch-1.tsv" + record_cgroup_epoch 1 "$epoch_receipt" + setup_workload "$conn" "$sample_report/workload-setup.tsv" + capture_settings "$conn" "$sample_report/effective-settings.tsv" + fresh_validate_postgres_profile_settings \ + "$sample_report/effective-settings.tsv" \ + "$sample_report/postgres-profile-validation.tsv" + snapshot_wal "$pgdata" before-steady "$snapshots" + + current_stage="standalone:initial-quiescence" + start_memory_sampler 1 initial-quiescent "$memory_samples" + sleep 2 + + current_stage="standalone:steady" + set_memory_phase steady + capture_full_stats "$conn" "$sample_report/full-stats-before-steady.tsv" + capture_checkpoint_state "$conn" "$sample_report/checkpoint-before.tsv" + time_checkpoint_baseline="$(completed_time_checkpoints "$server_log")" + run_probe "$conn" "$sample_report" "$duration_seconds" "$sequence_offset" + wait_for_periodic_checkpoints \ + "$server_log" "$time_checkpoint_baseline" \ + "$sample_report/time-checkpoint-log-delta.tsv" + capture_checkpoint_state "$conn" "$sample_report/checkpoint-after.tsv" + capture_full_stats "$conn" "$sample_report/full-stats-after-steady.tsv" + validate_periodic_probe wasix "$sample_report" "$server_log" \ + "$duration_seconds" + snapshot_wal "$pgdata" after-steady "$snapshots" + + current_stage="standalone:volume-checkpoint" + set_memory_phase volume-checkpoint + capture_full_stats "$conn" "$sample_report/full-stats-before-volume.tsv" + run_volume_checkpoint \ + "$conn" "$sample_report/volume-transaction.tsv" "$server_log" + snapshot_wal "$pgdata" after-volume "$snapshots" + run_plateau_checkpoints "$conn" "$pgdata" "$snapshots" "$sample_report" + capture_full_stats "$conn" "$sample_report/full-stats-after-volume.tsv" + capture_database_state "$conn" "$sample_report/database-state-before.tsv" + old_state_sha="$(fresh_wasmer_bin_hash "$sample_report/database-state-before.tsv")" + + current_stage="standalone:smart-recycle-shutdown" + old_pid="$active_pid" + old_identity="$active_identity" + old_cgroup_dir="$active_cgroup_dir" + old_cgroup_identity="$active_cgroup_identity" + old_epoch_origin="$active_epoch_origin_ns" + set_memory_phase recycle-shutdown + stop_smart "$sample_report/smart-shutdown-epoch-1.tsv" + stop_memory_sampler + validate_postgres_cache_telemetry "$server_log" + + current_stage="standalone:recycle-startup" + start_server \ + wasix "$pgdata" "$dev_shm" "$start_port" "$restart_log" \ + 0 "$sample_label" 2 1 + conn="$(connection_uri wasix "$start_port")" + wait_ready "$conn" "$sample_report/readiness-epoch-2.log" + verify_online_data_checksums \ + "$conn" "$sample_report/data-checksums-online-epoch-2.tsv" + record_cgroup_epoch 2 "$epoch_receipt" + [ "$active_identity" != "$old_identity" ] && + [ "$active_cgroup_dir" != "$old_cgroup_dir" ] && + [ "$active_cgroup_identity" != "$old_cgroup_identity" ] || + return 125 + grep -Fq 'database system was shut down at' "$restart_log" + if grep -Eq \ + 'database system was interrupted|redo starts at|redo done at|automatic recovery' \ + "$restart_log"; then + return 1 + fi + start_memory_sampler 2 recycle-startup "$memory_samples" + capture_database_state "$conn" "$sample_report/database-state-after.tsv" + new_state_sha="$(fresh_wasmer_bin_hash "$sample_report/database-state-after.tsv")" + [ "$new_state_sha" = "$old_state_sha" ] && + cmp -s \ + "$sample_report/database-state-before.tsv" \ + "$sample_report/database-state-after.tsv" + { + printf 'schema_version\told_pid\told_birth_identity\tnew_pid\tnew_birth_identity\told_cgroup_path\told_cgroup_identity\tnew_cgroup_path\tnew_cgroup_identity\told_epoch_origin_monotonic_ns\tnew_epoch_origin_monotonic_ns\tdatabase_state_sha256\tclean_shutdown\tcrash_recovery\tstate_exact\n' + printf 'oliphaunt.wasix-postmaster.clean-recycle.v1\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\ttrue\tfalse\ttrue\n' \ + "$old_pid" "$old_identity" "$active_pid" "$active_identity" \ + "$old_cgroup_dir" "$old_cgroup_identity" \ + "$active_cgroup_dir" "$active_cgroup_identity" \ + "$old_epoch_origin" "$active_epoch_origin_ns" "$new_state_sha" + } >"$sample_report/clean-recycle.tsv" + + current_stage="standalone:second-steady" + set_memory_phase second-steady + capture_checkpoint_state \ + "$conn" "$sample_report/second-steady/checkpoint-before.tsv" + time_checkpoint_baseline="$(completed_time_checkpoints "$restart_log")" + run_probe \ + "$conn" "$sample_report/second-steady" "$duration_seconds" \ + "$((sequence_offset + 1000000000))" + wait_for_periodic_checkpoints \ + "$restart_log" "$time_checkpoint_baseline" \ + "$sample_report/second-steady/time-checkpoint-log-delta.tsv" + capture_checkpoint_state \ + "$conn" "$sample_report/second-steady/checkpoint-after.tsv" + validate_periodic_probe \ + wasix "$sample_report/second-steady" "$restart_log" "$duration_seconds" + + current_stage="standalone:post-recycle-quiescence" + set_memory_phase post-recycle-quiescent + sleep "$post_recycle_quiescence_seconds" + stop_smart "$sample_report/smart-shutdown-epoch-2.tsv" + stop_memory_sampler + validate_postgres_cache_telemetry "$restart_log" + + current_stage="standalone:offline-checksums" + set +e + run_timed "$sample_report/pg-checksums.log" \ + "$pg_checksums_bin" --check -D "$pgdata" + checksum_status=$? + set -e + checksum_result=failed + if [ "$checksum_status" -eq 0 ]; then + checksum_result=passed + fi + checksum_log_sha256="$(fresh_wasmer_bin_hash "$sample_report/pg-checksums.log")" + { + printf 'schema_version\tstatus\texit_status\ttool_path\ttool_sha256\tpgdata\tlog\tlog_sha256\n' + printf 'oliphaunt.wasix-postmaster.offline-checksums.v1\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$checksum_result" "$checksum_status" \ + "$pg_checksums_bin" "$pg_checksums_sha256" \ + "$pgdata" "$sample_report/pg-checksums.log" "$checksum_log_sha256" + } >"$sample_report/pg-checksums.tsv" + [ "$checksum_status" -eq 0 ] + grep -Fq 'Checksum operation completed' "$sample_report/pg-checksums.log" + + current_stage="standalone:validate-memory-wal" + python3 "$FRESH_ROOT/bin/validate-checkpoint-memory.py" \ + --samples "$memory_samples" \ + --epochs "$epoch_receipt" \ + --output "$sample_report/memory-summary.tsv" \ + --gates-output "$sample_report/memory-gates.tsv" + python3 "$FRESH_ROOT/bin/validate-wal-recycle.py" \ + --snapshots "$snapshots" \ + --server-log "$server_log" \ + --output "$sample_report/wal-summary.tsv" \ + --gates-output "$sample_report/wal-gates.tsv" + memory_status=passed +} + +# Every paired row is a fresh postmaster. Positions form two adjacent +# native/WASIX pairs; block parity reverses the order to balance drift. +printf 'block\tpair\tposition\ttarget\tlabel\tharness_status\tsample_status\tp95_ns\tp99_ns\treport_dir\tsettings_sha256\n' \ + >"$report_root/samples.tsv" +for ((block = 1; block <= blocks; block++)); do + if [ $((block % 2)) -eq 1 ]; then + order=ABBA + target_order=(native wasix wasix native) + else + order=BAAB + target_order=(wasix native native wasix) + fi + for position_index in "${!target_order[@]}"; do + position=$((position_index + 1)) + pair=$(((position_index / 2) + 1)) + target="${target_order[$position_index]}" + sample_label="$(printf '%s-b%02d-%s-p%d-%s' \ + "$run_label" "$block" "$order" "$position" "$target")" + printf 'checkpoint block=%s order=%s pair=%s position=%s target=%s\n' \ + "$block" "$order" "$pair" "$position" "$target" + run_periodic_sample \ + "$target" "$block" "$pair" "$position" "$sample_label" "$start_port" + done +done + +memory_status=not-run +if [ "$skip_memory_sample" -eq 0 ]; then + run_standalone_wasix +fi +assert_frozen_inputs + +fresh_capture_qualification_carrier_identity "$sealed_carrier" && + [ "$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" = "$carrier_identity" ] && + [ "$FRESH_QUALIFICATION_CORE_PROFILE" = "$core_profile" ] && + [ "$FRESH_QUALIFICATION_GUEST_BUILD_RECIPE_SHA256" = \ + "$guest_build_recipe_sha256" ] || { + echo 'sealed carrier failed the campaign-end full verification' >&2 + exit 125 +} + +current_stage=sealed-loader-validation +python3 "$loader_validator" \ + --audit "$sealed_loader_audit" \ + --manifest "$sealed_carrier/manifest.json" \ + --output "$sealed_loader_validation" \ + --required-snapshot-mode direct-immutable-inode \ + --expected-initdb-executions "$expected_initdb_executions" \ + --expected-postgres-executions "$expected_postgres_executions" +chmod 0444 "$sealed_loader_audit" "$sealed_loader_validation" +assert_frozen_inputs + +current_stage=cache-telemetry-validation +observed_cache_observe_validations="$( + find "$report_root" -type f -name '*.cache-offers-validation.tsv' -print | wc -l +)" +observed_cache_adaptive_validations="$( + find "$report_root" -type f -name '*.cache-offers-adaptive-validation.tsv' -print | wc -l +)" +[ "$observed_cache_observe_validations" -eq \ + "$expected_cache_observe_validations" ] && + [ "$observed_cache_adaptive_validations" -eq \ + "$expected_cache_adaptive_validations" ] || { + printf 'cache telemetry validation population differs: observe=%s/%s adaptive=%s/%s\n' \ + "$observed_cache_observe_validations" \ + "$expected_cache_observe_validations" \ + "$observed_cache_adaptive_validations" \ + "$expected_cache_adaptive_validations" >&2 + exit 1 +} +cache_adaptive_active_count=0 +cache_adaptive_fallback_count=0 +while IFS= read -r adaptive_validation_file; do + adaptive_outcome="$(awk -F '\t' 'NR == 2 { print $3 }' \ + "$adaptive_validation_file")" + case "$adaptive_outcome" in + adaptive-active) + cache_adaptive_active_count=$((cache_adaptive_active_count + 1)) + ;; + observe-only-fallback) + cache_adaptive_fallback_count=$((cache_adaptive_fallback_count + 1)) + ;; + *) + printf 'unknown adaptive validation outcome %s in %s\n' \ + "$adaptive_outcome" "$adaptive_validation_file" >&2 + exit 1 + ;; + esac +done < <( + find "$report_root" -type f \ + -name '*.cache-offers-adaptive-validation.tsv' -print | LC_ALL=C sort +) +[ $((cache_adaptive_active_count + cache_adaptive_fallback_count)) -eq \ + "$observed_cache_adaptive_validations" ] || { + echo 'adaptive cache admission outcomes do not cover the validation population' >&2 + exit 1 +} +cache_validation_identity="$( + find "$report_root" -type f \ + \( -name '*.cache-offers-validation.tsv' -o \ + -name '*.cache-offers-adaptive-validation.tsv' \) -print | + LC_ALL=C sort | + while IFS= read -r validation_file; do + fresh_wasmer_bin_hash "$validation_file" + done | + fresh_sha256_stream +)" + +current_stage=aggregate +set +e +python3 "$FRESH_ROOT/bin/summarize-checkpoint-qualification.py" \ + --samples "$report_root/samples.tsv" \ + --output "$report_root/paired-summary.tsv" \ + --result "$report_root/qualification-result.tsv" \ + --mode "$mode" \ + --blocks "$blocks" \ + --policy-sha256 "$policy_identity" \ + --carrier-identity "$carrier_identity" \ + --native-identity "$native_identity" \ + --memory-status "$memory_status" +summary_status=$? +set -e + +{ + printf '# WASIX checkpoint/recycle qualification\n\n' + printf -- '- Status: `%s`\n' \ + "$([ "$summary_status" -eq 0 ] && printf passed || printf failed)" + printf -- '- Classification: `candidate-unqualified-%s-non-release`\n' "$mode" + printf -- '- Fresh-server order: alternating `ABBA` / `BAAB`\n' + printf -- '- Fixed offers: `%s clients x %s TPS/client x %s seconds`\n' \ + "$connections" "$tps_per_client" "$duration_seconds" + printf -- '- Checkpoint policy: `%s` (`%s`)\n' \ + "$checkpoint_policy" "$checkpoint_policy_sha256" + printf -- '- Pair receipts: `%s`\n' "$report_root/samples.tsv" + printf -- '- Pair summary: `%s`\n' "$report_root/paired-summary.tsv" + printf -- '- Standalone recycle evidence: `%s`\n' \ + "$report_root/standalone-wasix" + printf -- '- Immutable deployment receipt: `%s` (`%s`, dev `%s`, ino `%s`)\n' \ + "$immutable_carrier_receipt" "$immutable_receipt_sha256" \ + "$immutable_receipt_dev" "$immutable_receipt_ino" + printf -- '- Direct immutable loader audit / validation: `%s` / `%s`\n' \ + "$sealed_loader_audit" "$sealed_loader_validation" + printf -- '- Cache telemetry policy / validation identity: `%s` / `%s`\n' \ + "$cache_telemetry_policy" "$cache_validation_identity" + printf -- '- Adaptive admission outcomes: `%s active / %s observe-only fallback`\n' \ + "$cache_adaptive_active_count" "$cache_adaptive_fallback_count" + printf -- '- Standalone cgroup policy: `%s` (`%s`)\n' \ + "$standalone_cgroup_policy" "$standalone_cgroup_policy_sha256" + printf -- '- Bound evidence envelope: `%s`\n' "$sealed_loader_envelope" + printf -- '- Claim boundary: no product promotion or embedded-default change\n' +} >"$report_root/summary.md" + +printf 'schema_version\tstatus\tcarrier_closure_identity\timmutable_receipt_sha256\timmutable_receipt_dev\timmutable_receipt_ino\tqualification_policy_sha256\tsealed_loader_audit_sha256\tsealed_loader_validation_sha256\tcache_telemetry_policy_sha256\tcache_validation_identity\tcache_observe_validation_count\tcache_adaptive_validation_count\tcache_adaptive_active_count\tcache_adaptive_fallback_count\tstandalone_cgroup_policy_sha256\tqualification_result_sha256\tcore_profile\tguest_build_recipe_sha256\timmutable_verification_scope\n' \ + >"$sealed_loader_envelope" +printf 'oliphaunt.wasix-postmaster.checkpoint-evidence-envelope.v3\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$([ "$summary_status" -eq 0 ] && printf passed || printf failed)" \ + "$carrier_identity" "$immutable_receipt_sha256" \ + "$immutable_receipt_dev" "$immutable_receipt_ino" "$policy_identity" \ + "$(fresh_wasmer_bin_hash "$sealed_loader_audit")" \ + "$(fresh_wasmer_bin_hash "$sealed_loader_validation")" \ + "$cache_telemetry_policy_sha256" "$cache_validation_identity" \ + "$observed_cache_observe_validations" \ + "$observed_cache_adaptive_validations" \ + "$cache_adaptive_active_count" "$cache_adaptive_fallback_count" \ + "$standalone_cgroup_policy_sha256" \ + "$(fresh_wasmer_bin_hash "$report_root/qualification-result.tsv")" \ + "$core_profile" "$guest_build_recipe_sha256" \ + campaign-boundary-full-fast-samples \ + >>"$sealed_loader_envelope" +chmod 0444 "$sealed_loader_envelope" + +if [ "$summary_status" -eq 0 ]; then + qualification_status=passed + current_stage=complete + if [ "$keep_pgdata" -eq 0 ]; then + fresh_require_managed_generated_path "$run_root" "checkpoint run root" + rm -rf -- "$run_root" + fi + printf 'passed: candidate checkpoint/recycle qualification; see %s\n' \ + "$report_root/summary.md" +else + current_stage=aggregate-failed + printf 'failed: checkpoint/recycle qualification; see %s\n' \ + "$report_root/summary.md" >&2 + exit "$summary_status" +fi diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-checkpoint-recycle.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-checkpoint-recycle.test.sh new file mode 100755 index 00000000..4613ee9c --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-checkpoint-recycle.test.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +runner="$project_root/bin/qualify-wasix-checkpoint-recycle.sh" +fixture="$(mktemp -d)" +trap 'rm -rf -- "$fixture"' EXIT +mkdir -p "$fixture/carrier" + +"$runner" --print-plan >"$fixture/smoke.tsv" +grep -Fxq $'mode\tsmoke' "$fixture/smoke.tsv" +grep -Fxq $'blocks\t1' "$fixture/smoke.tsv" +grep -Fxq $'duration_seconds\t40' "$fixture/smoke.tsv" +grep -Fxq $'classification\tcandidate-unqualified-non-release' \ + "$fixture/smoke.tsv" + +"$runner" --mode diagnostic --print-plan >"$fixture/diagnostic.tsv" +grep -Fxq $'blocks\t3' "$fixture/diagnostic.tsv" +grep -Fxq $'duration_seconds\t240' "$fixture/diagnostic.tsv" +grep -Fxq $'min_wal_bytes\t536870912' "$fixture/diagnostic.tsv" +grep -Fxq $'min_checkpoints\t6' "$fixture/diagnostic.tsv" +grep -Fxq $'cgroup_memory_max\t256M' "$fixture/diagnostic.tsv" +grep -Fxq $'cgroup_memory_high\t224M' "$fixture/diagnostic.tsv" +grep -Fxq $'cgroup_swap_max\t0' "$fixture/diagnostic.tsv" + +"$runner" --cgroup-memory-max 384M --cgroup-memory-high 320MiB \ + --cgroup-swap-max 64M --print-plan >"$fixture/cgroup.tsv" +grep -Fxq $'cgroup_memory_max_bytes\t402653184' "$fixture/cgroup.tsv" +grep -Fxq $'cgroup_memory_high_bytes\t335544320' "$fixture/cgroup.tsv" +grep -Fxq $'cgroup_swap_max_bytes\t67108864' "$fixture/cgroup.tsv" + +if "$runner" --cgroup-memory-max 256M --cgroup-memory-high 257M \ + --print-plan >"$fixture/high.stdout" 2>"$fixture/high.stderr"; then + echo 'plan accepted MemoryHigh above MemoryMax' >&2 + exit 1 +fi +grep -Fq 'MemoryHigh may not exceed MemoryMax' "$fixture/high.stderr" + +if "$runner" --cgroup-memory-max infinity --print-plan \ + >"$fixture/infinite.stdout" 2>"$fixture/infinite.stderr"; then + echo 'plan accepted an unbounded MemoryMax' >&2 + exit 1 +fi +grep -Fq 'invalid cgroup size: infinity' "$fixture/infinite.stderr" + +if "$runner" --mode promotional --blocks 9 --print-plan \ + >"$fixture/promo.stdout" 2>"$fixture/promo.stderr"; then + echo 'promotional plan accepted fewer than ten balanced blocks' >&2 + exit 1 +fi +grep -Fq 'requires at least 10 balanced blocks' "$fixture/promo.stderr" + +if "$runner" --stagger-us 1000001 --print-plan \ + >"$fixture/stagger.stdout" 2>"$fixture/stagger.stderr"; then + echo 'plan accepted a stagger outside the compiled probe limit' >&2 + exit 1 +fi +grep -Fq 'exceeds the probe limit' "$fixture/stagger.stderr" + +grep -Fq 'fresh postmaster' "$runner" +grep -Fq '"--property=MemoryMax=$cgroup_memory_max"' "$runner" +grep -Fq '"--property=MemoryHigh=$cgroup_memory_high"' "$runner" +grep -Fq '"--property=MemorySwapMax=$cgroup_swap_max"' "$runner" +grep -Fq 'database system was shut down at' "$runner" +grep -Fq 'candidate-unqualified' "$runner" +[ "$(grep -Fc 'run --quiet --disable-cache' "$runner")" -eq 1 ] || { + echo 'qualifier must have exactly one Wasmer run prefix' >&2 + exit 1 +} +[ "$(grep -Fc -- '--data-checksums --no-instructions' "$runner")" -eq 2 ] || { + echo 'both native and WASIX initdb must enable data checksums' >&2 + exit 1 +} +[ "$(grep -Fc 'verify_online_data_checksums' "$runner")" -eq 4 ] || { + echo 'online checksum verification must cover both pairs and recycle epochs' >&2 + exit 1 +} +grep -Fq '"$pg_checksums_bin" --check -D "$pgdata"' "$runner" +grep -Fq "grep -Fq 'Checksum operation completed'" "$runner" +grep -Fq 'extract-checkpoint-summary.py' "$runner" +grep -Fq 'OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1' "$runner" +grep -Fq -- '--required-snapshot-mode direct-immutable-inode' "$runner" +grep -Fq 'checkpoint-qualification-policy.v3' "$runner" +grep -Fq 'validate-adaptive-file-cache-telemetry.py' "$runner" +grep -Fq 'Path::with_extension("adaptive.json")' "$runner" +grep -Fq 'checkpoint-evidence-envelope.v3' "$runner" +grep -Fq 'standalone-cgroup-policy.v1' "$runner" +if grep -Fq 'OLIPHAUNT_WASIX_ADAPTIVE_CACHE_TELEMETRY_FILE' "$runner"; then + echo 'checkpoint qualifier added an adaptive policy environment surface' >&2 + exit 1 +fi +if "$runner" --sealed-carrier "$fixture/carrier" \ + >"$fixture/missing-receipt.stdout" 2>"$fixture/missing-receipt.stderr"; then + echo 'executable checkpoint qualifier accepted no immutable receipt' >&2 + exit 1 +fi +grep -Fq -- '--immutable-carrier-receipt is required' \ + "$fixture/missing-receipt.stderr" + +printf 'checkpoint/recycle qualifier tests passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-cold-ownership.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-cold-ownership.sh new file mode 100755 index 00000000..a082df92 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-cold-ownership.sh @@ -0,0 +1,350 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" +source "$FRESH_ROOT/lib/sealed-carrier.sh" +source "$FRESH_ROOT/lib/qualification-identities.sh" + +usage() { + cat <<'USAGE' +Usage: qualify-wasix-cold-ownership.sh --sealed-carrier DIR --immutable-carrier-receipt FILE [options] + +Repeat independently initialized WASIX cold postmaster launches. Each block +uses targeted per-file eviction plus a mincore zero-residency proof immediately +before launch, then validates first-query latency and whole-cgroup memory, +dirty/writeback, pressure, and explicitly statused I/O evidence. A delegated +cgroup without io.stat remains valid memory/cold evidence but makes no I/O +first-touch claim. This never writes drop_caches. + +Options: + --sealed-carrier DIR Compiler-free, read-only carrier. Required. + --immutable-carrier-receipt FILE + External Linux immutable-deployment receipt. + Required; the cold lane always enforces direct + immutable, zero-write AOT/image activation. + --blocks N Independent cold launches. Default: 10; minimum: 5. + --timeout SECONDS Initdb/readiness/control timeout. Default: 300. + --start-port PORT Reused after verified clean shutdown. Default: 56020. + --label NAME Default: UTC timestamp. + --runtime-footprint ID Default: embedded-concurrent. + --durability ID Default: safe. + --memory-max SIZE Default: 256M. + --memory-high SIZE Default: 224M. + --swap-max SIZE Default: 0. + --resource-interval S Full sampler interval. Default: 0.05. + --max-p95-ms MS Optional cold spawn-to-first-query p95 ceiling. + --skip-build Require the existing native psql client/install. + --keep-pgdata Retain successful initialized roots. + --print-plan Validate options and print the exact campaign plan. + -h, --help Show this help. + +The result is candidate, non-release evidence. It closes page-cache ownership +ambiguity; it does not by itself promote the research runtime or qualify other +platforms. +USAGE +} + +sealed_carrier="" +immutable_carrier_receipt="" +blocks="${WASIX_COLD_BLOCKS:-10}" +timeout_seconds="${WASIX_COLD_TIMEOUT:-300}" +start_port="${WASIX_COLD_PORT:-56020}" +run_label="${WASIX_COLD_LABEL:-$(date -u +%Y%m%dT%H%M%SZ)}" +runtime_footprint="${WASIX_RUNTIME_FOOTPRINT:-embedded-concurrent}" +durability="${WASIX_DURABILITY_PROFILE:-safe}" +memory_max="${WASIX_COLD_MEMORY_MAX:-256M}" +memory_high="${WASIX_COLD_MEMORY_HIGH:-224M}" +swap_max="${WASIX_COLD_SWAP_MAX:-0}" +resource_interval="${WASIX_COLD_RESOURCE_INTERVAL:-0.05}" +max_p95_ms="${WASIX_COLD_MAX_P95_MS:-}" +skip_build=0 +discard_pgdata=1 +print_plan=0 + +while [ "$#" -gt 0 ]; do + case "$1" in + --sealed-carrier) shift; [ "$#" -gt 0 ] || { echo "--sealed-carrier requires a value" >&2; exit 2; }; sealed_carrier="$1" ;; + --immutable-carrier-receipt) shift; [ "$#" -gt 0 ] || { echo "--immutable-carrier-receipt requires a value" >&2; exit 2; }; [ -z "$immutable_carrier_receipt" ] || { echo "--immutable-carrier-receipt may only be specified once" >&2; exit 2; }; immutable_carrier_receipt="$1" ;; + --blocks) shift; [ "$#" -gt 0 ] || { echo "--blocks requires a value" >&2; exit 2; }; blocks="$1" ;; + --timeout) shift; [ "$#" -gt 0 ] || { echo "--timeout requires a value" >&2; exit 2; }; timeout_seconds="$1" ;; + --start-port) shift; [ "$#" -gt 0 ] || { echo "--start-port requires a value" >&2; exit 2; }; start_port="$1" ;; + --label) shift; [ "$#" -gt 0 ] || { echo "--label requires a value" >&2; exit 2; }; run_label="$1" ;; + --runtime-footprint) shift; [ "$#" -gt 0 ] || { echo "--runtime-footprint requires a value" >&2; exit 2; }; runtime_footprint="$1" ;; + --durability) shift; [ "$#" -gt 0 ] || { echo "--durability requires a value" >&2; exit 2; }; durability="$1" ;; + --memory-max) shift; [ "$#" -gt 0 ] || { echo "--memory-max requires a value" >&2; exit 2; }; memory_max="$1" ;; + --memory-high) shift; [ "$#" -gt 0 ] || { echo "--memory-high requires a value" >&2; exit 2; }; memory_high="$1" ;; + --swap-max) shift; [ "$#" -gt 0 ] || { echo "--swap-max requires a value" >&2; exit 2; }; swap_max="$1" ;; + --resource-interval) shift; [ "$#" -gt 0 ] || { echo "--resource-interval requires a value" >&2; exit 2; }; resource_interval="$1" ;; + --max-p95-ms) shift; [ "$#" -gt 0 ] || { echo "--max-p95-ms requires a value" >&2; exit 2; }; max_p95_ms="$1" ;; + --skip-build) skip_build=1 ;; + --keep-pgdata) discard_pgdata=0 ;; + --print-plan) print_plan=1 ;; + -h|--help) usage; exit 0 ;; + *) printf 'unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +if ! [[ "$blocks" =~ ^[1-9][0-9]*$ ]] || [ "$blocks" -lt 5 ]; then + echo "--blocks requires an integer of at least 5" >&2 + exit 2 +fi +[[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || { echo "--timeout requires a positive integer" >&2; exit 2; } +if ! [[ "$start_port" =~ ^[1-9][0-9]*$ ]] || [ "$start_port" -gt 65535 ]; then + echo "--start-port requires a port from 1 through 65535" >&2 + exit 2 +fi +case "$run_label" in ""|*[!A-Za-z0-9._-]*) echo "--label contains unsupported characters" >&2; exit 2 ;; esac +for value in "$memory_max" "$memory_high" "$swap_max"; do + [[ "$value" =~ ^[0-9]+([KMGTPE]([i]?B)?)?$ ]] || { + printf 'invalid cgroup size: %s\n' "$value" >&2 + exit 2 + } +done +awk -v value="$resource_interval" 'BEGIN { exit !(value > 0) }' || { + echo "--resource-interval requires a positive number" >&2 + exit 2 +} +if [ -n "$max_p95_ms" ]; then + awk -v value="$max_p95_ms" 'BEGIN { exit !(value > 0) }' || { + echo "--max-p95-ms requires a positive number" >&2 + exit 2 + } +fi +[ "$(uname -s)" = Linux ] || { echo "cold ownership qualification requires Linux" >&2; exit 2; } +[ -r /sys/fs/cgroup/cgroup.controllers ] || { echo "cold ownership qualification requires cgroup v2" >&2; exit 2; } +command -v systemd-run >/dev/null 2>&1 || { echo "cold ownership qualification requires systemd-run" >&2; exit 127; } +[ -n "$sealed_carrier" ] || { echo "--sealed-carrier is required" >&2; exit 2; } +[ -n "$immutable_carrier_receipt" ] || { echo "--immutable-carrier-receipt is required" >&2; exit 2; } +if [ ! -d "$sealed_carrier" ] || [ -L "$sealed_carrier" ]; then + printf 'sealed carrier must be a non-symlink directory: %s\n' "$sealed_carrier" >&2 + exit 2 +fi +sealed_carrier="$(cd "$sealed_carrier" && pwd -P)" +receipt_parent="$(dirname "$immutable_carrier_receipt")" +[ -d "$receipt_parent" ] && [ ! -L "$receipt_parent" ] || { + printf 'immutable carrier receipt parent must be a non-symlink directory: %s\n' \ + "$receipt_parent" >&2 + exit 2 +} +immutable_carrier_receipt="$(cd "$receipt_parent" && pwd -P)/$(basename "$immutable_carrier_receipt")" +[ "$(id -u)" -ne 0 ] || { + echo 'cold ownership qualification must run unprivileged after deployment' >&2 + exit 2 +} +cap_eff="$(awk '$1 == "CapEff:" { print $2 }' /proc/self/status)" +[[ "$cap_eff" =~ ^[0-9a-fA-F]+$ ]] || { + echo 'could not read exact CapEff for cold immutable qualification' >&2 + exit 2 +} +if (( (16#$cap_eff & (1 << 9)) != 0 )); then + echo 'cold ownership qualification refuses effective CAP_LINUX_IMMUTABLE' >&2 + exit 2 +fi + +fresh_capture_qualification_carrier_identity "$sealed_carrier" +frozen_carrier_identity="$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" +frozen_core_profile="$FRESH_QUALIFICATION_CORE_PROFILE" +frozen_guest_build_recipe_sha256="$FRESH_QUALIFICATION_GUEST_BUILD_RECIPE_SHA256" +"$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier" \ + --receipt "$immutable_carrier_receipt" --fast +immutable_receipt_sha256="$(fresh_wasmer_bin_hash "$immutable_carrier_receipt")" +read -r immutable_receipt_dev immutable_receipt_ino < <( + stat -c '%d %i' -- "$immutable_carrier_receipt" +) + +printf 'schema_version\toliphaunt.wasix-postmaster.cold-ownership-plan.v2\n' +printf 'classification\tcandidate-unqualified-non-release\n' +printf 'blocks\t%s\n' "$blocks" +printf 'carrier\t%s\n' "$sealed_carrier" +printf 'runtime_footprint\t%s\n' "$runtime_footprint" +printf 'durability\t%s\n' "$durability" +printf 'memory_max\t%s\n' "$memory_max" +printf 'memory_high\t%s\n' "$memory_high" +printf 'swap_max\t%s\n' "$swap_max" +printf 'resource_detail\tfull\n' +printf 'resource_interval\t%s\n' "$resource_interval" +printf 'eviction\tper-file-posix-fadvise-dontneed\n' +printf 'residency_proof\tmincore-zero-pages\n' +printf 'global_drop_caches\tforbidden\n' +printf 'require_zero_write_aot\t1\n' +printf 'required_snapshot_mode\tdirect-immutable-inode\n' +printf 'carrier_closure_identity\t%s\n' "$frozen_carrier_identity" +printf 'immutable_receipt_path\t%s\n' "$immutable_carrier_receipt" +printf 'immutable_receipt_sha256\t%s\n' "$immutable_receipt_sha256" +printf 'immutable_receipt_dev\t%s\n' "$immutable_receipt_dev" +printf 'immutable_receipt_ino\t%s\n' "$immutable_receipt_ino" +printf 'core_profile\t%s\n' "$frozen_core_profile" +printf 'guest_build_recipe_sha256\t%s\n' "$frozen_guest_build_recipe_sha256" +printf 'immutable_verification_scope\tcampaign-boundary-full-fast-samples\n' +printf 'max_p95_ms\t%s\n' "${max_p95_ms:-report-only}" +[ "$print_plan" -eq 0 ] || exit 0 + +qualification_root="$REPORT_DIR/cold-ownership-qualification/$run_label" +qualification_runs="$RUN_DIR/cold-ownership-qualification/$run_label" +if [ -e "$qualification_root" ] || [ -L "$qualification_root" ] || + [ -e "$qualification_runs" ] || [ -L "$qualification_runs" ]; then + printf 'qualification label already exists: %s\n' "$run_label" >&2 + exit 2 +fi +mkdir -p "$qualification_root" "$qualification_runs" +plan="$qualification_root/qualification-plan.tsv" +sealed_loader_verification="$qualification_root/sealed-loader-verification.tsv" +qualification_result="$qualification_root/qualification-result.tsv" +# The plan was already resolved against the one full campaign-start capture. +{ + printf 'schema_version\toliphaunt.wasix-postmaster.cold-ownership-plan.v2\n' + printf 'classification\tcandidate-unqualified-non-release\n' + printf 'blocks\t%s\n' "$blocks" + printf 'carrier\t%s\n' "$sealed_carrier" + printf 'runtime_footprint\t%s\n' "$runtime_footprint" + printf 'durability\t%s\n' "$durability" + printf 'memory_max\t%s\n' "$memory_max" + printf 'memory_high\t%s\n' "$memory_high" + printf 'swap_max\t%s\n' "$swap_max" + printf 'resource_detail\tfull\n' + printf 'resource_interval\t%s\n' "$resource_interval" + printf 'eviction\tper-file-posix-fadvise-dontneed\n' + printf 'residency_proof\tmincore-zero-pages\n' + printf 'global_drop_caches\tforbidden\n' + printf 'require_zero_write_aot\t1\n' + printf 'required_snapshot_mode\tdirect-immutable-inode\n' + printf 'carrier_closure_identity\t%s\n' "$frozen_carrier_identity" + printf 'immutable_receipt_path\t%s\n' "$immutable_carrier_receipt" + printf 'immutable_receipt_sha256\t%s\n' "$immutable_receipt_sha256" + printf 'immutable_receipt_dev\t%s\n' "$immutable_receipt_dev" + printf 'immutable_receipt_ino\t%s\n' "$immutable_receipt_ino" + printf 'core_profile\t%s\n' "$frozen_core_profile" + printf 'guest_build_recipe_sha256\t%s\n' "$frozen_guest_build_recipe_sha256" + printf 'immutable_verification_scope\tcampaign-boundary-full-fast-samples\n' + printf 'max_p95_ms\t%s\n' "${max_p95_ms:-report-only}" +} >"$plan" +plan_identity="$(fresh_wasmer_bin_hash "$plan")" +printf 'block\tpolicy_receipt\tpolicy_sha256\taudit_receipt\taudit_sha256\tvalidation_receipt\tvalidation_sha256\tstatus\n' \ + >"$sealed_loader_verification" + +assert_immutable_inputs() { + local stage="$1" receipt_stat observed_dev observed_ino + "$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier" \ + --receipt "$immutable_carrier_receipt" --fast || { + printf 'sealed carrier identity changed at %s\n' "$stage" >&2 + return 125 + } + receipt_stat="$(stat -c '%d %i' -- "$immutable_carrier_receipt")" || return + read -r observed_dev observed_ino <<<"$receipt_stat" + [ "$observed_dev" = "$immutable_receipt_dev" ] && + [ "$observed_ino" = "$immutable_receipt_ino" ] || { + printf 'immutable deployment receipt inode changed at %s\n' "$stage" >&2 + return 125 + } + [ "$(fresh_wasmer_bin_hash "$plan")" = "$plan_identity" ] || { + printf 'cold ownership plan changed at %s\n' "$stage" >&2 + return 125 + } +} + +sample_inputs=() +for ((block = 1; block <= blocks; block++)); do + assert_immutable_inputs "block-$block:before" + block_root="$qualification_root/blocks/$block" + block_runs="$qualification_runs/blocks/$block" + block_label="cold-$block" + mkdir -p "$block_root" "$block_runs" + bench_args=( + --target wasix + --sealed-carrier "$sealed_carrier" + --require-zero-write-aot + --immutable-carrier-receipt "$immutable_carrier_receipt" + --immutable-carrier-verification-scope campaign-fast + --cold-ownership + --connections 1 + --iterations 1 + --rows 1 + --resource-detail full + --resource-interval "$resource_interval" + --cgroup-memory-max "$memory_max" + --cgroup-memory-high "$memory_high" + --cgroup-swap-max "$swap_max" + --runtime-footprint "$runtime_footprint" + --durability "$durability" + --timeout "$timeout_seconds" + --start-port "$start_port" + --label "$block_label" + ) + [ "$skip_build" -eq 0 ] || bench_args+=(--skip-build) + [ "$discard_pgdata" -eq 0 ] || bench_args+=(--discard-pgdata) + env -u WASIX_PERF_WAIT_DUMP_INTERVAL_MS \ + -u WASIX_PERF_WAIT_DUMP_FILE \ + -u WASIX_PERF_WAIT_DUMP_MAX_PER_WAIT \ + -u WASIX_PERF_WAIT_DUMP_VERBOSE \ + -u WASIX_WAIT_DUMP_INTERVAL_MS \ + -u WASIX_WAIT_DUMP_FILE \ + -u WASIX_WAIT_DUMP_MAX_PER_WAIT \ + -u WASIX_WAIT_DUMP_VERBOSE \ + -u WASIX_WAIT_DUMP_FENCE_REQUEST_FILE \ + -u WASIX_WAIT_DUMP_FENCE_ACK_FILE \ + -u OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT \ + -u OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE \ + REPORT_DIR="$block_root" RUN_DIR="$block_runs" \ + "$FRESH_ROOT/bin/bench-wasix-concurrent-query-suite.sh" "${bench_args[@]}" + sample="$block_root/concurrent-query-suite/$block_label/wasix/cold-ownership-sample.tsv" + [ -s "$sample" ] || { printf 'cold block produced no sample: %s\n' "$sample" >&2; exit 1; } + block_report="$block_root/concurrent-query-suite/$block_label" + loader_policy="$block_report/sealed-loader-policy.tsv" + loader_audit="$block_report/wasix/sealed-loader-audit.jsonl" + loader_validation="$block_report/wasix/sealed-loader-audit-validation.tsv" + if [ ! -f "$loader_policy" ] || [ -L "$loader_policy" ] || + [ ! -f "$loader_audit" ] || [ -L "$loader_audit" ] || + [ ! -f "$loader_validation" ] || [ -L "$loader_validation" ] || + ! awk -F '\t' -v receipt_path="$immutable_carrier_receipt" \ + -v receipt_sha="$immutable_receipt_sha256" \ + -v receipt_dev="$immutable_receipt_dev" \ + -v receipt_ino="$immutable_receipt_ino" \ + -v carrier_identity="$frozen_carrier_identity" \ + 'NR == 2 && $1 == "oliphaunt.wasix-postmaster.sealed-loader-policy.v2" && $3 == 1 && $4 == "campaign-fast" && $5 == "direct-immutable-only" && $9 == "direct-immutable-inode" && $15 == receipt_path && $16 == receipt_sha && $17 == receipt_dev && $18 == receipt_ino && $19 == carrier_identity { ok = 1 } END { exit !(NR == 2 && ok) }' \ + "$loader_policy" || + ! awk -F '\t' 'NR == 2 && $1 == "oliphaunt.wasix-postmaster.sealed-loader-audit-validation.v3" && $2 == "passed" && $6 == 1 && $7 == 1 && $10 == "direct-immutable-inode" { ok = 1 } END { exit !(NR == 2 && ok) }' \ + "$loader_validation"; then + printf 'cold block lacks exact direct immutable loader proof: %s\n' "$block" >&2 + exit 1 + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\tpassed\n' \ + "$block" "$loader_policy" "$(fresh_wasmer_bin_hash "$loader_policy")" \ + "$loader_audit" "$(fresh_wasmer_bin_hash "$loader_audit")" \ + "$loader_validation" "$(fresh_wasmer_bin_hash "$loader_validation")" \ + >>"$sealed_loader_verification" + assert_immutable_inputs "block-$block:after" + sample_inputs+=(--input "$sample") +done + +fresh_capture_qualification_carrier_identity "$sealed_carrier" && + [ "$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" = "$frozen_carrier_identity" ] || { + echo 'sealed carrier failed the campaign-end full verification' >&2 + exit 125 +} + +summary="$qualification_root/summary.tsv" +receipt="$qualification_root/qualification-receipt.json" +summary_args=( + "${sample_inputs[@]}" + --expected-blocks "$blocks" + --output "$summary" + --receipt "$receipt" +) +[ -z "$max_p95_ms" ] || summary_args+=(--max-p95-ms "$max_p95_ms") +python3 "$FRESH_ROOT/bin/summarize-wasix-cold-ownership.py" "${summary_args[@]}" +chmod 0444 "$plan" "$sealed_loader_verification" +printf 'schema_version\tstatus\tcarrier_closure_identity\timmutable_receipt_sha256\timmutable_receipt_dev\timmutable_receipt_ino\tqualification_plan_sha256\tsealed_loader_verification_sha256\tsummary_sha256\tqualification_receipt_sha256\tcore_profile\tguest_build_recipe_sha256\timmutable_verification_scope\n' \ + >"$qualification_result" +printf 'oliphaunt.wasix-postmaster.cold-ownership-result.v2\tpassed\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\tcampaign-boundary-full-fast-samples\n' \ + "$frozen_carrier_identity" "$immutable_receipt_sha256" \ + "$immutable_receipt_dev" "$immutable_receipt_ino" "$plan_identity" \ + "$(fresh_wasmer_bin_hash "$sealed_loader_verification")" \ + "$(fresh_wasmer_bin_hash "$summary")" "$(fresh_wasmer_bin_hash "$receipt")" \ + "$frozen_core_profile" "$frozen_guest_build_recipe_sha256" \ + >>"$qualification_result" +chmod 0444 "$qualification_result" +printf 'cold ownership qualification: %s\n' "$summary" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-cold-ownership.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-cold-ownership.test.sh new file mode 100755 index 00000000..88420dbf --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-cold-ownership.test.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +qualifier="$root/bin/qualify-wasix-cold-ownership.sh" +bench="$root/bin/bench-wasix-concurrent-query-suite.sh" +temp="$(mktemp -d)" +cleanup() { + [ ! -d "$temp/dynamic-carrier" ] || chmod 0755 "$temp/dynamic-carrier" 2>/dev/null || true + [ ! -d "$temp/dynamic-pgdata" ] || chmod 0755 "$temp/dynamic-pgdata" 2>/dev/null || true + chmod -R u+rwX "$temp" 2>/dev/null || true + rm -rf -- "$temp" +} +trap cleanup EXIT HUP INT TERM +mkdir -p "$temp/carrier" + +if "$qualifier" --sealed-carrier "$temp/carrier" --blocks 5 --print-plan \ + >"$temp/missing-receipt.out" 2>"$temp/missing-receipt.err"; then + echo "cold qualifier accepted no immutable deployment receipt" >&2 + exit 1 +fi +grep -Fq -- '--immutable-carrier-receipt is required' \ + "$temp/missing-receipt.err" +grep -Fq -- '--require-zero-write-aot' "$qualifier" +grep -Fq -- '--immutable-carrier-receipt "$immutable_carrier_receipt"' \ + "$qualifier" +grep -Fq 'required_snapshot_mode\tdirect-immutable-inode' "$qualifier" +grep -Fq 'global_drop_caches\tforbidden' "$qualifier" +grep -Fq -- '--immutable-carrier-verification-scope campaign-fast' "$qualifier" +grep -Fq -- '--cold-ownership-workloads' "$bench" +grep -Fq 'cold_ownership_mode=whole-lifecycle' "$bench" +grep -Fq 'oliphaunt.wasix-postmaster.cold-ownership-mode.v1' "$bench" + +if "$qualifier" --sealed-carrier "$temp/carrier" --blocks 4 --print-plan \ + >"$temp/short.out" 2>"$temp/short.err"; then + echo "cold qualifier accepted fewer than five independent blocks" >&2 + exit 1 +fi +grep -Fq 'at least 5' "$temp/short.err" + +if "$bench" --cold-ownership --resource-detail full \ + >"$temp/bench.out" 2>"$temp/bench.err"; then + echo "cold bench accepted missing cgroup limits and sealed carrier" >&2 + exit 1 +fi +grep -Fq 'requires explicit cgroup MemoryMax' "$temp/bench.err" + +# Execute the actual start_wasix_server function with the roots made unreadable +# immediately after the real mincore helper returns. Any accidental verifier, +# hash, stat, or content open before the mocked exec boundary then fails. +sed -n '/^start_wasix_server()/,/^run_client_process()/p' "$bench" | + sed '$d' >"$temp/start-wasix-server.sh" +# shellcheck source=/dev/null +source "$temp/start-wasix-server.sh" +dynamic_carrier="$temp/dynamic-carrier" +dynamic_pgdata="$temp/dynamic-pgdata" +mkdir -p "$dynamic_carrier/bin" "$dynamic_pgdata" +printf 'sealed runtime fixture\n' >"$dynamic_carrier/bin/wasmer-headless" +chmod 0555 "$dynamic_carrier/bin/wasmer-headless" "$dynamic_carrier/bin" \ + "$dynamic_carrier" +dynamic_receipt="$temp/dynamic-cold-receipt.json" +FRESH_ROOT="$root" +sealed_carrier_root="$dynamic_carrier" +sealed_manifest_hash="$(printf manifest | sha256sum | awk '{ print $1 }')" +sealed_receipt_hash="$(printf receipt | sha256sum | awk '{ print $1 }')" +sealed_payload_inventory_hash="$(printf payload | sha256sum | awk '{ print $1 }')" +execution_identity_sha256="$(printf execution | sha256sum | awk '{ print $1 }')" +cold_ownership=1 +wasix_perf_stats=0 +wasix_wait_dump_interval_ms=0 +wasix_lifecycle_plateau=0 +wasix_runtime_mode=sealed-headless +require_zero_write_aot=0 +timeout_seconds=1 +connections=1 +effective_postgres_gucs=() +wasmer_env=() +wasmer_args=() +wasmer_env_command=(env) +wasmer_bin="$dynamic_carrier/bin/wasmer-headless" +wasix_initdb_module="$dynamic_carrier/bin/wasmer-headless" +wasix_postgres_module="$dynamic_carrier/bin/wasmer-headless" +roots_locked=0 +build_wasmer_args() { + wasmer_env=() + wasmer_args=() + wasmer_env_command=(env) +} +run_logged_timeout() { + printf '%s\n' "$*" >"$temp/initdb-command" + mkdir -p "$dynamic_pgdata/base" + printf '18\n' >"$dynamic_pgdata/PG_VERSION" + printf 'relation data\n' >"$dynamic_pgdata/base/1" +} +now_ns() { printf '1000000000\n'; } +python3() { + command python3 "$@" + local status=$? + [ "$status" -eq 0 ] || return "$status" + if [ "$1" = "$FRESH_ROOT/bin/prove-linux-cold-residency.py" ]; then + find "$dynamic_carrier" "$dynamic_pgdata" -type f -exec chmod 000 {} + + roots_locked=1 + fi +} +fresh_spawn_process_group() { + [ "$roots_locked" -eq 1 ] + [ ! -r "$dynamic_carrier/bin/wasmer-headless" ] + [ ! -r "$dynamic_pgdata/PG_VERSION" ] + printf '%s\n' "$*" >"$temp/server-command" + FRESH_PROCESS_GROUP_PID=42 + FRESH_PROCESS_GROUP_PGID=42 + FRESH_PROCESS_GROUP_IDENTITY=fixture:42 +} +start_wasix_server "$dynamic_pgdata" "$temp/dev-shm" 15432 \ + "$temp/initdb.log" "$temp/server.log" "$temp/initdb-perf.log" \ + "$temp/server-perf.log" "$temp/limits" "$temp/fence.request" \ + "$temp/fence.ack" "$dynamic_receipt" "$temp/sealed-loader-audit.jsonl" \ + "$temp/cache-offers-initdb.json" "$temp/cache-offers-postgres.json" +[ "$started_server_pid" = 42 ] && [ -s "$dynamic_receipt" ] +grep -Fq "OLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE=$temp/cache-offers-initdb.json" \ + "$temp/initdb-command" +grep -Fq "OLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE=$temp/cache-offers-postgres.json" \ + "$temp/server-command" +chmod 0755 "$dynamic_carrier" "$dynamic_pgdata" +find "$dynamic_carrier" "$dynamic_pgdata" -depth -exec chmod u+rwX {} + + +if grep -Eq 'drop_caches|malloc_trim' "$qualifier"; then + # The help/plan intentionally names drop_caches as forbidden. Only an actual + # procfs write or allocator call is prohibited. + grep -Eq '/proc/sys/vm/drop_caches|malloc_trim[[:space:]]*\(' "$qualifier" && { + echo "cold qualifier contains a forbidden global cache/allocator operation" >&2 + exit 1 + } +fi + +printf 'cold ownership qualifier policy tests passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-immediate-recovery.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-immediate-recovery.sh new file mode 100755 index 00000000..6d683823 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-immediate-recovery.sh @@ -0,0 +1,1386 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Dynamic product-root sourcing is intentional. +# shellcheck disable=SC1091 +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" +source "$FRESH_ROOT/lib/sealed-carrier.sh" +source "$FRESH_ROOT/lib/postgres-profiles.sh" +source "$FRESH_ROOT/lib/process-supervision.sh" +source "$FRESH_ROOT/lib/server-lifecycle.sh" +source "$FRESH_ROOT/lib/qualification-identities.sh" + +usage() { + cat <<'USAGE' +Usage: qualify-wasix-immediate-recovery.sh --sealed-carrier DIR [options] + +Prove PostgreSQL crash recovery through the compiler-free WASIX postmaster +carrier. The qualifier creates one cluster with the embedded-concurrent and +safe profiles, checkpoints a baseline, records acknowledged post-checkpoint +transactions, delivers host SIGQUIT to the identity-checked Wasmer leader, +and restarts the exact same carrier, PGDATA, and /dev/shm mount. + +Options: + --sealed-carrier DIR Compiler-free sealed carrier. Required. + --mode MODE qualification or diagnostic. Default: + qualification. Qualification mode requires the + immutable/zero-write and finite cgroup controls + below; diagnostic preserves the compatibility + execution path but is non-promotional. + --require-zero-write-aot Require direct activation from immutable AOT and + executable memory-image inodes, then validate the + complete loader audit after the final restart. + --immutable-carrier-receipt FILE + External receipt created by + deploy-immutable-sealed-carrier.sh. Required with + --require-zero-write-aot and forbidden otherwise. + --cgroup-memory-max SIZE Finite MemoryMax for each postmaster server tree. + --cgroup-memory-high SIZE Finite MemoryHigh for each postmaster server tree. + --cgroup-swap-max SIZE Finite MemorySwapMax for each postmaster server + tree; use 0 to forbid swap. The three cgroup + controls are an all-or-none set. + --port PORT TCP port. Default: 55940. + --timeout SECONDS Per operation and lifecycle deadline. Default: 180. + --transactions N Acknowledged post-checkpoint commits. Default: 16. + --rows-per-transaction N Rows inserted by each commit. Default: 128. + --label NAME Evidence label. Default: timestamped. + --keep-pgdata Retain successful PGDATA and /dev/shm evidence. + -h, --help Show this help. + +Success requires all of the following: + * the sealed carrier and named-profile inputs remain byte-identical; + * every commit is observed with WAL flushed under the safe profile; + * no checkpoint occurs after the controlled baseline; + * SIGQUIT reaches PostgreSQL as an immediate shutdown without escalation; + * the Wasmer process group, listener, and shared-object directory drain; + * restart performs WAL redo and reproduces the exact table checksum; + * a bridged SIGTERM performs smart shutdown; and + * a final reopen is clean and reproduces the checksum again. + +Qualification mode additionally requires full immutable-carrier verification +at both campaign boundaries, receipt-bound fast checks before every execution, +an exact population of one outer initdb and three outer postgres executor +invocations, direct-immutable evidence for every module activation (including +initdb's bootstrap postgres and dynamic modules), and proven +MemoryMax/MemoryHigh/MemorySwapMax membership for every server tree. +USAGE +} + +sealed_carrier="" +sealed_carrier_explicit=0 +mode="${WASIX_RECOVERY_MODE:-qualification}" +mode_explicit=0 +require_zero_write_aot=0 +immutable_carrier_receipt="" +cgroup_memory_max="${WASIX_RECOVERY_CGROUP_MEMORY_MAX:-}" +cgroup_memory_high="${WASIX_RECOVERY_CGROUP_MEMORY_HIGH:-}" +cgroup_swap_max="${WASIX_RECOVERY_CGROUP_SWAP_MAX:-}" +cgroup_memory_max_explicit=0 +cgroup_memory_high_explicit=0 +cgroup_swap_max_explicit=0 +port="${WASIX_RECOVERY_PORT:-55940}" +timeout_seconds="${WASIX_RECOVERY_TIMEOUT:-180}" +transaction_count="${WASIX_RECOVERY_TRANSACTIONS:-16}" +rows_per_transaction="${WASIX_RECOVERY_ROWS_PER_TRANSACTION:-128}" +run_label="${WASIX_RECOVERY_LABEL:-$(date -u +%Y%m%dT%H%M%SZ)}" +keep_pgdata=0 + +while [ "$#" -gt 0 ]; do + case "$1" in + --sealed-carrier) + shift + [ "$#" -gt 0 ] || { echo "--sealed-carrier requires a directory" >&2; exit 2; } + [ "$sealed_carrier_explicit" -eq 0 ] || { + echo "--sealed-carrier may only be specified once" >&2 + exit 2 + } + sealed_carrier="$1" + sealed_carrier_explicit=1 + ;; + --mode) + shift + [ "$#" -gt 0 ] || { echo "--mode requires qualification or diagnostic" >&2; exit 2; } + [ "$mode_explicit" -eq 0 ] || { + echo "--mode may only be specified once" >&2 + exit 2 + } + mode="$1" + mode_explicit=1 + ;; + --require-zero-write-aot) + [ "$require_zero_write_aot" -eq 0 ] || { + echo "--require-zero-write-aot may only be specified once" >&2 + exit 2 + } + require_zero_write_aot=1 + ;; + --immutable-carrier-receipt) + shift + [ "$#" -gt 0 ] || { echo "--immutable-carrier-receipt requires a file" >&2; exit 2; } + [ -z "$immutable_carrier_receipt" ] || { + echo "--immutable-carrier-receipt may only be specified once" >&2 + exit 2 + } + immutable_carrier_receipt="$1" + ;; + --cgroup-memory-max) + shift + [ "$#" -gt 0 ] || { echo "--cgroup-memory-max requires a size" >&2; exit 2; } + [ "$cgroup_memory_max_explicit" -eq 0 ] || { + echo "--cgroup-memory-max may only be specified once" >&2 + exit 2 + } + cgroup_memory_max="$1" + cgroup_memory_max_explicit=1 + ;; + --cgroup-memory-high) + shift + [ "$#" -gt 0 ] || { echo "--cgroup-memory-high requires a size" >&2; exit 2; } + [ "$cgroup_memory_high_explicit" -eq 0 ] || { + echo "--cgroup-memory-high may only be specified once" >&2 + exit 2 + } + cgroup_memory_high="$1" + cgroup_memory_high_explicit=1 + ;; + --cgroup-swap-max) + shift + [ "$#" -gt 0 ] || { echo "--cgroup-swap-max requires a size" >&2; exit 2; } + [ "$cgroup_swap_max_explicit" -eq 0 ] || { + echo "--cgroup-swap-max may only be specified once" >&2 + exit 2 + } + cgroup_swap_max="$1" + cgroup_swap_max_explicit=1 + ;; + --port) + shift + [ "$#" -gt 0 ] || { echo "--port requires a value" >&2; exit 2; } + port="$1" + ;; + --timeout) + shift + [ "$#" -gt 0 ] || { echo "--timeout requires a value" >&2; exit 2; } + timeout_seconds="$1" + ;; + --transactions) + shift + [ "$#" -gt 0 ] || { echo "--transactions requires a value" >&2; exit 2; } + transaction_count="$1" + ;; + --rows-per-transaction) + shift + [ "$#" -gt 0 ] || { echo "--rows-per-transaction requires a value" >&2; exit 2; } + rows_per_transaction="$1" + ;; + --label) + shift + [ "$#" -gt 0 ] || { echo "--label requires a value" >&2; exit 2; } + run_label="$1" + ;; + --keep-pgdata) + keep_pgdata=1 + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +is_positive_integer() { + [[ "$1" =~ ^[1-9][0-9]*$ ]] +} + +validate_cgroup_size() { + [[ "$1" =~ ^[0-9]+([KMGTPE]([i]?B)?)?$ ]] +} + +cgroup_size_to_bytes() { + python3 - "$1" <<'PY' +import re +import sys + +match = re.fullmatch(r"([0-9]+)([KMGTPE])?(?:i?B)?", sys.argv[1]) +if match is None: + raise SystemExit(2) +value = int(match.group(1)) +suffix = match.group(2) +if suffix is not None: + value *= 1024 ** ("KMGTPE".index(suffix) + 1) +if value > 2**63 - 1: + raise SystemExit(2) +print(value) +PY +} + +[ -n "$sealed_carrier" ] || { echo "--sealed-carrier is required" >&2; exit 2; } +[ -d "$sealed_carrier" ] || { printf 'missing sealed carrier: %s\n' "$sealed_carrier" >&2; exit 2; } +sealed_carrier="$(cd "$sealed_carrier" && pwd -P)" +case "$mode" in + qualification|diagnostic) ;; + *) echo "--mode requires qualification or diagnostic" >&2; exit 2 ;; +esac +if [ "$mode" = qualification ]; then + classification=product-qualification +else + classification=diagnostic-non-promotional +fi +if [ "$require_zero_write_aot" -eq 1 ]; then + boundary_verification_scope=full-cryptographic-plus-immutable-receipt +else + boundary_verification_scope=full-cryptographic-carrier-only +fi +if ! is_positive_integer "$port" || [ "$port" -gt 65535 ]; then + echo "--port requires a port number from 1 through 65535" >&2 + exit 2 +fi +is_positive_integer "$timeout_seconds" || { echo "--timeout requires a positive integer" >&2; exit 2; } +is_positive_integer "$transaction_count" || { echo "--transactions requires a positive integer" >&2; exit 2; } +is_positive_integer "$rows_per_transaction" || { echo "--rows-per-transaction requires a positive integer" >&2; exit 2; } +for cgroup_size in "$cgroup_memory_max" "$cgroup_memory_high" "$cgroup_swap_max"; do + if [ -n "$cgroup_size" ] && ! validate_cgroup_size "$cgroup_size"; then + printf 'invalid cgroup size: %s\n' "$cgroup_size" >&2 + exit 2 + fi +done +cgroup_enabled=0 +if [ -n "$cgroup_memory_max$cgroup_memory_high$cgroup_swap_max" ]; then + if [ -z "$cgroup_memory_max" ] || [ -z "$cgroup_memory_high" ] || + [ -z "$cgroup_swap_max" ]; then + echo "cgroup MemoryMax, MemoryHigh, and MemorySwapMax are an all-or-none set" >&2 + exit 2 + fi + cgroup_enabled=1 +fi +if [ "$mode" = qualification ]; then + [ "$require_zero_write_aot" -eq 1 ] || { + echo "qualification mode requires --require-zero-write-aot" >&2 + exit 2 + } + [ "$cgroup_enabled" -eq 1 ] || { + echo "qualification mode requires finite --cgroup-memory-max, --cgroup-memory-high, and --cgroup-swap-max" >&2 + exit 2 + } +fi +if [ "$require_zero_write_aot" -eq 1 ]; then + [ -n "$immutable_carrier_receipt" ] || { + echo "--require-zero-write-aot requires --immutable-carrier-receipt" >&2 + exit 2 + } +elif [ -n "$immutable_carrier_receipt" ]; then + echo "--immutable-carrier-receipt requires --require-zero-write-aot" >&2 + exit 2 +fi +fresh_require_command python3 +if [ "$cgroup_enabled" -eq 1 ]; then + cgroup_memory_max_bytes="$(cgroup_size_to_bytes "$cgroup_memory_max")" || { + echo "--cgroup-memory-max exceeds the supported finite range" >&2 + exit 2 + } + cgroup_memory_high_bytes="$(cgroup_size_to_bytes "$cgroup_memory_high")" || { + echo "--cgroup-memory-high exceeds the supported finite range" >&2 + exit 2 + } + cgroup_swap_max_bytes="$(cgroup_size_to_bytes "$cgroup_swap_max")" || { + echo "--cgroup-swap-max exceeds the supported finite range" >&2 + exit 2 + } + if [ "$cgroup_memory_max_bytes" -le 0 ] || + [ "$cgroup_memory_high_bytes" -le 0 ]; then + echo "MemoryMax and MemoryHigh must be finite positive sizes" >&2 + exit 2 + fi + [ "$cgroup_memory_high_bytes" -le "$cgroup_memory_max_bytes" ] || { + echo "MemoryHigh may not exceed MemoryMax" >&2 + exit 2 + } +else + cgroup_memory_max_bytes=none + cgroup_memory_high_bytes=none + cgroup_swap_max_bytes=none +fi +case "$run_label" in + ""|*[!A-Za-z0-9._-]*) + echo "--label may only contain letters, numbers, '.', '_', and '-'" >&2 + exit 2 + ;; +esac +[ "$(uname -s)" = Linux ] || { + echo "immediate-recovery qualification currently requires Linux process birth identities" >&2 + exit 2 +} + +fresh_require_command perl +fresh_require_command stat +if [ "$cgroup_enabled" -eq 1 ]; then + fresh_require_command systemd-run + [ -r /sys/fs/cgroup/cgroup.controllers ] || { + echo "server cgroup controls require a cgroup-v2 host" >&2 + exit 2 + } +fi +[ -x "$NATIVE_INSTALL_DIR/bin/psql" ] || { + printf 'missing native PostgreSQL 18 psql client: %s\n' "$NATIVE_INSTALL_DIR/bin/psql" >&2 + printf 'Build it with %s/bin/build-native-oracle.sh first.\n' "$FRESH_ROOT" >&2 + exit 2 +} + +fresh_capture_qualification_carrier_identity "$sealed_carrier" +frozen_carrier_identity="$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" +frozen_manifest_sha256="$FRESH_QUALIFICATION_CARRIER_MANIFEST_SHA256" +frozen_wasmer_receipt_sha256="$FRESH_QUALIFICATION_CARRIER_RECEIPT_SHA256" +frozen_payload_sha256="$FRESH_QUALIFICATION_CARRIER_PAYLOAD_SHA256" +frozen_headless_sha256="$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" +frozen_core_profile="$FRESH_QUALIFICATION_CORE_PROFILE" +frozen_guest_build_recipe_sha256="$FRESH_QUALIFICATION_GUEST_BUILD_RECIPE_SHA256" +immutable_receipt_sha256=none +immutable_receipt_dev=none +immutable_receipt_ino=none +if [ "$require_zero_write_aot" -eq 1 ]; then + [ "$(id -u)" -ne 0 ] || { + echo "zero-write recovery qualification must run unprivileged" >&2 + exit 2 + } + cap_eff="$(awk '$1 == "CapEff:" { print $2 }' /proc/self/status)" + [[ "$cap_eff" =~ ^[0-9a-fA-F]+$ ]] || { + echo "could not read exact CapEff for zero-write recovery qualification" >&2 + exit 2 + } + if (( (16#$cap_eff & (1 << 9)) != 0 )); then + echo "zero-write recovery qualification refuses effective CAP_LINUX_IMMUTABLE" >&2 + exit 2 + fi + receipt_parent="$(dirname "$immutable_carrier_receipt")" + if [ ! -d "$receipt_parent" ] || [ -L "$receipt_parent" ]; then + printf 'immutable carrier receipt parent must be a non-symlink directory: %s\n' \ + "$receipt_parent" >&2 + exit 2 + fi + immutable_carrier_receipt="$(cd "$receipt_parent" && pwd -P)/$(basename "$immutable_carrier_receipt")" + if [ ! -f "$immutable_carrier_receipt" ] || + [ -L "$immutable_carrier_receipt" ]; then + printf 'immutable carrier receipt must be a regular non-symlink file: %s\n' \ + "$immutable_carrier_receipt" >&2 + exit 2 + fi + case "$immutable_carrier_receipt/" in + "$sealed_carrier/"|"$sealed_carrier/"*) + echo "immutable deployment receipt must remain outside the sealed carrier" >&2 + exit 2 + ;; + esac + "$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier" \ + --receipt "$immutable_carrier_receipt" + fresh_capture_stable_regular_file_identity "$immutable_carrier_receipt" || { + echo "immutable carrier receipt changed while its identity was captured" >&2 + exit 1 + } + immutable_receipt_sha256="$FRESH_QUALIFICATION_REGULAR_FILE_SHA256" + immutable_receipt_dev="$FRESH_QUALIFICATION_REGULAR_FILE_DEVICE" + immutable_receipt_ino="$FRESH_QUALIFICATION_REGULAR_FILE_INODE" +fi +fresh_resolve_postgres_profiles embedded-concurrent safe +[ "${#FRESH_POSTGRES_PROFILE_OVERLAPPING_EXPLICIT[@]}" -eq 0 ] || exit 2 + +run_root="$FRESH_WORK_ROOT/run/immediate-recovery-$run_label" +report_dir="$FRESH_WORK_ROOT/reports/immediate-recovery-$run_label" +pgdata="$run_root/pgdata" +dev_shm="$run_root/dev-shm" +fresh_require_managed_generated_path "$run_root" "immediate-recovery run directory" +fresh_require_managed_generated_path "$report_dir" "immediate-recovery report directory" +if [ -e "$run_root" ] || [ -L "$run_root" ]; then + printf 'refusing to replace recovery run directory: %s\n' "$run_root" >&2 + exit 2 +fi +if [ -e "$report_dir" ] || [ -L "$report_dir" ]; then + printf 'refusing to replace recovery report directory: %s\n' "$report_dir" >&2 + exit 2 +fi +mkdir -p "$pgdata" "$dev_shm" "$report_dir" + +profile_inputs="$report_dir/postgres-profile-inputs.tsv" +profile_resolution="$report_dir/postgres-profile-resolution.tsv" +fresh_write_postgres_profile_evidence "$profile_inputs" "$profile_resolution" +loader_validator="$FRESH_ROOT/bin/validate-sealed-loader-audit.py" +loader_validator_sha256="$(fresh_wasmer_bin_hash "$loader_validator")" +cache_observe_validator="$FRESH_ROOT/bin/validate-file-cache-telemetry.py" +cache_observe_validator_sha256="$(fresh_wasmer_bin_hash "$cache_observe_validator")" +cache_adaptive_validator="$FRESH_ROOT/bin/validate-adaptive-file-cache-telemetry.py" +cache_adaptive_validator_sha256="$(fresh_wasmer_bin_hash "$cache_adaptive_validator")" +sealed_loader_audit="$report_dir/sealed-loader-audit.jsonl" +sealed_loader_validation="$report_dir/sealed-loader-audit-validation.tsv" +immutable_verification="$report_dir/immutable-carrier-verification.tsv" +carrier_boundary_verification="$report_dir/carrier-boundary-verification.tsv" +qualification_policy="$report_dir/qualification-policy.tsv" +cache_telemetry_policy="$report_dir/cache-telemetry-policy.tsv" +evidence_envelope="$report_dir/qualification-evidence-envelope.tsv" +if [ -e "$sealed_loader_audit" ] || [ -L "$sealed_loader_audit" ]; then + echo "sealed loader audit path exists before recovery qualification" >&2 + exit 2 +fi +printf 'stage\texpected_receipt_sha256\tobserved_receipt_sha256\texpected_receipt_dev\tobserved_receipt_dev\texpected_receipt_ino\tobserved_receipt_ino\tstatus\n' \ + >"$immutable_verification" +printf 'stage\tverification_scope\texpected_closure_identity\tobserved_closure_identity\tstatus\n' \ + >"$carrier_boundary_verification" +printf 'campaign-start\t%s\t%s\t%s\tpassed\n' \ + "$boundary_verification_scope" "$frozen_carrier_identity" \ + "$frozen_carrier_identity" \ + >>"$carrier_boundary_verification" +printf 'schema_version\tmode\tclassification\trequire_zero_write_aot\trequired_snapshot_mode\texpected_outer_initdb_invocations\texpected_outer_postgres_invocations\tcarrier_closure_identity\tmanifest_sha256\twasmer_receipt_sha256\tpayload_sha256\theadless_sha256\tcore_profile\tguest_build_recipe_sha256\timmutable_receipt_path\timmutable_receipt_sha256\timmutable_receipt_dev\timmutable_receipt_ino\tloader_validator_sha256\tcgroup_enabled\tcgroup_memory_max\tcgroup_memory_max_bytes\tcgroup_memory_high\tcgroup_memory_high_bytes\tcgroup_swap_max\tcgroup_swap_max_bytes\tpostgres_profile_identity\n' \ + >"$qualification_policy" +printf 'oliphaunt.wasix-postmaster.immediate-recovery-policy.v3\t%s\t%s\t%s\t%s\t1\t3\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$mode" "$classification" \ + "$require_zero_write_aot" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf direct-immutable-inode || printf compatibility)" \ + "$frozen_carrier_identity" "$frozen_manifest_sha256" \ + "$frozen_wasmer_receipt_sha256" "$frozen_payload_sha256" \ + "$frozen_headless_sha256" "$frozen_core_profile" \ + "$frozen_guest_build_recipe_sha256" \ + "${immutable_carrier_receipt:-none}" "$immutable_receipt_sha256" \ + "$immutable_receipt_dev" "$immutable_receipt_ino" \ + "$loader_validator_sha256" "$cgroup_enabled" \ + "${cgroup_memory_max:-none}" "$cgroup_memory_max_bytes" \ + "${cgroup_memory_high:-none}" "$cgroup_memory_high_bytes" \ + "${cgroup_swap_max:-none}" "$cgroup_swap_max_bytes" \ + "$FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY" \ + >>"$qualification_policy" +printf 'schema_version\tactivation_source\toutput_environment\tadaptive_path_derivation\tobserve_validator_sha256\tadaptive_validator_sha256\texpected_observe_validations\texpected_adaptive_validations\n' \ + >"$cache_telemetry_policy" +printf 'oliphaunt.wasix-postmaster.cache-telemetry-policy.v1\tsealed-manifest-only\tOLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE\tPath::with_extension("adaptive.json")\t%s\t%s\t4\t3\n' \ + "$cache_observe_validator_sha256" "$cache_adaptive_validator_sha256" \ + >>"$cache_telemetry_policy" +cache_telemetry_policy_sha256="$(fresh_wasmer_bin_hash "$cache_telemetry_policy")" +qualification_policy_sha256="$(fresh_wasmer_bin_hash "$qualification_policy")" +chmod 0444 "$qualification_policy" "$cache_telemetry_policy" + +wasmer_bin="$sealed_carrier/bin/wasmer-headless" +sealed_manifest="$sealed_carrier/manifest.json" +wasix_initdb="$sealed_carrier/bin/initdb" +wasix_postgres="$sealed_carrier/bin/postgres" +wasix_lib="$sealed_carrier/lib" +wasmer_stack_size="${WASMER_STACK_SIZE:-33554432}" +case "$wasmer_stack_size" in + ""|0|*[!0-9]*) echo "WASMER_STACK_SIZE requires a positive integer" >&2; exit 2 ;; +esac + +sealed_loader_unset_args=( + -u OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT + -u OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE + -u OLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE +) +sealed_loader_env=() +if [ "$require_zero_write_aot" -eq 1 ]; then + sealed_loader_env=( + OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1 + "OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE=$sealed_loader_audit" + ) +fi + +wasmer_args=( + run + --quiet + --disable-cache + --sealed-module-manifest "$sealed_manifest" + --stack-size "$wasmer_stack_size" + --enable-exceptions + --enable-threads + --net + --volume "$REPO_ROOT:$REPO_ROOT" + --volume "$wasix_lib:/lib" + --volume "$dev_shm:/dev/shm" +) +case "$FRESH_WORK_ROOT/" in + "$REPO_ROOT/"*) ;; + *) wasmer_args+=(--volume "$FRESH_WORK_ROOT:$FRESH_WORK_ROOT") ;; +esac +case "$sealed_carrier/" in + "$REPO_ROOT/"*|"$FRESH_WORK_ROOT/"*) ;; + *) wasmer_args+=(--volume "$sealed_carrier:$sealed_carrier") ;; +esac + +effective_gucs=("${FRESH_POSTGRES_PROFILE_GUCS[@]}") +effective_gucs+=( + checkpoint_timeout=1h + max_wal_size=8GB + min_wal_size=1GB +) + +conn="postgresql://wasix@127.0.0.1:$port/postgres" +carrier_snapshots="$report_dir/carrier-snapshots.tsv" +printf 'phase\tmanifest_sha256\treceipt_sha256\tpayload_sha256\theadless_sha256\n' \ + >"$carrier_snapshots" +current_stage="setup" +qualification_status="failed" +cleanup_escalation=0 +active_pid="" +active_pgid="" +active_identity="" +active_phase="" +active_cgroup_unit="" +active_cgroup_dir="" +active_cgroup_identity="" +server_command_prefix=() + +assert_frozen_immutable_carrier() { + local stage="$1" + local observed_sha=none observed_dev=none observed_ino=none status=passed + + [ "$require_zero_write_aot" -eq 1 ] || return 0 + if ! "$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier" \ + --receipt "$immutable_carrier_receipt" --fast; then + status=verification-failed + fi + if fresh_capture_stable_regular_file_identity \ + "$immutable_carrier_receipt" 2>/dev/null; then + observed_sha="$FRESH_QUALIFICATION_REGULAR_FILE_SHA256" + observed_dev="$FRESH_QUALIFICATION_REGULAR_FILE_DEVICE" + observed_ino="$FRESH_QUALIFICATION_REGULAR_FILE_INODE" + else + status=unreadable + fi + if [ "$status" = passed ] && { + [ "$observed_sha" != "$immutable_receipt_sha256" ] || + [ "$observed_dev" != "$immutable_receipt_dev" ] || + [ "$observed_ino" != "$immutable_receipt_ino" ]; + }; then + status=identity-changed + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$stage" "$immutable_receipt_sha256" "$observed_sha" \ + "$immutable_receipt_dev" "$observed_dev" \ + "$immutable_receipt_ino" "$observed_ino" "$status" \ + >>"$immutable_verification" + [ "$status" = passed ] || { + printf 'immutable carrier deployment identity failed at %s: %s\n' \ + "$stage" "$status" >&2 + return 125 + } +} + +assert_frozen_policy() { + [ "$(fresh_wasmer_bin_hash "$qualification_policy")" = \ + "$qualification_policy_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$cache_telemetry_policy")" = \ + "$cache_telemetry_policy_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$loader_validator")" = \ + "$loader_validator_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$cache_observe_validator")" = \ + "$cache_observe_validator_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$cache_adaptive_validator")" = \ + "$cache_adaptive_validator_sha256" ] && + fresh_assert_postgres_profile_inputs +} + +snapshot_carrier() { + local phase="$1" + local manifest_sha receipt_sha payload_sha headless_sha + + assert_frozen_policy || return + if [ "$require_zero_write_aot" -eq 1 ]; then + assert_frozen_immutable_carrier "$phase" || return + manifest_sha="$frozen_manifest_sha256" + receipt_sha="$frozen_wasmer_receipt_sha256" + payload_sha="$frozen_payload_sha256" + headless_sha="$frozen_headless_sha256" + else + fresh_capture_qualification_carrier_identity "$sealed_carrier" || return + [ "$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" = \ + "$frozen_carrier_identity" ] || { + printf 'sealed carrier identity changed during recovery qualification\n' >&2 + return 1 + } + manifest_sha="$FRESH_QUALIFICATION_CARRIER_MANIFEST_SHA256" + receipt_sha="$FRESH_QUALIFICATION_CARRIER_RECEIPT_SHA256" + payload_sha="$FRESH_QUALIFICATION_CARRIER_PAYLOAD_SHA256" + headless_sha="$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" + fi + printf '%s\t%s\t%s\t%s\t%s\n' \ + "$phase" "$manifest_sha" "$receipt_sha" "$payload_sha" "$headless_sha" \ + >>"$carrier_snapshots" +} + +cleanup_active_server() { + local status=0 + + [ -n "$active_pid" ] || return 0 + cleanup_escalation=1 + if [ -z "$active_pgid" ] || [ -z "$active_identity" ]; then + printf 'cannot safely clean incomplete active server identity for phase %s\n' \ + "$active_phase" >&2 + return 125 + fi + fresh_terminate_owned_process_group \ + "$active_pgid" "$active_pid" "$active_identity" 1000 3000 || status=$? + if [ "$status" -eq 0 ] && [ -n "$active_cgroup_dir" ] && + [ -n "$active_cgroup_identity" ]; then + fresh_wait_cgroup_empty "$active_cgroup_dir" "$active_cgroup_identity" \ + 3000 || status=$? + fi + active_pid="" + active_pgid="" + active_identity="" + active_phase="" + active_cgroup_unit="" + active_cgroup_dir="" + active_cgroup_identity="" + return "$status" +} + +on_exit() { + local status="$1" + local cleanup_status=0 + + trap - EXIT INT TERM HUP + if [ -n "$active_pid" ]; then + cleanup_active_server || cleanup_status=$? + fi + if [ "$status" -eq 0 ] && [ "$cleanup_status" -ne 0 ]; then + status="$cleanup_status" + fi + { + printf 'status\t%s\n' "$qualification_status" + printf 'exit_code\t%s\n' "$status" + printf 'last_stage\t%s\n' "$current_stage" + printf 'cleanup_escalation\t%s\n' "$cleanup_escalation" + printf 'mode\t%s\n' "$mode" + printf 'require_zero_write_aot\t%s\n' "$require_zero_write_aot" + printf 'cgroup_enabled\t%s\n' "$cgroup_enabled" + printf 'run_root\t%s\n' "$run_root" + printf 'report_dir\t%s\n' "$report_dir" + } >"$report_dir/result.tsv" + exit "$status" +} +trap 'on_exit $?' EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +run_logged() { + local log="$1" + shift + fresh_run_process_group_timeout "$timeout_seconds" -- "$@" >"$log" 2>&1 +} + +cache_telemetry_path_for_log() { + local log="$1" + case "$log" in + /*.log) printf '%s.cache-offers.json\n' "${log%.log}" ;; + *) printf 'cache telemetry requires an absolute .log path: %s\n' "$log" >&2; return 125 ;; + esac +} + +validate_observe_cache_telemetry() { + local log="$1" + local expected_workload="$2" + local telemetry validation + telemetry="$(cache_telemetry_path_for_log "$log")" || return + validation="${telemetry%.json}-validation.tsv" + python3 "$cache_observe_validator" \ + --telemetry "$telemetry" \ + --manifest "$sealed_manifest" \ + --output "$validation" \ + --expected-workload "$expected_workload" + chmod 0444 "$telemetry" +} + +validate_postgres_cache_telemetry() { + local log="$1" + local telemetry adaptive validation + validate_observe_cache_telemetry "$log" runtime:postgres + telemetry="$(cache_telemetry_path_for_log "$log")" || return + # Exact sibling convention used by Rust Path::with_extension("adaptive.json"). + adaptive="${telemetry%.json}.adaptive.json" + validation="${telemetry%.json}-adaptive-validation.tsv" + python3 "$cache_adaptive_validator" \ + --telemetry "$adaptive" \ + --manifest "$sealed_manifest" \ + --output "$validation" + chmod 0444 "$adaptive" +} + +configure_server_cgroup() { + local phase="$1" + + server_command_prefix=() + active_cgroup_unit="" + [ "$cgroup_enabled" -eq 1 ] || return 0 + active_cgroup_unit="oliphaunt-recovery-$$-$phase" + server_command_prefix=( + systemd-run + --user + --scope + --quiet + --collect + "--unit=$active_cgroup_unit" + --property=MemoryAccounting=yes + "--property=MemoryMax=$cgroup_memory_max" + "--property=MemoryHigh=$cgroup_memory_high" + "--property=MemorySwapMax=$cgroup_swap_max" + ) +} + +assert_active_server_cgroup() { + local observation="$1" + local deadline relative="" cgroup_dir observed_identity + local observed_max observed_high observed_swap output + + [ "$cgroup_enabled" -eq 1 ] || return 0 + [ -n "$active_pid" ] && [ -n "$active_cgroup_unit" ] || return 125 + deadline=$(( $(fresh_supervision_now_ms) + 5000 )) + while [ "$(fresh_supervision_now_ms)" -lt "$deadline" ]; do + relative="$(awk -F: '$1 == "0" { print $3; exit }' \ + "/proc/$active_pid/cgroup" 2>/dev/null || true)" + if [ "$(basename "$relative" 2>/dev/null || true)" = \ + "$active_cgroup_unit.scope" ]; then + break + fi + sleep 0.05 + done + case "$relative" in + /*) ;; + *) echo "server did not expose an absolute cgroup-v2 path" >&2; return 125 ;; + esac + [ "$(basename "$relative")" = "$active_cgroup_unit.scope" ] || { + printf 'server did not enter its requested cgroup scope: pid=%s expected=%s observed=%s\n' \ + "$active_pid" "$active_cgroup_unit.scope" "$relative" >&2 + return 125 + } + cgroup_dir="/sys/fs/cgroup$relative" + for control in memory.max memory.high memory.swap.max memory.current memory.peak; do + [ -r "$cgroup_dir/$control" ] || { + printf 'server cgroup control is unreadable: %s/%s\n' \ + "$cgroup_dir" "$control" >&2 + return 125 + } + done + observed_identity="$(fresh_path_identity "$cgroup_dir")" || return + if [ -n "$active_cgroup_identity" ]; then + [ "$observed_identity" = "$active_cgroup_identity" ] || { + echo "server cgroup identity changed during recovery phase" >&2 + return 125 + } + else + active_cgroup_dir="$cgroup_dir" + active_cgroup_identity="$observed_identity" + fi + observed_max="$(<"$cgroup_dir/memory.max")" + observed_high="$(<"$cgroup_dir/memory.high")" + observed_swap="$(<"$cgroup_dir/memory.swap.max")" + if [ "$observed_max" != "$cgroup_memory_max_bytes" ] || + [ "$observed_high" != "$cgroup_memory_high_bytes" ] || + [ "$observed_swap" != "$cgroup_swap_max_bytes" ]; then + printf 'server cgroup controls differ: max=%s/%s high=%s/%s swap=%s/%s\n' \ + "$observed_max" "$cgroup_memory_max_bytes" \ + "$observed_high" "$cgroup_memory_high_bytes" \ + "$observed_swap" "$cgroup_swap_max_bytes" >&2 + return 125 + fi + output="$report_dir/$active_phase.cgroup.tsv" + if [ ! -e "$output" ]; then + printf 'observation\tunit\tcgroup_path\tcgroup_identity\tmemory_max_bytes\tmemory_high_bytes\tmemory_swap_max_bytes\tmemory_current_bytes\tmemory_peak_bytes\n' \ + >"$output" + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$observation" "$active_cgroup_unit" "$relative" \ + "$active_cgroup_identity" "$observed_max" "$observed_high" \ + "$observed_swap" "$(<"$cgroup_dir/memory.current")" \ + "$(<"$cgroup_dir/memory.peak")" >>"$output" +} + +launch_with_embedded_nofile() { + local limits_file="$1" + shift + local pre_soft pre_hard actual_soft actual_hard + + pre_soft="$(ulimit -S -n)" + pre_hard="$(ulimit -H -n)" + case "$pre_hard" in + unlimited) ;; + ""|*[!0-9]*) return 125 ;; + *) [ "$pre_hard" -ge 1024 ] || return 125 ;; + esac + ulimit -S -n 1024 || return 125 + actual_soft="$(ulimit -S -n)" + actual_hard="$(ulimit -H -n)" + { + printf 'pre_soft_nofile=%s\n' "$pre_soft" + printf 'pre_hard_nofile=%s\n' "$pre_hard" + printf 'actual_soft_nofile=%s\n' "$actual_soft" + printf 'actual_hard_nofile=%s\n' "$actual_hard" + } >"$limits_file" + [ "$actual_soft" = 1024 ] && [ "$actual_hard" = "$pre_hard" ] || return 125 + exec "$@" +} + +start_server() { + local phase="$1" + local server_log="$2" + local limits_file="$3" + local postgres_args=( + -D "$pgdata" + -h 127.0.0.1 + -p "$port" + -c unix_socket_directories= + ) + local guc + local cache_telemetry + + [ -z "$active_pid" ] || { + echo "refusing to replace an active recovery server" >&2 + return 125 + } + fresh_tcp_port_open 127.0.0.1 "$port" && { + printf 'refusing occupied recovery port: 127.0.0.1:%s\n' "$port" >&2 + return 125 + } + [ -z "$(find "$dev_shm" -mindepth 1 -print -quit)" ] || { + printf 'shared-object directory is not empty before %s start: %s\n' \ + "$phase" "$dev_shm" >&2 + return 1 + } + fresh_assert_postgres_profile_inputs || return + for guc in "${effective_gucs[@]}"; do + postgres_args+=(-c "$guc") + done + assert_frozen_policy || return + assert_frozen_immutable_carrier "before-$phase-start" || return + configure_server_cgroup "$phase" + cache_telemetry="$(cache_telemetry_path_for_log "$server_log")" || return + fresh_spawn_process_group -- launch_with_embedded_nofile "$limits_file" \ + "${server_command_prefix[@]}" \ + env -u WASMER_DIR -u WASMER_CACHE_DIR \ + "${sealed_loader_unset_args[@]}" \ + "${sealed_loader_env[@]}" \ + "OLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE=$cache_telemetry" \ + "$wasmer_bin" "${wasmer_args[@]}" "$wasix_postgres" -- \ + "${postgres_args[@]}" >"$server_log" 2>&1 || return + active_pid="$FRESH_PROCESS_GROUP_PID" + active_pgid="$FRESH_PROCESS_GROUP_PGID" + active_identity="$FRESH_PROCESS_GROUP_IDENTITY" + active_phase="$phase" + if [ -z "$active_identity" ] || [ "$active_pid" != "$active_pgid" ]; then + printf 'incomplete server identity: pid=%s pgid=%s identity=%s\n' \ + "$active_pid" "$active_pgid" "$active_identity" >&2 + return 125 + fi + assert_active_server_cgroup launch +} + +wait_for_ready() { + local wait_log="$1" + local attempt_log="$wait_log.attempt" + local deadline status + + : >"$wait_log" + deadline=$(( $(fresh_supervision_now_ms) + timeout_seconds * 1000 )) + while [ "$(fresh_supervision_now_ms)" -lt "$deadline" ]; do + set +e + fresh_run_process_group_timeout_ms 1000 -- \ + env PGCONNECT_TIMEOUT=1 "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" -XAtq -v ON_ERROR_STOP=1 -c 'select 1' >"$attempt_log" 2>&1 + status=$? + set -e + if [ -s "$attempt_log" ]; then cat "$attempt_log" >>"$wait_log"; fi + if [ "$status" -eq 0 ]; then + rm -f -- "$attempt_log" + return 0 + fi + if [ "$status" -eq 125 ]; then + rm -f -- "$attempt_log" + return 125 + fi + if ! fresh_supervision_pid_running "$active_pid"; then + echo "server exited before readiness" >>"$wait_log" + rm -f -- "$attempt_log" + return 1 + fi + if ! fresh_pid_matches_birth_identity "$active_pid" "$active_identity"; then + echo "server birth identity changed before readiness" >>"$wait_log" + rm -f -- "$attempt_log" + return 125 + fi + sleep 0.1 + done + echo "readiness timed out" >>"$wait_log" + rm -f -- "$attempt_log" + return 124 +} + +psql_to_file() { + local output="$1" + local sql="$2" + fresh_run_process_group_timeout "$timeout_seconds" -- \ + env PGCONNECT_TIMEOUT=5 "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" -XAtq -F $'\t' -v ON_ERROR_STOP=1 -c "$sql" \ + >"$output" 2>"$output.stderr" +} + +capture_and_validate_settings() { + local phase="$1" + local settings="$report_dir/$phase.pg-settings.tsv" + local validation="$report_dir/$phase.profile-validation.tsv" + + { + printf 'name\tsetting\tunit\tsource\n' + fresh_run_process_group_timeout "$timeout_seconds" -- \ + env PGCONNECT_TIMEOUT=5 "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" -XAtq -F $'\t' -v ON_ERROR_STOP=1 -c " + SELECT name, setting, coalesce(unit, ''), source + FROM pg_settings + WHERE name IN ( + 'io_method', 'shared_buffers', 'max_connections', + 'fsync', 'full_page_writes', 'synchronous_commit' + ) + ORDER BY name + " + } >"$settings" 2>"$settings.stderr" + fresh_validate_postgres_profile_settings "$settings" "$validation" + psql_to_file "$report_dir/$phase.checkpoint-policy.tsv" " + SELECT + current_setting('checkpoint_timeout')::interval = interval '1 hour', + pg_size_bytes(current_setting('max_wal_size')) = 8589934592, + pg_size_bytes(current_setting('min_wal_size')) = 1073741824, + current_setting('fsync') = 'on', + current_setting('synchronous_commit') = 'on', + current_setting('full_page_writes') = 'on' + " + [ "$(tr -d '[:space:]' <"$report_dir/$phase.checkpoint-policy.tsv")" = tttttt ] || { + printf 'controlled checkpoint/durability policy mismatch during %s\n' "$phase" >&2 + return 1 + } +} + +capture_database_state() { + local output="$1" + { + printf 'row_count\tpayload_sum\tcontent_md5\tcheckpoint_lsn\tredo_lsn\tinsert_lsn\tflush_lsn\tcheckpoints_timed\tcheckpoints_requested\tcheckpoints_done\n' + fresh_run_process_group_timeout "$timeout_seconds" -- \ + env PGCONNECT_TIMEOUT=5 "$NATIVE_INSTALL_DIR/bin/psql" \ + "$conn" -XAtq -F $'\t' -v ON_ERROR_STOP=1 -c " + SELECT + count(*), + coalesce(sum(payload), 0), + md5(string_agg(id::text || ':' || payload::text, ',' ORDER BY id)), + (pg_control_checkpoint()).checkpoint_lsn, + (pg_control_checkpoint()).redo_lsn, + pg_current_wal_insert_lsn(), + pg_current_wal_flush_lsn(), + s.num_timed, + s.num_requested, + s.num_done + FROM recovery_probe, pg_stat_checkpointer s + GROUP BY s.num_timed, s.num_requested, s.num_done + " + } >"$output" 2>"$output.stderr" + [ "$(wc -l <"$output" | tr -d '[:space:]')" = 2 ] || { + printf 'unexpected database-state row count: %s\n' "$output" >&2 + return 1 + } +} + +state_field() { + local file="$1" + local field="$2" + awk -F '\t' -v field="$field" 'NR == 1 { for (i = 1; i <= NF; i++) if ($i == field) column = i; next } + NR == 2 && column { print $column }' "$file" +} + +assert_same_contents() { + local expected="$1" + local observed="$2" + local field expected_value observed_value + + for field in row_count payload_sum content_md5; do + expected_value="$(state_field "$expected" "$field")" + observed_value="$(state_field "$observed" "$field")" + if [ -z "$expected_value" ] || [ "$observed_value" != "$expected_value" ]; then + printf 'database content mismatch for %s: expected=%s observed=%s\n' \ + "$field" "$expected_value" "$observed_value" >&2 + return 1 + fi + done +} + +signal_active_server() { + local signal="$1" + local evidence="$2" + + [ -n "$active_pid" ] || return 125 + fresh_supervision_pid_running "$active_pid" || return 1 + fresh_pid_matches_birth_identity "$active_pid" "$active_identity" || return 125 + assert_active_server_cgroup "pre-$signal" || return + printf 'phase\tpid\tpgid\tbirth_identity\tsignal\tsent_at\n' >"$evidence" + printf '%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$active_phase" "$active_pid" "$active_pgid" "$active_identity" \ + "$signal" "$(fresh_timestamp)" >>"$evidence" + fresh_signal_owned_pid "$signal" "$active_pid" "$active_identity" +} + +wait_for_unassisted_exit() { + local exit_evidence="$1" + local deadline wait_status group_deadline cgroup_empty=not-requested + + deadline=$(( $(fresh_supervision_now_ms) + timeout_seconds * 1000 )) + while fresh_supervision_pid_running "$active_pid"; do + if ! fresh_pid_matches_birth_identity "$active_pid" "$active_identity"; then + # The leader can exit between the liveness check above and reading its + # immutable birth identity. Only classify an identity mismatch as PID + # reuse when the numeric PID is still live after that failed read. + fresh_supervision_pid_running "$active_pid" && return 125 + break + fi + [ "$(fresh_supervision_now_ms)" -lt "$deadline" ] || { + printf 'server did not exit after bridged signal without escalation\n' >&2 + return 124 + } + sleep 0.05 + done + fresh_reap_process_group_leader "$active_pid" + wait_status="$FRESH_PROCESS_GROUP_WAIT_STATUS" + group_deadline=$(( $(fresh_supervision_now_ms) + timeout_seconds * 1000 )) + while fresh_process_group_exists "$active_pgid"; do + [ "$(fresh_supervision_now_ms)" -lt "$group_deadline" ] || { + printf 'server process group remained after leader exit: %s\n' "$active_pgid" >&2 + return 124 + } + sleep 0.05 + done + if [ -n "$active_cgroup_dir" ] && [ -n "$active_cgroup_identity" ]; then + fresh_wait_cgroup_empty "$active_cgroup_dir" "$active_cgroup_identity" \ + "$((timeout_seconds * 1000))" + cgroup_empty=true + fi + fresh_wait_tcp_port_closed 127.0.0.1 "$port" "$((timeout_seconds * 1000))" + [ -z "$(find "$dev_shm" -mindepth 1 -print -quit)" ] || { + printf 'shared objects survived normal guest shutdown: %s\n' "$dev_shm" >&2 + return 1 + } + [ "$wait_status" -eq 0 ] || { + printf 'server leader exited nonzero after unassisted guest shutdown: phase=%s status=%s\n' \ + "$active_phase" "$wait_status" >&2 + return 1 + } + { + printf 'phase\twait_status\tprocess_group_empty\tcgroup_empty\tport_closed\tshared_objects_empty\tescalation_used\n' + printf '%s\t%s\ttrue\t%s\ttrue\ttrue\tfalse\n' \ + "$active_phase" "$wait_status" "$cgroup_empty" + } >"$exit_evidence" + active_pid="" + active_pgid="" + active_identity="" + active_phase="" + active_cgroup_unit="" + active_cgroup_dir="" + active_cgroup_identity="" +} + +snapshot_carrier before-initdb +current_stage="initdb" +assert_frozen_immutable_carrier before-initdb-execution +initdb_log="$report_dir/initdb.log" +initdb_cache_telemetry="$(cache_telemetry_path_for_log "$initdb_log")" +run_logged "$report_dir/initdb.log" \ + env -u WASMER_DIR -u WASMER_CACHE_DIR \ + "${sealed_loader_unset_args[@]}" \ + "${sealed_loader_env[@]}" \ + "OLIPHAUNT_WASIX_CACHE_OFFER_TELEMETRY_FILE=$initdb_cache_telemetry" \ + "$wasmer_bin" "${wasmer_args[@]}" "$wasix_initdb" -- \ + -D "$pgdata" -A trust --no-locale --encoding=UTF8 --no-instructions +validate_observe_cache_telemetry "$initdb_log" runtime:initdb +snapshot_carrier after-initdb + +current_stage="baseline-start" +start_server baseline "$report_dir/baseline.server.log" "$report_dir/baseline.limits" +wait_for_ready "$report_dir/baseline.wait.log" +capture_and_validate_settings baseline +psql_to_file "$report_dir/baseline-create.tsv" " + CREATE TABLE recovery_probe ( + id bigint PRIMARY KEY, + payload bigint NOT NULL + ); + INSERT INTO recovery_probe + SELECT g, ((g::bigint * 1103515245 + 12345) % 2147483647) + FROM generate_series(1, 4096) AS g; + CHECKPOINT; + SELECT count(*) FROM recovery_probe +" +capture_database_state "$report_dir/baseline-state.tsv" +baseline_checkpoint_lsn="$(state_field "$report_dir/baseline-state.tsv" checkpoint_lsn)" +baseline_flush_lsn="$(state_field "$report_dir/baseline-state.tsv" flush_lsn)" +[[ "$baseline_checkpoint_lsn" =~ ^[0-9A-F]+/[0-9A-F]+$ ]] || { + printf 'invalid baseline checkpoint LSN: %s\n' "$baseline_checkpoint_lsn" >&2 + exit 1 +} +[[ "$baseline_flush_lsn" =~ ^[0-9A-F]+/[0-9A-F]+$ ]] || { + printf 'invalid baseline flush LSN: %s\n' "$baseline_flush_lsn" >&2 + exit 1 +} + +current_stage="acknowledged-transactions" +printf 'transaction\tinsert_lsn\tflush_lsn\tflush_covers_insert\n' \ + >"$report_dir/acknowledged-transactions.tsv" +for transaction in $(seq 1 "$transaction_count"); do + first_id=$((4096 + (transaction - 1) * rows_per_transaction + 1)) + last_id=$((first_id + rows_per_transaction - 1)) + update_first=$((1 + ((transaction - 1) * rows_per_transaction) % 4096)) + update_last=$((update_first + rows_per_transaction - 1)) + if [ "$update_last" -gt 4096 ]; then update_last=4096; fi + transaction_result="$report_dir/transaction-$transaction.tsv" + psql_to_file "$transaction_result" " + BEGIN; + INSERT INTO recovery_probe + SELECT g, ((g::bigint * 1103515245 + $transaction * 12345) % 2147483647) + FROM generate_series($first_id, $last_id) AS g; + UPDATE recovery_probe + SET payload = payload + $transaction + WHERE id BETWEEN $update_first AND $update_last; + COMMIT; + SELECT + $transaction, + pg_current_wal_insert_lsn(), + pg_current_wal_flush_lsn(), + pg_current_wal_flush_lsn() >= pg_current_wal_insert_lsn() + " + transaction_line="$(tr -d '\r' <"$transaction_result")" + transaction_fields="$(awk -F '\t' '{ print NF }' <<<"$transaction_line")" + if [ "$transaction_fields" != 4 ] || [ "${transaction_line##*$'\t'}" != t ]; then + printf 'transaction %s was not proven flushed: %s\n' \ + "$transaction" "$transaction_line" >&2 + exit 1 + fi + printf '%s\n' "$transaction_line" >>"$report_dir/acknowledged-transactions.tsv" +done + +capture_database_state "$report_dir/pre-crash-state.tsv" +precrash_checkpoint_lsn="$(state_field "$report_dir/pre-crash-state.tsv" checkpoint_lsn)" +precrash_flush_lsn="$(state_field "$report_dir/pre-crash-state.tsv" flush_lsn)" +[ "$precrash_checkpoint_lsn" = "$baseline_checkpoint_lsn" ] || { + printf 'checkpoint advanced after controlled baseline: baseline=%s precrash=%s\n' \ + "$baseline_checkpoint_lsn" "$precrash_checkpoint_lsn" >&2 + exit 1 +} +[ "$precrash_flush_lsn" != "$baseline_flush_lsn" ] || { + echo "post-checkpoint transactions did not advance the flushed WAL LSN" >&2 + exit 1 +} +snapshot_carrier before-sigquit + +current_stage="immediate-shutdown" +signal_active_server QUIT "$report_dir/sigquit.tsv" +wait_for_unassisted_exit "$report_dir/immediate-exit.tsv" +validate_postgres_cache_telemetry "$report_dir/baseline.server.log" +grep -Fq 'received immediate shutdown request' "$report_dir/baseline.server.log" || { + echo "PostgreSQL did not log an immediate shutdown request after host SIGQUIT" >&2 + exit 1 +} +snapshot_carrier after-sigquit + +current_stage="recovery-start" +start_server recovery "$report_dir/recovery.server.log" "$report_dir/recovery.limits" +wait_for_ready "$report_dir/recovery.wait.log" +capture_and_validate_settings recovery +grep -Fq 'database system was not properly shut down; automatic recovery in progress' \ + "$report_dir/recovery.server.log" || { + echo "restart did not enter PostgreSQL automatic recovery" >&2 + exit 1 +} +grep -Fq 'redo starts at' "$report_dir/recovery.server.log" || { + echo "restart did not report WAL redo" >&2 + exit 1 +} +capture_database_state "$report_dir/recovered-state.tsv" +assert_same_contents "$report_dir/pre-crash-state.tsv" "$report_dir/recovered-state.tsv" +snapshot_carrier after-recovery + +current_stage="smart-shutdown" +signal_active_server TERM "$report_dir/sigterm.tsv" +wait_for_unassisted_exit "$report_dir/smart-exit.tsv" +validate_postgres_cache_telemetry "$report_dir/recovery.server.log" +grep -Fq 'received smart shutdown request' "$report_dir/recovery.server.log" || { + echo "PostgreSQL did not log a smart shutdown request after host SIGTERM" >&2 + exit 1 +} +snapshot_carrier after-smart-shutdown + +current_stage="clean-reopen" +start_server clean-reopen "$report_dir/clean-reopen.server.log" "$report_dir/clean-reopen.limits" +wait_for_ready "$report_dir/clean-reopen.wait.log" +capture_and_validate_settings clean-reopen +grep -Fq 'database system was shut down at' "$report_dir/clean-reopen.server.log" || { + echo "final reopen did not observe a cleanly shut down cluster" >&2 + exit 1 +} +if grep -Fq 'automatic recovery in progress' "$report_dir/clean-reopen.server.log"; then + echo "final reopen unexpectedly entered crash recovery" >&2 + exit 1 +fi +capture_database_state "$report_dir/clean-reopen-state.tsv" +assert_same_contents "$report_dir/pre-crash-state.tsv" "$report_dir/clean-reopen-state.tsv" + +current_stage="final-smart-shutdown" +signal_active_server TERM "$report_dir/final-sigterm.tsv" +wait_for_unassisted_exit "$report_dir/final-smart-exit.tsv" +validate_postgres_cache_telemetry "$report_dir/clean-reopen.server.log" +grep -Fq 'received smart shutdown request' "$report_dir/clean-reopen.server.log" || { + echo "final PostgreSQL process did not receive smart shutdown" >&2 + exit 1 +} +snapshot_carrier final + +current_stage="sealed-loader-validation" +if [ "$require_zero_write_aot" -eq 1 ]; then + python3 "$loader_validator" \ + --audit "$sealed_loader_audit" \ + --manifest "$sealed_manifest" \ + --output "$sealed_loader_validation" \ + --required-snapshot-mode direct-immutable-inode \ + --expected-initdb-executions 1 \ + --expected-postgres-executions 3 + chmod 0444 "$sealed_loader_audit" "$sealed_loader_validation" +fi + +current_stage="campaign-end-verification" +if [ "$require_zero_write_aot" -eq 1 ]; then + "$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier" \ + --receipt "$immutable_carrier_receipt" +fi +fresh_capture_qualification_carrier_identity "$sealed_carrier" +if [ "$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" != \ + "$frozen_carrier_identity" ] || + [ "$FRESH_QUALIFICATION_CORE_PROFILE" != "$frozen_core_profile" ] || + [ "$FRESH_QUALIFICATION_GUEST_BUILD_RECIPE_SHA256" != \ + "$frozen_guest_build_recipe_sha256" ]; then + echo "sealed carrier failed the campaign-end full verification" >&2 + exit 125 +fi +printf 'campaign-end\t%s\t%s\t%s\tpassed\n' \ + "$boundary_verification_scope" "$frozen_carrier_identity" \ + "$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" \ + >>"$carrier_boundary_verification" +assert_frozen_policy +assert_frozen_immutable_carrier campaign-end + +if [ "$keep_pgdata" -eq 0 ]; then + fresh_require_managed_generated_path "$pgdata" "successful recovery PGDATA" + fresh_require_managed_generated_path "$dev_shm" "successful recovery shared-object directory" + rm -rf -- "$pgdata" "$dev_shm" + printf 'status\tdiscarded-after-success\npgdata\t%s\ndev_shm\t%s\n' \ + "$pgdata" "$dev_shm" >"$report_dir/run-retention.tsv" +else + printf 'status\tretained\npgdata\t%s\ndev_shm\t%s\n' \ + "$pgdata" "$dev_shm" >"$report_dir/run-retention.tsv" +fi + +sealed_loader_audit_sha256=none +sealed_loader_validation_sha256=none +if [ "$require_zero_write_aot" -eq 1 ]; then + sealed_loader_audit_sha256="$(fresh_wasmer_bin_hash "$sealed_loader_audit")" + sealed_loader_validation_sha256="$(fresh_wasmer_bin_hash "$sealed_loader_validation")" +fi +carrier_snapshots_sha256="$(fresh_wasmer_bin_hash "$carrier_snapshots")" +immutable_verification_sha256="$(fresh_wasmer_bin_hash "$immutable_verification")" +carrier_boundary_verification_sha256="$( + fresh_wasmer_bin_hash "$carrier_boundary_verification" +)" +cgroup_evidence_identity=none +if [ "$cgroup_enabled" -eq 1 ]; then + cgroup_evidence_identity="$( + { + fresh_wasmer_bin_hash "$report_dir/baseline.cgroup.tsv" + fresh_wasmer_bin_hash "$report_dir/recovery.cgroup.tsv" + fresh_wasmer_bin_hash "$report_dir/clean-reopen.cgroup.tsv" + } | fresh_sha256_stream + )" +fi +observed_cache_observe_validations="$( + find "$report_dir" -type f -name '*.cache-offers-validation.tsv' -print | wc -l +)" +observed_cache_adaptive_validations="$( + find "$report_dir" -type f -name '*.cache-offers-adaptive-validation.tsv' -print | wc -l +)" +[ "$observed_cache_observe_validations" -eq 4 ] && + [ "$observed_cache_adaptive_validations" -eq 3 ] || { + printf 'recovery cache telemetry validation population differs: observe=%s/4 adaptive=%s/3\n' \ + "$observed_cache_observe_validations" \ + "$observed_cache_adaptive_validations" >&2 + exit 1 +} +cache_adaptive_active_count=0 +cache_adaptive_fallback_count=0 +while IFS= read -r adaptive_validation_file; do + adaptive_outcome="$(awk -F '\t' 'NR == 2 { print $3 }' \ + "$adaptive_validation_file")" + case "$adaptive_outcome" in + adaptive-active) + cache_adaptive_active_count=$((cache_adaptive_active_count + 1)) + ;; + observe-only-fallback) + cache_adaptive_fallback_count=$((cache_adaptive_fallback_count + 1)) + ;; + *) + printf 'unknown adaptive validation outcome %s in %s\n' \ + "$adaptive_outcome" "$adaptive_validation_file" >&2 + exit 1 + ;; + esac +done < <( + find "$report_dir" -type f \ + -name '*.cache-offers-adaptive-validation.tsv' -print | LC_ALL=C sort +) +[ $((cache_adaptive_active_count + cache_adaptive_fallback_count)) -eq 3 ] || { + echo 'recovery adaptive cache outcomes do not cover all postgres epochs' >&2 + exit 1 +} +cache_validation_identity="$( + find "$report_dir" -type f \ + \( -name '*.cache-offers-validation.tsv' -o \ + -name '*.cache-offers-adaptive-validation.tsv' \) -print | + LC_ALL=C sort | + while IFS= read -r validation_file; do + fresh_wasmer_bin_hash "$validation_file" + done | + fresh_sha256_stream +)" +printf 'schema_version\tstatus\tmode\tclassification\tcarrier_closure_identity\tqualification_policy_sha256\tcache_telemetry_policy_sha256\tcache_validation_identity\tcache_observe_validation_count\tcache_adaptive_validation_count\tcache_adaptive_active_count\tcache_adaptive_fallback_count\timmutable_receipt_sha256\timmutable_receipt_dev\timmutable_receipt_ino\timmutable_verification_sha256\tcarrier_boundary_verification_sha256\tsealed_loader_audit_sha256\tsealed_loader_validation_sha256\tcarrier_snapshots_sha256\tcgroup_evidence_identity\tcgroup_memory_max_bytes\tcgroup_memory_high_bytes\tcgroup_swap_max_bytes\tcore_profile\tguest_build_recipe_sha256\n' \ + >"$evidence_envelope" +printf 'oliphaunt.wasix-postmaster.immediate-recovery-evidence.v3\tpassed\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$mode" "$classification" "$frozen_carrier_identity" \ + "$qualification_policy_sha256" "$cache_telemetry_policy_sha256" \ + "$cache_validation_identity" "$observed_cache_observe_validations" \ + "$observed_cache_adaptive_validations" \ + "$cache_adaptive_active_count" "$cache_adaptive_fallback_count" \ + "$immutable_receipt_sha256" \ + "$immutable_receipt_dev" "$immutable_receipt_ino" \ + "$immutable_verification_sha256" \ + "$carrier_boundary_verification_sha256" \ + "$sealed_loader_audit_sha256" "$sealed_loader_validation_sha256" \ + "$carrier_snapshots_sha256" "$cgroup_evidence_identity" \ + "$cgroup_memory_max_bytes" \ + "$cgroup_memory_high_bytes" "$cgroup_swap_max_bytes" \ + "$frozen_core_profile" "$frozen_guest_build_recipe_sha256" \ + >>"$evidence_envelope" +chmod 0444 "$immutable_verification" "$carrier_snapshots" \ + "$carrier_boundary_verification" "$evidence_envelope" \ + "$report_dir/run-retention.tsv" +if [ "$cgroup_enabled" -eq 1 ]; then + chmod 0444 "$report_dir/baseline.cgroup.tsv" \ + "$report_dir/recovery.cgroup.tsv" \ + "$report_dir/clean-reopen.cgroup.tsv" +fi + +current_stage="complete" +qualification_status="passed" + +printf 'WASIX immediate recovery %s passed: %s\n' \ + "$classification" "$report_dir" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-immediate-recovery.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-immediate-recovery.test.sh new file mode 100755 index 00000000..7f24dda5 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-immediate-recovery.test.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +qualifier="$root/bin/qualify-wasix-immediate-recovery.sh" +test_root="$(mktemp -d "${TMPDIR:-/tmp}/oliphaunt-immediate-recovery-test.XXXXXX")" +trap 'rm -rf -- "$test_root"' EXIT HUP INT TERM + +help_output="$("$qualifier" --help)" +for option in \ + '--mode MODE' \ + '--require-zero-write-aot' \ + '--immutable-carrier-receipt FILE' \ + '--cgroup-memory-max SIZE' \ + '--cgroup-memory-high SIZE' \ + '--cgroup-swap-max SIZE' +do + grep -Fq -- "$option" <<<"$help_output" +done +grep -Fq 'qualification mode requires --require-zero-write-aot' "$qualifier" +grep -Fq 'qualification mode requires finite --cgroup-memory-max' "$qualifier" +grep -Fq -- '--expected-initdb-executions 1' "$qualifier" +grep -Fq -- '--expected-postgres-executions 3' "$qualifier" +grep -Fq 'expected_outer_initdb_invocations' "$qualifier" +grep -Fq 'expected_outer_postgres_invocations' "$qualifier" +grep -Fq "initdb's bootstrap postgres and dynamic modules" <<<"$help_output" +grep -Fq 'validate-adaptive-file-cache-telemetry.py' "$qualifier" +grep -Fq 'Path::with_extension("adaptive.json")' "$qualifier" +grep -Fq 'immediate-recovery-evidence.v3' "$qualifier" +if grep -Fq 'OLIPHAUNT_WASIX_ADAPTIVE_CACHE_TELEMETRY_FILE' "$qualifier"; then + echo 'recovery qualifier added an adaptive policy environment surface' >&2 + exit 1 +fi + +for function_name in validate_cgroup_size cgroup_size_to_bytes configure_server_cgroup; do + awk -v signature="${function_name}() {" ' + $0 == signature { capture = 1 } + capture { print } + capture && /^}$/ { exit } + ' "$qualifier" >>"$test_root/cgroup-functions.sh" +done +# shellcheck source=/dev/null +source "$test_root/cgroup-functions.sh" + +validate_cgroup_size 256M +validate_cgroup_size 224MiB +validate_cgroup_size 0 +! validate_cgroup_size infinity +! validate_cgroup_size -1 +[ "$(cgroup_size_to_bytes 256M)" = 268435456 ] +[ "$(cgroup_size_to_bytes 224MiB)" = 234881024 ] +[ "$(cgroup_size_to_bytes 0)" = 0 ] +! cgroup_size_to_bytes 9223372036854775808 >/dev/null 2>&1 + +cgroup_enabled=1 +cgroup_memory_max=256M +cgroup_memory_high=224M +cgroup_swap_max=0 +active_cgroup_unit="" +server_command_prefix=() +configure_server_cgroup baseline +[ "$active_cgroup_unit" = "oliphaunt-recovery-$$-baseline" ] +prefix_text="$(printf '%s\n' "${server_command_prefix[@]}")" +grep -Fxq -- '--property=MemoryAccounting=yes' <<<"$prefix_text" +grep -Fxq -- '--property=MemoryMax=256M' <<<"$prefix_text" +grep -Fxq -- '--property=MemoryHigh=224M' <<<"$prefix_text" +grep -Fxq -- '--property=MemorySwapMax=0' <<<"$prefix_text" +cgroup_enabled=0 +configure_server_cgroup recovery +[ -z "$active_cgroup_unit" ] +[ "${#server_command_prefix[@]}" -eq 0 ] + +awk ' + /^wait_for_unassisted_exit\(\) \{$/ { capture = 1 } + capture { print } + capture && /^}$/ { exit } +' "$qualifier" >"$test_root/wait-for-unassisted-exit.sh" +grep -Fq 'wait_for_unassisted_exit() {' \ + "$test_root/wait-for-unassisted-exit.sh" +# shellcheck source=/dev/null +source "$test_root/wait-for-unassisted-exit.sh" + +fresh_supervision_now_ms() { + printf '1000\n' +} + +fresh_supervision_pid_running() { + return 1 +} + +fresh_pid_matches_birth_identity() { + return 0 +} + +fresh_reap_process_group_leader() { + FRESH_PROCESS_GROUP_WAIT_STATUS="$fixture_wait_status" +} + +fresh_process_group_exists() { + return 1 +} + +fresh_wait_tcp_port_closed() { + return 0 +} + +reset_fixture() { + fixture_wait_status="$1" + active_pid=4242 + active_pgid=4242 + active_identity=linux-starttime:303 + active_phase=fixture-shutdown +} + +timeout_seconds=1 +port=15432 +dev_shm="$test_root/dev-shm" +active_cgroup_unit="" +active_cgroup_dir="" +active_cgroup_identity="" +mkdir -p "$dev_shm" + +reset_fixture 17 +set +e +wait_for_unassisted_exit "$test_root/nonzero-exit.tsv" \ + >"$test_root/nonzero.out" 2>"$test_root/nonzero.err" +status=$? +set -e +[ "$status" -eq 1 ] || { + printf 'expected nonzero leader status to reject recovery evidence, got %s\n' \ + "$status" >&2 + exit 1 +} +[ ! -e "$test_root/nonzero-exit.tsv" ] || { + printf 'nonzero leader status produced successful recovery evidence\n' >&2 + exit 1 +} +grep -Fqx \ + 'server leader exited nonzero after unassisted guest shutdown: phase=fixture-shutdown status=17' \ + "$test_root/nonzero.err" +[ "$active_pid" = 4242 ] +[ "$active_pgid" = 4242 ] +[ "$active_identity" = linux-starttime:303 ] +[ "$active_phase" = fixture-shutdown ] + +reset_fixture 0 +wait_for_unassisted_exit "$test_root/zero-exit.tsv" +awk -F '\t' ' + NR == 1 { + valid = ($1 == "phase" && $2 == "wait_status" && + $3 == "process_group_empty" && $4 == "cgroup_empty" && + $5 == "port_closed" && $6 == "shared_objects_empty" && + $7 == "escalation_used") + } + NR == 2 { + valid = valid && ($1 == "fixture-shutdown" && $2 == "0" && + $3 == "true" && $4 == "not-requested" && $5 == "true" && + $6 == "true" && $7 == "false") + } + END { exit !(valid && NR == 2) } +' "$test_root/zero-exit.tsv" +[ -z "$active_pid" ] +[ -z "$active_pgid" ] +[ -z "$active_identity" ] +[ -z "$active_phase" ] + +fresh_wait_cgroup_empty() { + [ "$1" = "$test_root/fake-cgroup" ] + [ "$2" = 42:99 ] + [ "$3" = 1000 ] + cgroup_wait_called=1 +} +reset_fixture 0 +active_cgroup_unit=oliphaunt-recovery-fixture +active_cgroup_dir="$test_root/fake-cgroup" +active_cgroup_identity=42:99 +cgroup_wait_called=0 +wait_for_unassisted_exit "$test_root/cgroup-exit.tsv" +[ "$cgroup_wait_called" -eq 1 ] +awk -F '\t' 'NR == 2 { exit !($4 == "true") }' \ + "$test_root/cgroup-exit.tsv" + +printf 'immediate recovery exit tests passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-libpq-latency.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-libpq-latency.sh new file mode 100755 index 00000000..64ec7dd4 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-libpq-latency.sh @@ -0,0 +1,796 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" +source "$FRESH_ROOT/lib/sealed-carrier.sh" +source "$FRESH_ROOT/lib/postgres-profiles.sh" +source "$FRESH_ROOT/lib/qualification-identities.sh" + +usage() { + cat <<'USAGE' +Usage: qualify-wasix-libpq-latency.sh --sealed-carrier DIR [options] + +Run true-libpq persistent-query and reconnect latency on fresh native and WASIX +servers in alternating ABBA/BAAB blocks. The comparator re-opens every raw +CLOCK_MONOTONIC sample stream, verifies exact carrier/native/libpq/probe/profile +identities, and gates the p95 of server-pair and server-run tail distributions. + +Options: + --sealed-carrier DIR Compiler-free carrier. Required. + --require-zero-write-aot Require direct immutable AOT/memory + activation and validated loader audit. + --immutable-carrier-receipt FILE Exact external Linux immutable-deployment + receipt. Required with the option above. + --blocks N Even count of four-server blocks. Default: 10. + --samples N Measured calls per mode/server. Default: 1000. + --warmup N Warmup calls per mode/server. Default: 100. + --timeout SECONDS Per benchmark operation. Default: 300. + --start-port PORT Reused after verified shutdown. Default: 55920. + --label NAME Default: UTC timestamp. + --runtime-footprint ID Default: embedded-concurrent. + --durability ID Default: safe. + --cgroup-memory-max SIZE Bind every measured postmaster to a + dedicated systemd user scope with + MemoryMax=SIZE. + --cgroup-memory-high SIZE Bind that scope with MemoryHigh=SIZE. + --cgroup-swap-max SIZE Bind that scope with MemorySwapMax=SIZE. + All three limits must be configured + together. Defaults are inherited from the + matching WASIX_CGROUP_* variables; an + entirely unset triple disables the scope. + --max-persistent-p95-ratio R Default: 2.0. + --max-persistent-p99-ratio R Default: 2.5. + --max-reconnect-p95-ratio R Default: 3.5. + --max-reconnect-p99-ratio R Default: 4.5. + --max-wasix-persistent-p95-ms N Default: 0.25 ms. + --max-wasix-persistent-p99-ms N Default: 0.40 ms. + --max-wasix-reconnect-p95-ms N Default: 20 ms. + --max-wasix-reconnect-p99-ms N Default: 30 ms. + --keep-pgdata Retain successful generated clusters. + -h, --help Show this help. + +Promotion evidence requires at least 10 blocks, 100 warmups, and 1000 measured +samples per mode/server. Smaller runs remain useful but are classified as +diagnostic. Resource-detail sampling and WASIX perf statistics are always off +in this timed lane; qualify memory independently. +USAGE +} + +sealed_carrier="" +blocks="${WASIX_LIBPQ_QUALIFICATION_BLOCKS:-10}" +samples="${WASIX_LIBPQ_QUALIFICATION_SAMPLES:-1000}" +warmup="${WASIX_LIBPQ_QUALIFICATION_WARMUP:-100}" +timeout_seconds="${WASIX_LIBPQ_QUALIFICATION_TIMEOUT:-300}" +start_port="${WASIX_LIBPQ_QUALIFICATION_PORT:-55920}" +run_label="${WASIX_LIBPQ_QUALIFICATION_LABEL:-$(date -u +%Y%m%dT%H%M%SZ)}" +runtime_footprint="${WASIX_RUNTIME_FOOTPRINT:-embedded-concurrent}" +durability_profile="${WASIX_DURABILITY_PROFILE:-safe}" +max_persistent_p95_ratio="${WASIX_LIBPQ_MAX_PERSISTENT_P95_RATIO:-2.0}" +max_persistent_p99_ratio="${WASIX_LIBPQ_MAX_PERSISTENT_P99_RATIO:-2.5}" +max_reconnect_p95_ratio="${WASIX_LIBPQ_MAX_RECONNECT_P95_RATIO:-3.5}" +max_reconnect_p99_ratio="${WASIX_LIBPQ_MAX_RECONNECT_P99_RATIO:-4.5}" +max_wasix_persistent_p95_ms="${WASIX_LIBPQ_MAX_WASIX_PERSISTENT_P95_MS:-0.25}" +max_wasix_persistent_p99_ms="${WASIX_LIBPQ_MAX_WASIX_PERSISTENT_P99_MS:-0.40}" +max_wasix_reconnect_p95_ms="${WASIX_LIBPQ_MAX_WASIX_RECONNECT_P95_MS:-20}" +max_wasix_reconnect_p99_ms="${WASIX_LIBPQ_MAX_WASIX_RECONNECT_P99_MS:-30}" +discard_pgdata=1 +require_zero_write_aot=0 +immutable_carrier_receipt="" +cgroup_memory_max="${WASIX_CGROUP_MEMORY_MAX:-}" +cgroup_memory_high="${WASIX_CGROUP_MEMORY_HIGH:-}" +cgroup_swap_max="${WASIX_CGROUP_SWAP_MAX:-}" +wait_dump_environment_names=( + WASIX_PERF_WAIT_DUMP_INTERVAL_MS + WASIX_PERF_WAIT_DUMP_FILE + WASIX_PERF_WAIT_DUMP_MAX_PER_WAIT + WASIX_PERF_WAIT_DUMP_VERBOSE + WASIX_WAIT_DUMP_INTERVAL_MS + WASIX_WAIT_DUMP_FILE + WASIX_WAIT_DUMP_MAX_PER_WAIT + WASIX_WAIT_DUMP_VERBOSE + WASIX_WAIT_DUMP_FENCE_REQUEST_FILE + WASIX_WAIT_DUMP_FENCE_ACK_FILE +) +wait_dump_unset_args=() +for wait_dump_name in "${wait_dump_environment_names[@]}"; do + wait_dump_unset_args+=(-u "$wait_dump_name") +done +wait_dump_unset_args+=( + -u OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT + -u OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE + -u WASIX_CGROUP_MEMORY_MAX + -u WASIX_CGROUP_MEMORY_HIGH + -u WASIX_CGROUP_SWAP_MAX +) +wait_dump_sanitized_environment="$( + IFS=, + printf '%s' "${wait_dump_environment_names[*]}" +)" + +while [ "$#" -gt 0 ]; do + case "$1" in + --sealed-carrier|--immutable-carrier-receipt|--blocks|--samples|--warmup|--timeout|--start-port|--label|--runtime-footprint|--durability|--cgroup-memory-max|--cgroup-memory-high|--cgroup-swap-max|--max-persistent-p95-ratio|--max-persistent-p99-ratio|--max-reconnect-p95-ratio|--max-reconnect-p99-ratio|--max-wasix-persistent-p95-ms|--max-wasix-persistent-p99-ms|--max-wasix-reconnect-p95-ms|--max-wasix-reconnect-p99-ms) + option="$1" + shift + [ "$#" -gt 0 ] || { printf '%s requires a value\n' "$option" >&2; exit 2; } + case "$option" in + --sealed-carrier) sealed_carrier="$1" ;; + --immutable-carrier-receipt) + [ -z "$immutable_carrier_receipt" ] || { + echo '--immutable-carrier-receipt may only be specified once' >&2 + exit 2 + } + immutable_carrier_receipt="$1" + ;; + --blocks) blocks="$1" ;; + --samples) samples="$1" ;; + --warmup) warmup="$1" ;; + --timeout) timeout_seconds="$1" ;; + --start-port) start_port="$1" ;; + --label) run_label="$1" ;; + --runtime-footprint) runtime_footprint="$1" ;; + --durability) durability_profile="$1" ;; + --cgroup-memory-max) cgroup_memory_max="$1" ;; + --cgroup-memory-high) cgroup_memory_high="$1" ;; + --cgroup-swap-max) cgroup_swap_max="$1" ;; + --max-persistent-p95-ratio) max_persistent_p95_ratio="$1" ;; + --max-persistent-p99-ratio) max_persistent_p99_ratio="$1" ;; + --max-reconnect-p95-ratio) max_reconnect_p95_ratio="$1" ;; + --max-reconnect-p99-ratio) max_reconnect_p99_ratio="$1" ;; + --max-wasix-persistent-p95-ms) max_wasix_persistent_p95_ms="$1" ;; + --max-wasix-persistent-p99-ms) max_wasix_persistent_p99_ms="$1" ;; + --max-wasix-reconnect-p95-ms) max_wasix_reconnect_p95_ms="$1" ;; + --max-wasix-reconnect-p99-ms) max_wasix_reconnect_p99_ms="$1" ;; + esac + ;; + --keep-pgdata) discard_pgdata=0 ;; + --require-zero-write-aot) + [ "$require_zero_write_aot" -eq 0 ] || { + echo '--require-zero-write-aot may only be specified once' >&2 + exit 2 + } + require_zero_write_aot=1 + ;; + -h|--help) usage; exit 0 ;; + *) printf 'unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +is_positive_integer() { [[ "$1" =~ ^[1-9][0-9]*$ ]]; } +is_nonnegative_integer() { [[ "$1" =~ ^(0|[1-9][0-9]*)$ ]]; } +is_positive_number() { + [[ "$1" =~ ^([0-9]+([.][0-9]*)?|[.][0-9]+)$ ]] && + awk -v value="$1" 'BEGIN { exit !(value > 0) }' +} +validate_cgroup_size() { + [[ "$1" =~ ^[0-9]+([KMGTPE]([i]?B)?)?$ ]] +} + +classify_latency_result() { + local status="$1" + local block_count="$2" + local warmup_count="$3" + local sample_count="$4" + + if [ "$block_count" -ge 10 ] && [ "$warmup_count" -ge 100 ] && + [ "$sample_count" -ge 1000 ]; then + if [ "$status" -eq 0 ]; then + printf 'latency-qualified-non-promotional' + else + printf 'failed-latency-qualification-non-promotional' + fi + else + printf 'latency-diagnostic-non-promotional' + fi +} + +[ "$(uname -s)" = Linux ] || { + echo 'true-libpq promotion qualification requires Linux host-FD evidence' >&2 + exit 2 +} +[ -n "$sealed_carrier" ] || { echo '--sealed-carrier is required' >&2; exit 2; } +[ -d "$sealed_carrier" ] || { printf 'missing sealed carrier: %s\n' "$sealed_carrier" >&2; exit 2; } +sealed_carrier="$(cd "$sealed_carrier" && pwd -P)" +fresh_capture_qualification_carrier_identity "$sealed_carrier" || { + printf 'sealed carrier verification failed before qualification: %s\n' "$sealed_carrier" >&2 + exit 1 +} +frozen_carrier_identity="$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" +frozen_core_profile="$FRESH_QUALIFICATION_CORE_PROFILE" +frozen_guest_build_recipe_sha256="$FRESH_QUALIFICATION_GUEST_BUILD_RECIPE_SHA256" +frozen_carrier_manifest_sha="$FRESH_QUALIFICATION_CARRIER_MANIFEST_SHA256" +frozen_carrier_receipt_sha="$FRESH_QUALIFICATION_CARRIER_RECEIPT_SHA256" +frozen_carrier_payload_sha="$FRESH_QUALIFICATION_CARRIER_PAYLOAD_SHA256" +frozen_carrier_headless_sha="$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" + +if [ "$require_zero_write_aot" -eq 1 ]; then + [ -n "$immutable_carrier_receipt" ] || { + echo '--require-zero-write-aot requires --immutable-carrier-receipt' >&2 + exit 2 + } +elif [ -n "$immutable_carrier_receipt" ]; then + echo '--immutable-carrier-receipt requires --require-zero-write-aot' >&2 + exit 2 +fi +is_positive_integer "$blocks" && [ $((blocks % 2)) -eq 0 ] || { + echo '--blocks requires a positive even integer for equal ABBA/BAAB representation' >&2 + exit 2 +} +is_positive_integer "$samples" || { echo '--samples requires a positive integer' >&2; exit 2; } +is_nonnegative_integer "$warmup" || { echo '--warmup requires a nonnegative integer' >&2; exit 2; } +is_positive_integer "$timeout_seconds" || { echo '--timeout requires a positive integer' >&2; exit 2; } +if ! is_positive_integer "$start_port" || [ "$start_port" -gt 65535 ]; then + echo '--start-port requires a port from 1 through 65535' >&2 + exit 2 +fi +case "$run_label" in + ""|*[!A-Za-z0-9._-]*) echo '--label contains unsupported characters' >&2; exit 2 ;; +esac +cgroup_limit_count=0 +for cgroup_size in "$cgroup_memory_max" "$cgroup_memory_high" "$cgroup_swap_max"; do + if [ -n "$cgroup_size" ]; then + cgroup_limit_count=$((cgroup_limit_count + 1)) + validate_cgroup_size "$cgroup_size" || { + printf 'invalid cgroup size: %s\n' "$cgroup_size" >&2 + exit 2 + } + fi +done +case "$cgroup_limit_count" in + 0) + cgroup_binding=disabled + cgroup_environment_action=ambient-sanitized-disabled + ;; + 3) + command -v systemd-run >/dev/null 2>&1 || { + echo 'cgroup memory controls require systemd-run' >&2 + exit 127 + } + cgroup_binding=dedicated-systemd-user-scope + cgroup_environment_action=ambient-sanitized-explicit-argv + ;; + *) + echo '--cgroup-memory-max, --cgroup-memory-high, and --cgroup-swap-max must be configured together' >&2 + exit 2 + ;; +esac +for threshold in \ + "$max_persistent_p95_ratio" "$max_persistent_p99_ratio" \ + "$max_reconnect_p95_ratio" "$max_reconnect_p99_ratio" \ + "$max_wasix_persistent_p95_ms" "$max_wasix_persistent_p99_ms" \ + "$max_wasix_reconnect_p95_ms" "$max_wasix_reconnect_p99_ms" +do + is_positive_number "$threshold" || { + printf 'all latency gates require finite positive decimal values, got: %s\n' "$threshold" >&2 + exit 2 + } +done +awk -v p95="$max_persistent_p95_ratio" -v p99="$max_persistent_p99_ratio" \ + 'BEGIN { exit !(p99 >= p95) }' || { echo 'persistent p99 ratio gate must be >= p95 ratio gate' >&2; exit 2; } +awk -v p95="$max_reconnect_p95_ratio" -v p99="$max_reconnect_p99_ratio" \ + 'BEGIN { exit !(p99 >= p95) }' || { echo 'reconnect p99 ratio gate must be >= p95 ratio gate' >&2; exit 2; } +awk -v p95="$max_wasix_persistent_p95_ms" -v p99="$max_wasix_persistent_p99_ms" \ + 'BEGIN { exit !(p99 >= p95) }' || { echo 'persistent p99 absolute gate must be >= p95 gate' >&2; exit 2; } +awk -v p95="$max_wasix_reconnect_p95_ms" -v p99="$max_wasix_reconnect_p99_ms" \ + 'BEGIN { exit !(p99 >= p95) }' || { echo 'reconnect p99 absolute gate must be >= p95 gate' >&2; exit 2; } + +fresh_resolve_postgres_profiles "$runtime_footprint" "$durability_profile" || exit +[ -n "$FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY" ] || { + echo 'latency qualification requires named PostgreSQL profiles' >&2 + exit 2 +} + +qualification_root="$REPORT_DIR/libpq-latency-qualification/$run_label" +runs_tsv="$qualification_root/runs.tsv" +profile_comparisons_tsv="$qualification_root/profile-comparisons.tsv" +profile_inputs_tsv="$qualification_root/postgres-profile-inputs.tsv" +profile_resolution_tsv="$qualification_root/postgres-profile-resolution.tsv" +plan_tsv="$qualification_root/qualification-plan.tsv" +native_oracle_tsv="$qualification_root/native-oracle-identity.tsv" +carrier_identity_tsv="$qualification_root/carrier-identity.tsv" +carrier_verification_tsv="$qualification_root/carrier-verification.tsv" +native_verification_tsv="$qualification_root/native-oracle-verification.tsv" +samples_tsv="$qualification_root/samples.tsv" +pairs_tsv="$qualification_root/paired-samples.tsv" +paired_summary_tsv="$qualification_root/paired-summary.tsv" +identity_tsv="$qualification_root/sample-identity.tsv" +result_tsv="$qualification_root/qualification-result.tsv" +sealed_loader_verification_tsv="$qualification_root/sealed-loader-verification.tsv" +immutable_carrier_verification_tsv="$qualification_root/immutable-carrier-verification.tsv" +wasix_execution_identity_tsv="$qualification_root/wasix-execution-identity.tsv" +wasix_execution_identity_sha256=none +wasix_postgres_module_sha256=none +summary_md="$qualification_root/summary.md" +harness="$FRESH_ROOT/bin/bench-wasix-concurrent-query-suite.sh" +comparator="$FRESH_ROOT/bin/compare-libpq-latency.py" +probe_source="$FRESH_ROOT/probes/libpq_latency_probe.c" +immutable_carrier_receipt_sha256=none +immutable_carrier_receipt_dev=none +immutable_carrier_receipt_ino=none +immutable_carrier_closure_identity=none + +if [ -e "$qualification_root" ] || [ -L "$qualification_root" ]; then + printf 'qualification label already exists: %s\n' "$qualification_root" >&2 + exit 2 +fi + +if [ "$require_zero_write_aot" -eq 1 ]; then + receipt_parent="$(dirname "$immutable_carrier_receipt")" + [ -d "$receipt_parent" ] && [ ! -L "$receipt_parent" ] || { + printf 'immutable carrier receipt parent must be a non-symlink directory: %s\n' \ + "$receipt_parent" >&2 + exit 2 + } + immutable_carrier_receipt="$(cd "$receipt_parent" && pwd -P)/$(basename "$immutable_carrier_receipt")" + "$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier" \ + --receipt "$immutable_carrier_receipt" --fast || { + echo 'immutable carrier deployment verification failed before latency qualification' >&2 + exit 1 + } + fresh_capture_stable_regular_file_identity "$immutable_carrier_receipt" || { + echo 'immutable carrier receipt changed while its identity was captured' >&2 + exit 1 + } + immutable_carrier_receipt_sha256="$FRESH_QUALIFICATION_REGULAR_FILE_SHA256" + immutable_carrier_receipt_dev="$FRESH_QUALIFICATION_REGULAR_FILE_DEVICE" + immutable_carrier_receipt_ino="$FRESH_QUALIFICATION_REGULAR_FILE_INODE" +fi + +immutable_carrier_closure_identity="$([ "$require_zero_write_aot" -eq 1 ] && printf '%s' "$frozen_carrier_identity" || printf none)" +fresh_capture_native_oracle_identity "$NATIVE_INSTALL_DIR" || { + printf 'native oracle verification failed before qualification: %s\n' "$NATIVE_INSTALL_DIR" >&2 + exit 1 +} +frozen_native_identity="$FRESH_QUALIFICATION_NATIVE_ORACLE_IDENTITY" +frozen_profile_identity="$FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY" +probe_source_sha="$(fresh_wasmer_bin_hash "$probe_source")" + +mkdir -p "$qualification_root/logs" "$qualification_root/profile-comparisons" \ + "$qualification_root/carrier-provenance" +printf 'schema_version\tstatus\tclassification\tdetail\tcarrier_closure_identity\tnative_oracle_identity\tpostgres_profile_resolution_identity\truntime_footprint_sha256\tdurability_profile_sha256\tqualification_plan_sha256\tsamples_sha256\tpaired_samples_sha256\tpaired_summary_sha256\tsample_identity_sha256\twasix_execution_identity_sha256\tpostgres_module_sha256\timmutable_carrier_verification_sha256\tsealed_loader_verification_sha256\tcore_profile\tguest_build_recipe_sha256\timmutable_verification_scope\tcgroup_binding\tcgroup_memory_max\tcgroup_memory_high\tcgroup_swap_max\tcgroup_environment_action\n' >"$result_tsv" +result_written=0 +result_hash_or_none() { + if [ -f "$1" ] && [ ! -L "$1" ]; then + fresh_wasmer_bin_hash "$1" + else + printf none + fi +} +finish_result() { + local status="$1" classification="$2" detail="$3" + [ "$result_written" -eq 0 ] || return 0 + printf 'oliphaunt.wasix-postmaster.latency-result.v4\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$status" "$classification" "$detail" \ + "${frozen_carrier_identity:-none}" "${frozen_native_identity:-none}" \ + "${frozen_profile_identity:-none}" \ + "${FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256:-none}" \ + "${FRESH_POSTGRES_DURABILITY_SHA256:-none}" \ + "${plan_identity:-none}" "$(result_hash_or_none "$samples_tsv")" \ + "$(result_hash_or_none "$pairs_tsv")" \ + "$(result_hash_or_none "$paired_summary_tsv")" \ + "$(result_hash_or_none "$identity_tsv")" \ + "$wasix_execution_identity_sha256" "$wasix_postgres_module_sha256" \ + "$(result_hash_or_none "$immutable_carrier_verification_tsv")" \ + "$(result_hash_or_none "$sealed_loader_verification_tsv")" \ + "${frozen_core_profile:-none}" \ + "${frozen_guest_build_recipe_sha256:-none}" \ + "$([ "${require_zero_write_aot:-0}" -eq 1 ] && printf campaign-boundary-full-fast-samples || printf full-per-check)" \ + "${cgroup_binding:-none}" "${cgroup_memory_max:-none}" \ + "${cgroup_memory_high:-none}" "${cgroup_swap_max:-none}" \ + "${cgroup_environment_action:-none}" \ + >>"$result_tsv" + result_written=1 +} +on_exit() { + local status=$? + if [ "$result_written" -eq 0 ]; then + finish_result failed latency-incomplete-non-promotional "runner-exit-$status" + fi +} +trap on_exit EXIT + +fresh_write_postgres_profile_evidence "$profile_inputs_tsv" "$profile_resolution_tsv" +fresh_write_native_oracle_manifest "$NATIVE_INSTALL_DIR" "$native_oracle_tsv" +[ "$(fresh_wasmer_bin_hash "$native_oracle_tsv")" = "$frozen_native_identity" ] || { + echo 'native oracle receipt does not match its frozen identity' >&2 + exit 1 +} +cp -p "$sealed_carrier/manifest.json" "$qualification_root/carrier-provenance/manifest.json" +cp -p "$sealed_carrier/wasmer-build.receipt" "$qualification_root/carrier-provenance/wasmer-build.receipt" +cp -p "$sealed_carrier/payload.files" "$qualification_root/carrier-provenance/payload.files" +[ "$(fresh_wasmer_bin_hash "$qualification_root/carrier-provenance/manifest.json")" = \ + "$frozen_carrier_manifest_sha" ] && + [ "$(fresh_wasmer_bin_hash "$qualification_root/carrier-provenance/wasmer-build.receipt")" = \ + "$frozen_carrier_receipt_sha" ] && + [ "$(fresh_wasmer_bin_hash "$qualification_root/carrier-provenance/payload.files")" = \ + "$frozen_carrier_payload_sha" ] || { + echo 'copied carrier provenance does not match the frozen closure' >&2 + exit 1 + } +printf 'closure_identity\tmanifest_sha256\treceipt_sha256\tpayload_inventory_sha256\theadless_sha256\tcarrier_root\tcore_profile\tguest_build_recipe_sha256\n' >"$carrier_identity_tsv" +printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$frozen_carrier_identity" \ + "$frozen_carrier_manifest_sha" "$frozen_carrier_receipt_sha" \ + "$frozen_carrier_payload_sha" "$frozen_carrier_headless_sha" \ + "$sealed_carrier" "$frozen_core_profile" \ + "$frozen_guest_build_recipe_sha256" >>"$carrier_identity_tsv" + +# This receipt exists and is frozen before the first server starts. Defaults +# therefore cannot silently drift after results are known. +printf 'schema_version\tblocks\twarmup\tsamples\ttimeout_seconds\tstart_port\torder\truntime_footprint\tdurability\tcheckpoint_policy\tresource_detail\thost_fd_allowance\tsanitized_environment\tmax_persistent_p95_ratio\tmax_persistent_p99_ratio\tmax_reconnect_p95_ratio\tmax_reconnect_p99_ratio\tmax_wasix_persistent_p95_ms\tmax_wasix_persistent_p99_ms\tmax_wasix_reconnect_p95_ms\tmax_wasix_reconnect_p99_ms\trequire_zero_write_aot\tactivation_policy\tcarrier_closure_identity\timmutable_receipt_path\timmutable_receipt_sha256\timmutable_receipt_dev\timmutable_receipt_ino\tcore_profile\tguest_build_recipe_sha256\timmutable_verification_scope\tcgroup_binding\tcgroup_memory_max\tcgroup_memory_high\tcgroup_swap_max\tcgroup_environment_action\n' >"$plan_tsv" +printf 'oliphaunt.wasix-postmaster.libpq-latency-plan.v4\t%s\t%s\t%s\t%s\t%s\tABBA/BAAB\t%s\t%s\tcontrolled\toff\t0\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$blocks" "$warmup" "$samples" "$timeout_seconds" "$start_port" \ + "$runtime_footprint" "$durability_profile" "$wait_dump_sanitized_environment" \ + "$max_persistent_p95_ratio" "$max_persistent_p99_ratio" \ + "$max_reconnect_p95_ratio" "$max_reconnect_p99_ratio" \ + "$max_wasix_persistent_p95_ms" "$max_wasix_persistent_p99_ms" \ + "$max_wasix_reconnect_p95_ms" "$max_wasix_reconnect_p99_ms" \ + "$require_zero_write_aot" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf direct-immutable-only || printf compatibility)" \ + "$frozen_carrier_identity" "${immutable_carrier_receipt:-none}" \ + "$immutable_carrier_receipt_sha256" "$immutable_carrier_receipt_dev" \ + "$immutable_carrier_receipt_ino" \ + "$frozen_core_profile" "$frozen_guest_build_recipe_sha256" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf campaign-boundary-full-fast-samples || printf full-per-check)" \ + "$cgroup_binding" "${cgroup_memory_max:-none}" \ + "${cgroup_memory_high:-none}" "${cgroup_swap_max:-none}" \ + "$cgroup_environment_action" \ + >>"$plan_tsv" +plan_identity="$(fresh_wasmer_bin_hash "$plan_tsv")" +chmod 0444 "$plan_tsv" "$native_oracle_tsv" "$carrier_identity_tsv" \ + "$qualification_root/carrier-provenance/manifest.json" \ + "$qualification_root/carrier-provenance/wasmer-build.receipt" \ + "$qualification_root/carrier-provenance/payload.files" + +printf 'stage\texpected_closure_identity\tobserved_closure_identity\tstatus\n' >"$carrier_verification_tsv" +printf 'initial\t%s\t%s\tpassed\n' "$frozen_carrier_identity" "$frozen_carrier_identity" >>"$carrier_verification_tsv" +printf 'stage\texpected_native_oracle_identity\tobserved_native_oracle_identity\tstatus\n' >"$native_verification_tsv" +printf 'initial\t%s\t%s\tpassed\n' "$frozen_native_identity" "$frozen_native_identity" >>"$native_verification_tsv" +printf 'stage\texpected_receipt_sha256\tobserved_receipt_sha256\texpected_receipt_dev\tobserved_receipt_dev\texpected_receipt_ino\tobserved_receipt_ino\tstatus\n' \ + >"$immutable_carrier_verification_tsv" + +assert_frozen_carrier() { + local stage="$1" verification="${2:-fast}" observed="" status=passed + if [ "$require_zero_write_aot" -eq 1 ] && [ "$verification" = fast ]; then + observed="$frozen_carrier_identity" + elif fresh_capture_qualification_carrier_identity "$sealed_carrier"; then + observed="$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" + else + status=verification-failed + fi + if [ "$status" = passed ] && [ "$observed" != "$frozen_carrier_identity" ]; then + status=identity-changed + fi + printf '%s\t%s\t%s\t%s\n' "$stage" "$frozen_carrier_identity" "$observed" "$status" >>"$carrier_verification_tsv" + [ "$status" = passed ] || { + printf 'carrier identity failed at %s: %s\n' "$stage" "$status" >&2 + return 1 + } +} + +assert_frozen_native_oracle() { + local stage="$1" observed="" status=passed + if fresh_capture_native_oracle_identity "$NATIVE_INSTALL_DIR"; then + observed="$FRESH_QUALIFICATION_NATIVE_ORACLE_IDENTITY" + else + status=verification-failed + fi + if [ "$status" = passed ] && [ "$observed" != "$frozen_native_identity" ]; then + status=identity-changed + fi + printf '%s\t%s\t%s\t%s\n' "$stage" "$frozen_native_identity" "$observed" "$status" >>"$native_verification_tsv" + [ "$status" = passed ] || { + printf 'native oracle identity failed at %s: %s\n' "$stage" "$status" >&2 + return 1 + } +} + +assert_frozen_immutable_carrier() { + local stage="$1" observed_sha=none observed_dev=none observed_ino=none + local status=passed + [ "$require_zero_write_aot" -eq 1 ] || return 0 + if ! "$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier" \ + --receipt "$immutable_carrier_receipt" --fast; then + status=unreadable + fi + if fresh_capture_stable_regular_file_identity "$immutable_carrier_receipt"; then + observed_sha="$FRESH_QUALIFICATION_REGULAR_FILE_SHA256" + observed_dev="$FRESH_QUALIFICATION_REGULAR_FILE_DEVICE" + observed_ino="$FRESH_QUALIFICATION_REGULAR_FILE_INODE" + else + status=unreadable + fi + if [ "$status" = passed ] && { + [ "$observed_sha" != "$immutable_carrier_receipt_sha256" ] || + [ "$observed_dev" != "$immutable_carrier_receipt_dev" ] || + [ "$observed_ino" != "$immutable_carrier_receipt_ino" ]; + }; then + status=identity-changed + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$stage" "$immutable_carrier_receipt_sha256" "$observed_sha" \ + "$immutable_carrier_receipt_dev" "$observed_dev" \ + "$immutable_carrier_receipt_ino" "$observed_ino" "$status" \ + >>"$immutable_carrier_verification_tsv" + [ "$status" = passed ] || { + printf 'immutable carrier deployment identity failed at %s: %s\n' \ + "$stage" "$status" >&2 + return 1 + } +} + +assert_frozen_inputs() { + local stage="$1" + [ "$(fresh_wasmer_bin_hash "$plan_tsv")" = "$plan_identity" ] || { + printf 'qualification plan changed at %s\n' "$stage" >&2 + return 1 + } + [ "$(fresh_wasmer_bin_hash "$probe_source")" = "$probe_source_sha" ] || { + printf 'libpq probe source changed at %s\n' "$stage" >&2 + return 1 + } + [ "$FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY" = "$frozen_profile_identity" ] || { + printf 'PostgreSQL profile resolution changed at %s\n' "$stage" >&2 + return 1 + } + fresh_assert_postgres_profile_inputs +} + +printf 'schema_version\tblock\torder\tpair\tposition\ttarget\trun_label\tharness_status\treport_dir\teffective_settings\teffective_settings_sha256\tcarrier_closure_identity\tnative_oracle_identity\tpostgres_profile_resolution_identity\tqualification_plan_identity\n' >"$runs_tsv" +printf 'schema_version\tblock\torder\tpair\tnative_settings\twasix_settings\tcomparison\tcomparison_sha256\tstatus\n' >"$profile_comparisons_tsv" +printf 'sample\ttarget\tpolicy_receipt\tpolicy_sha256\taudit_receipt\taudit_sha256\tvalidation_receipt\tvalidation_sha256\tstatus\n' \ + >"$sealed_loader_verification_tsv" + +overall_status=0 +for ((block = 1; block <= blocks; block++)); do + if [ $((block % 2)) -eq 1 ]; then + order=ABBA + target_order=(native wasix wasix native) + else + order=BAAB + target_order=(wasix native native wasix) + fi + pair_first_target="" + pair_first_settings="" + for position_index in "${!target_order[@]}"; do + position=$((position_index + 1)) + pair=$(((position_index / 2) + 1)) + target="${target_order[$position_index]}" + sample_label="$(printf '%s-b%02d-p%d-%s' "$run_label" "$block" "$position" "$target")" + sample_log="$qualification_root/logs/$sample_label.log" + sample_report="$REPORT_DIR/concurrent-query-suite/$sample_label" + settings="$sample_report/$target/effective-postgres-settings.tsv" + args=( + --skip-build + --target "$target" + --libpq-latency-only + --libpq-latency-warmup "$warmup" + --libpq-latency-samples "$samples" + --timeout "$timeout_seconds" + --start-port "$start_port" + --checkpoint-policy controlled + --resource-detail off + --runtime-footprint "$runtime_footprint" + --durability "$durability_profile" + --label "$sample_label" + ) + if [ "$cgroup_binding" != disabled ]; then + args+=( + --cgroup-memory-max "$cgroup_memory_max" + --cgroup-memory-high "$cgroup_memory_high" + --cgroup-swap-max "$cgroup_swap_max" + ) + fi + if [ "$discard_pgdata" -eq 1 ]; then args+=(--discard-pgdata); fi + if [ "$target" = wasix ]; then + args+=(--sealed-carrier "$sealed_carrier") + if [ "$require_zero_write_aot" -eq 1 ]; then + args+=( + --require-zero-write-aot + --immutable-carrier-receipt "$immutable_carrier_receipt" + --immutable-carrier-verification-scope campaign-fast + ) + fi + fi + + printf 'latency block=%s order=%s pair=%s position=%s target=%s\n' \ + "$block" "$order" "$pair" "$position" "$target" + assert_frozen_inputs "$sample_label:before" || exit 1 + assert_frozen_carrier "$sample_label:before" || exit 1 + assert_frozen_immutable_carrier "$sample_label:before" || exit 1 + assert_frozen_native_oracle "$sample_label:before" || exit 1 + set +e + env \ + "${wait_dump_unset_args[@]}" \ + WASIX_PERF_STATS=0 \ + WASIX_LIBPQ_LATENCY_HOST_FD_ALLOWANCE=0 \ + "$harness" "${args[@]}" >"$sample_log" 2>&1 + harness_status=$? + set -e + sample_loader_status=0 + sample_loader_policy="$sample_report/sealed-loader-policy.tsv" + sample_loader_audit="$sample_report/$target/sealed-loader-audit.jsonl" + sample_loader_validation="$sample_report/$target/sealed-loader-audit-validation.tsv" + sample_loader_policy_sha="" + sample_loader_audit_sha="" + sample_loader_validation_sha="" + if [ "$require_zero_write_aot" -eq 1 ] && [ "$target" = wasix ]; then + sample_loader_status=1 + if [ -f "$sample_loader_policy" ] && [ ! -L "$sample_loader_policy" ] && + [ -f "$sample_loader_audit" ] && [ ! -L "$sample_loader_audit" ] && + [ -f "$sample_loader_validation" ] && [ ! -L "$sample_loader_validation" ] && + awk -F '\t' -v receipt_path="$immutable_carrier_receipt" \ + -v receipt_sha="$immutable_carrier_receipt_sha256" \ + -v receipt_dev="$immutable_carrier_receipt_dev" \ + -v receipt_ino="$immutable_carrier_receipt_ino" \ + -v carrier_identity="$immutable_carrier_closure_identity" \ + -v core_profile="$frozen_core_profile" \ + -v guest_recipe="$frozen_guest_build_recipe_sha256" \ + 'NR == 2 && $1 == "oliphaunt.wasix-postmaster.sealed-loader-policy.v2" && $3 == 1 && $4 == "campaign-fast" && $5 == "direct-immutable-only" && $6 == "OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1" && $8 == "sanitized-then-explicit" && $9 == "direct-immutable-inode" && $10 == 0 && $11 == 0 && $12 == 0 && $15 == receipt_path && $16 == receipt_sha && $17 == receipt_dev && $18 == receipt_ino && $19 == carrier_identity && $20 == core_profile && $21 == guest_recipe { valid = 1 } END { exit !(NR == 2 && valid) }' "$sample_loader_policy" && + awk -F '\t' 'NR == 2 && $1 == "oliphaunt.wasix-postmaster.sealed-loader-audit-validation.v3" && $2 == "passed" && $6 == 1 && $7 == 1 && $10 == "direct-immutable-inode" { valid = 1 } END { exit !(NR == 2 && valid) }' "$sample_loader_validation" + then + sample_loader_status=0 + sample_loader_policy_sha="$(fresh_wasmer_bin_hash "$sample_loader_policy")" + sample_loader_audit_sha="$(fresh_wasmer_bin_hash "$sample_loader_audit")" + sample_loader_validation_sha="$(fresh_wasmer_bin_hash "$sample_loader_validation")" + fi + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$sample_label" "$target" "$sample_loader_policy" "$sample_loader_policy_sha" \ + "$sample_loader_audit" "$sample_loader_audit_sha" \ + "$sample_loader_validation" "$sample_loader_validation_sha" \ + "$([ "$sample_loader_status" -eq 0 ] && printf passed || printf failed)" \ + >>"$sealed_loader_verification_tsv" + assert_frozen_inputs "$sample_label:after" || exit 1 + assert_frozen_carrier "$sample_label:after" || exit 1 + assert_frozen_immutable_carrier "$sample_label:after" || exit 1 + assert_frozen_native_oracle "$sample_label:after" || exit 1 + + settings_sha="" + if [ -f "$settings" ] && [ ! -L "$settings" ]; then + settings_sha="$(fresh_wasmer_bin_hash "$settings")" + else + overall_status=1 + fi + if [ "$harness_status" -ne 0 ] || [ "$sample_loader_status" -ne 0 ]; then + overall_status=1 + fi + printf '1\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$block" "$order" "$pair" "$position" "$target" "$sample_label" \ + "$harness_status" "$sample_report" "$settings" "$settings_sha" \ + "$frozen_carrier_identity" "$frozen_native_identity" \ + "$frozen_profile_identity" "$plan_identity" >>"$runs_tsv" + + if [ $((position % 2)) -eq 1 ]; then + pair_first_target="$target" + pair_first_settings="$settings" + else + if [ "$pair_first_target" = native ]; then + native_settings="$pair_first_settings" + wasix_settings="$settings" + else + native_settings="$settings" + wasix_settings="$pair_first_settings" + fi + comparison="$qualification_root/profile-comparisons/$(printf 'b%02d-p%d.tsv' "$block" "$pair")" + comparison_status=failed + comparison_sha="" + if [ -f "$native_settings" ] && [ ! -L "$native_settings" ] && + [ -f "$wasix_settings" ] && [ ! -L "$wasix_settings" ]; then + set +e + python3 "$FRESH_ROOT/bin/compare-postgres-settings.py" \ + "$native_settings" "$wasix_settings" "$comparison" + compare_status=$? + set -e + if [ "$compare_status" -eq 0 ] && [ -f "$comparison" ] && [ ! -L "$comparison" ]; then + comparison_status=passed + comparison_sha="$(fresh_wasmer_bin_hash "$comparison")" + fi + fi + if [ "$comparison_status" != passed ]; then overall_status=1; fi + printf '1\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$block" "$order" "$pair" "$native_settings" "$wasix_settings" \ + "$comparison" "$comparison_sha" "$comparison_status" >>"$profile_comparisons_tsv" + fi + done +done + +assert_frozen_inputs final || exit 1 +assert_frozen_carrier final full || exit 1 +assert_frozen_immutable_carrier final || exit 1 +assert_frozen_native_oracle final || exit 1 +chmod 0444 "$immutable_carrier_verification_tsv" \ + "$sealed_loader_verification_tsv" + +wasix_execution_reports=() +while IFS= read -r sample_report; do + [ -n "$sample_report" ] && wasix_execution_reports+=("$sample_report") +done < <(awk -F '\t' 'NR > 1 && $6 == "wasix" { print $9 }' "$runs_tsv" | LC_ALL=C sort -u) +if fresh_freeze_wasix_execution_identity \ + "$wasix_execution_identity_tsv" \ + "$frozen_carrier_identity" "$frozen_carrier_manifest_sha" \ + "$frozen_carrier_receipt_sha" "$frozen_carrier_payload_sha" \ + "$frozen_carrier_headless_sha" "$runtime_footprint" \ + "$FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256" "$durability_profile" \ + "$FRESH_POSTGRES_DURABILITY_SHA256" "$frozen_profile_identity" \ + "${wasix_execution_reports[@]}"; then + wasix_execution_identity_sha256="$FRESH_QUALIFICATION_EXECUTION_IDENTITY_SHA256" + wasix_postgres_module_sha256="$FRESH_QUALIFICATION_POSTGRES_MODULE_SHA256" +else + overall_status=1 +fi + +set +e +python3 "$comparator" \ + --runs "$runs_tsv" \ + --profile-comparisons "$profile_comparisons_tsv" \ + --profile-inputs "$profile_inputs_tsv" \ + --profile-resolution "$profile_resolution_tsv" \ + --native-oracle-manifest "$native_oracle_tsv" \ + --native-install-dir "$NATIVE_INSTALL_DIR" \ + --benchmark-reports-root "$REPORT_DIR/concurrent-query-suite" \ + --benchmark-runs-root "$RUN_DIR/concurrent-query-suite" \ + --expected-blocks "$blocks" \ + --expected-warmup "$warmup" \ + --expected-samples "$samples" \ + --carrier-identity "$frozen_carrier_identity" \ + --native-oracle-identity "$frozen_native_identity" \ + --profile-identity "$frozen_profile_identity" \ + --probe-source-sha256 "$probe_source_sha" \ + --plan-identity "$plan_identity" \ + --max-persistent-p95-ratio "$max_persistent_p95_ratio" \ + --max-persistent-p99-ratio "$max_persistent_p99_ratio" \ + --max-reconnect-p95-ratio "$max_reconnect_p95_ratio" \ + --max-reconnect-p99-ratio "$max_reconnect_p99_ratio" \ + --max-wasix-persistent-p95-ms "$max_wasix_persistent_p95_ms" \ + --max-wasix-persistent-p99-ms "$max_wasix_persistent_p99_ms" \ + --max-wasix-reconnect-p95-ms "$max_wasix_reconnect_p95_ms" \ + --max-wasix-reconnect-p99-ms "$max_wasix_reconnect_p99_ms" \ + --receipt-output "$samples_tsv" \ + --pairs-output "$pairs_tsv" \ + --summary-output "$paired_summary_tsv" \ + --identity-output "$identity_tsv" +comparator_status=$? +set -e +if [ "$comparator_status" -ne 0 ]; then overall_status=1; fi + +classification="$(classify_latency_result "$overall_status" "$blocks" "$warmup" "$samples")" +{ + printf '# WASIX true-libpq latency qualification\n\n' + printf -- '- Status: `%s`\n' "$([ "$overall_status" -eq 0 ] && printf passed || printf failed)" + printf -- '- Classification: `%s`\n' "$classification" + printf -- '- Order: alternating `ABBA` / `BAAB`, one fresh server per position\n' + printf -- '- Resource detail: `off` (dedicated timed lane)\n' + printf -- '- Server cgroup binding: `%s`\n' "$cgroup_binding" + printf -- '- Server cgroup MemoryMax / MemoryHigh / MemorySwapMax: `%s / %s / %s`\n' \ + "${cgroup_memory_max:-unset}" "${cgroup_memory_high:-unset}" \ + "${cgroup_swap_max:-unset}" + printf -- '- Blocks / warmups / samples: `%s / %s / %s`\n' "$blocks" "$warmup" "$samples" + printf -- '- Qualification plan: `%s` (`%s`)\n' "$plan_tsv" "$plan_identity" + printf -- '- Direct immutable activation required: `%s`\n' "$require_zero_write_aot" + printf -- '- Per-sample sealed-loader verification: `%s`\n' \ + "$sealed_loader_verification_tsv" + if [ "$require_zero_write_aot" -eq 1 ]; then + printf -- '- Immutable deployment receipt: `%s` (`%s`, dev `%s`, ino `%s`)\n' \ + "$immutable_carrier_receipt" "$immutable_carrier_receipt_sha256" \ + "$immutable_carrier_receipt_dev" "$immutable_carrier_receipt_ino" + printf -- '- Immutable deployment identity verification: `%s`\n' \ + "$immutable_carrier_verification_tsv" + fi + printf -- '- Raw-sample receipts: `%s`\n' "$samples_tsv" + printf -- '- Pair evidence: `%s`\n' "$pairs_tsv" + printf -- '- Gate summary: `%s`\n' "$paired_summary_tsv" + printf -- '- Exact identities: `%s`\n' "$identity_tsv" +} >"$summary_md" + +if [ "$overall_status" -eq 0 ]; then + finish_result passed "$classification" all-declared-gates-passed + printf 'passed: true-libpq latency qualification (non-promotional); see %s\n' "$summary_md" +else + finish_result failed "$classification" one-or-more-declared-gates-failed + printf 'failed: true-libpq latency qualification; see %s\n' "$summary_md" >&2 + exit 1 +fi diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-libpq-latency.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-libpq-latency.test.sh new file mode 100755 index 00000000..ba66f988 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-libpq-latency.test.sh @@ -0,0 +1,501 @@ +#!/usr/bin/env bash + +set -euo pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TEST_ROOT="$(mktemp -d)" +if [ "${KEEP_TEST_ROOT:-0}" = 1 ]; then + printf 'retaining latency qualifier test root: %s\n' "$TEST_ROOT" >&2 +else + trap 'rm -rf -- "$TEST_ROOT"' EXIT HUP INT TERM +fi +FAKE_PROJECT="$TEST_ROOT/project" +FAKE_REPORTS="$TEST_ROOT/reports" +FAKE_RUNS="$TEST_ROOT/runs" +FAKE_NATIVE="$TEST_ROOT/native" +FAKE_CARRIER="$TEST_ROOT/carrier" +FAKE_IMMUTABLE_RECEIPT="$TEST_ROOT/immutable-carrier.receipt.json" +mkdir -p "$FAKE_PROJECT/bin" "$FAKE_PROJECT/lib" "$FAKE_PROJECT/probes" \ + "$FAKE_PROJECT/profiles" \ + "$FAKE_REPORTS" "$FAKE_RUNS" "$FAKE_NATIVE/bin" "$FAKE_NATIVE/lib" "$FAKE_CARRIER/bin" + +cp "$PROJECT_ROOT/bin/qualify-wasix-libpq-latency.sh" "$FAKE_PROJECT/bin/" +cp "$PROJECT_ROOT/bin/compare-libpq-latency.py" "$FAKE_PROJECT/bin/" +cp "$PROJECT_ROOT/bin/compare-postgres-settings.py" "$FAKE_PROJECT/bin/" +cp "$PROJECT_ROOT/lib/qualification-identities.sh" "$FAKE_PROJECT/lib/" +cp "$PROJECT_ROOT/probes/libpq_latency_probe.c" "$FAKE_PROJECT/probes/" + +cat >"$FAKE_PROJECT/lib/common.sh" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +export FRESH_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export REPORT_DIR="${REPORT_DIR:?}" +export RUN_DIR="${RUN_DIR:?}" +export NATIVE_INSTALL_DIR="${NATIVE_INSTALL_DIR:?}" +fresh_sha256_stream() { sha256sum | awk '{ print $1 }'; } +fresh_wasmer_bin_hash() { sha256sum "$1" | awk '{ print $1 }'; } +EOF + +cat >"$FAKE_PROJECT/bin/verify-immutable-sealed-carrier.sh" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +{ [ "$#" -eq 4 ] || { [ "$#" -eq 5 ] && [ "$5" = --fast ]; }; } && + [ "$1" = --sealed-carrier ] && [ -d "$2" ] && + [ "$3" = --receipt ] && [ -f "$4" ] && [ ! -L "$4" ] +EOF + +cat >"$FAKE_PROJECT/lib/sealed-carrier.sh" <<'EOF' +#!/usr/bin/env bash +fresh_verify_sealed_headless_carrier() { + [ -f "$1/manifest.json" ] && [ -f "$1/wasmer-build.receipt" ] && + [ -f "$1/payload.files" ] && [ -x "$1/bin/wasmer-headless" ] +} +EOF + +cat >"$FAKE_PROJECT/lib/postgres-profiles.sh" <<'EOF' +#!/usr/bin/env bash +fresh_resolve_postgres_profiles() { + [ "$1" = embedded-concurrent ] && [ "$2" = safe ] || return 2 + profile_path="$FRESH_ROOT/profiles/fake-profile" + profile_sha="$(fresh_wasmer_bin_hash "$profile_path")" + FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY="$( + { + printf 'schema\toliphaunt.wasix-postmaster.postgres-profile-resolution.v1\n' + printf 'input\truntime-footprint\tembedded-concurrent\t%s\n' "$profile_sha" + printf 'setting\tshared_buffers\t4096\truntime-footprint\tembedded-concurrent\t%s\t1\n' "$profile_sha" + } | fresh_sha256_stream + )" + FRESH_POSTGRES_RUNTIME_FOOTPRINT_ID="$1" + FRESH_POSTGRES_DURABILITY_ID="$2" + FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256="$profile_sha" + FRESH_POSTGRES_DURABILITY_SHA256="$(fresh_wasmer_bin_hash "$FRESH_ROOT/profiles/fake-durability")" +} +fresh_assert_postgres_profile_inputs() { return 0; } +fresh_write_postgres_profile_evidence() { + profile_path="$FRESH_ROOT/profiles/fake-profile" + profile_sha="$(fresh_wasmer_bin_hash "$profile_path")" + printf 'kind\tid\tpath\tsha256\nruntime-footprint\tembedded-concurrent\t%s\t%s\n' \ + "$profile_path" "$profile_sha" >"$1" + printf 'name\tvalue\tsource\tprofile_id\tprofile_path\tprofile_sha256\tprecedence\nshared_buffers\t4096\truntime-footprint\tembedded-concurrent\t%s\t%s\t1\n' \ + "$profile_path" "$profile_sha" >"$2" + chmod 0444 "$1" "$2" +} +EOF + +cat >"$FAKE_PROJECT/bin/bench-wasix-concurrent-query-suite.sh" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +source "$FRESH_ROOT/lib/common.sh" +source "$FRESH_ROOT/lib/sealed-carrier.sh" +source "$FRESH_ROOT/lib/postgres-profiles.sh" +source "$FRESH_ROOT/lib/qualification-identities.sh" +target="" +label="" +warmup="" +samples="" +resource="" +runtime="" +durability="" +latency_only=0 +sealed=0 +sealed_carrier="" +require_zero_write_aot=0 +immutable_carrier_receipt="" +immutable_verification_scope=full +cgroup_memory_max="" +cgroup_memory_high="" +cgroup_swap_max="" +original=("$@") +while [ "$#" -gt 0 ]; do + case "$1" in + --target|--label|--libpq-latency-warmup|--libpq-latency-samples|--resource-detail|--runtime-footprint|--durability|--cgroup-memory-max|--cgroup-memory-high|--cgroup-swap-max) + option="$1"; shift + case "$option" in + --target) target="$1" ;; + --label) label="$1" ;; + --libpq-latency-warmup) warmup="$1" ;; + --libpq-latency-samples) samples="$1" ;; + --resource-detail) resource="$1" ;; + --runtime-footprint) runtime="$1" ;; + --durability) durability="$1" ;; + --cgroup-memory-max) cgroup_memory_max="$1" ;; + --cgroup-memory-high) cgroup_memory_high="$1" ;; + --cgroup-swap-max) cgroup_swap_max="$1" ;; + esac + ;; + --libpq-latency-only) latency_only=1 ;; + --require-zero-write-aot) require_zero_write_aot=1 ;; + --immutable-carrier-receipt) shift; immutable_carrier_receipt="$1" ;; + --immutable-carrier-verification-scope) shift; immutable_verification_scope="$1" ;; + --sealed-carrier) shift; sealed=1; sealed_carrier="$1" ;; + --skip-build|--discard-pgdata) ;; + --timeout|--start-port|--checkpoint-policy) shift ;; + *) printf 'unexpected fake harness argument: %s\n' "$1" >&2; exit 64 ;; + esac + shift +done +if [ "$require_zero_write_aot" -eq 1 ]; then + [ -f "$immutable_carrier_receipt" ] +fi +[ "$resource" = off ] && [ "$latency_only" -eq 1 ] && + [ "$runtime" = embedded-concurrent ] && [ "$durability" = safe ] +if [ "$target" = native ]; then + [ "$sealed" -eq 0 ] +else + [ "$target" = wasix ] && [ "$sealed" -eq 1 ] +fi +[ "${WASIX_PERF_STATS:-}" = 0 ] && [ "${WASIX_LIBPQ_LATENCY_HOST_FD_ALLOWANCE:-}" = 0 ] +for wait_dump_name in \ + WASIX_PERF_WAIT_DUMP_INTERVAL_MS \ + WASIX_PERF_WAIT_DUMP_FILE \ + WASIX_PERF_WAIT_DUMP_MAX_PER_WAIT \ + WASIX_PERF_WAIT_DUMP_VERBOSE \ + WASIX_WAIT_DUMP_INTERVAL_MS \ + WASIX_WAIT_DUMP_FILE \ + WASIX_WAIT_DUMP_MAX_PER_WAIT \ + WASIX_WAIT_DUMP_VERBOSE \ + WASIX_WAIT_DUMP_FENCE_REQUEST_FILE \ + WASIX_WAIT_DUMP_FENCE_ACK_FILE +do + if [[ -v $wait_dump_name ]]; then + printf 'timed qualifier leaked ambient instrumentation: %s\n' "$wait_dump_name" >&2 + exit 65 + fi +done +[ ! -v OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT ] +[ ! -v OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE ] +[ ! -v WASIX_CGROUP_MEMORY_MAX ] +[ ! -v WASIX_CGROUP_MEMORY_HIGH ] +[ ! -v WASIX_CGROUP_SWAP_MAX ] +if [ -n "${FAKE_EXPECT_CGROUP_BINDING:-}" ]; then + IFS=: read -r expected_max expected_high expected_swap \ + <<<"$FAKE_EXPECT_CGROUP_BINDING" + [ "$cgroup_memory_max" = "$expected_max" ] && + [ "$cgroup_memory_high" = "$expected_high" ] && + [ "$cgroup_swap_max" = "$expected_swap" ] +else + [ -z "$cgroup_memory_max$cgroup_memory_high$cgroup_swap_max" ] +fi +printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$target" "$resource" "$label" \ + "${cgroup_memory_max:-none}" "${cgroup_memory_high:-none}" \ + "${cgroup_swap_max:-none}" >>"${FAKE_HARNESS_CALLS:?}" +plan="$REPORT_DIR/libpq-latency-qualification/${FAKE_QUAL_LABEL:?}/qualification-plan.tsv" +[ -f "$plan" ] && grep -Fq $'\tABBA/BAAB\tembedded-concurrent\tsafe\tcontrolled\toff\t0\tWASIX_PERF_WAIT_DUMP_INTERVAL_MS,WASIX_PERF_WAIT_DUMP_FILE,WASIX_PERF_WAIT_DUMP_MAX_PER_WAIT,WASIX_PERF_WAIT_DUMP_VERBOSE,WASIX_WAIT_DUMP_INTERVAL_MS,WASIX_WAIT_DUMP_FILE,WASIX_WAIT_DUMP_MAX_PER_WAIT,WASIX_WAIT_DUMP_VERBOSE,WASIX_WAIT_DUMP_FENCE_REQUEST_FILE,WASIX_WAIT_DUMP_FENCE_ACK_FILE\t2.0\t2.5\t3.5\t4.5\t0.25\t0.40\t20\t30' "$plan" + +report="$REPORT_DIR/concurrent-query-suite/$label" +target_report="$report/$target" +latency_dir="$target_report/libpq-latency" +mkdir -p "$latency_dir" +fake_carrier_closure=none +if [ -n "$sealed_carrier" ]; then + fresh_capture_qualification_carrier_identity "$sealed_carrier" + fake_carrier_closure="$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" +fi +immutable_receipt_sha="$(fresh_wasmer_bin_hash "$immutable_carrier_receipt" 2>/dev/null || printf none)" +read -r immutable_receipt_dev immutable_receipt_ino < <( + stat -c '%d %i' -- "$immutable_carrier_receipt" 2>/dev/null || printf 'none none\n' +) +printf 'schema_version\truntime_mode\trequire_zero_write_aot\tverification_scope\tactivation_policy\truntime_environment\taudit_environment\tenvironment_inheritance\tallowed_snapshot_modes\tmax_source_bytes_written\tmax_snapshot_bytes_written\tmax_sync_calls\tvalidator\tvalidator_sha256\timmutable_receipt_path\timmutable_receipt_sha256\timmutable_receipt_dev\timmutable_receipt_ino\tcarrier_closure_identity\tcore_profile\tguest_build_recipe_sha256\n' >"$report/sealed-loader-policy.tsv" +printf 'oliphaunt.wasix-postmaster.sealed-loader-policy.v2\tsealed-headless\t%s\t%s\t%s\t%s\t%s\tsanitized-then-explicit\t%s\t0\t0\t0\tfake-validator\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$require_zero_write_aot" "$immutable_verification_scope" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf direct-immutable-only || printf compatibility)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1 || printf unset)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf owned-per-target-jsonl || printf disabled)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf direct-immutable-inode || printf unrestricted)" \ + "$(printf fake-validator | sha256sum | awk '{ print $1 }')" \ + "${immutable_carrier_receipt:-none}" "$immutable_receipt_sha" \ + "$immutable_receipt_dev" "$immutable_receipt_ino" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf '%s' "$fake_carrier_closure" || printf none)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf '%s' "$FRESH_QUALIFICATION_CORE_PROFILE" || printf none)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf '%s' "$FRESH_QUALIFICATION_GUEST_BUILD_RECIPE_SHA256" || printf none)" \ + >>"$report/sealed-loader-policy.tsv" +if [ "$require_zero_write_aot" -eq 1 ]; then + [ "$target" = wasix ] + printf '{"fake":"direct-immutable"}\n' >"$target_report/sealed-loader-audit.jsonl" + loader_validation_status=passed + case "$label" in + loader-gate-failure-*-p2-wasix) loader_validation_status=failed ;; + esac + printf 'schema_version\tstatus\trecords\taot_records\tmemory_records\tinitdb_executions\tpostgres_executions\tinitdb_pids\tpostgres_pids\trequired_snapshot_mode\n%s\t%s\t4\t2\t2\t1\t1\t101\t202\tdirect-immutable-inode\n' \ + oliphaunt.wasix-postmaster.sealed-loader-audit-validation.v3 \ + "$loader_validation_status" \ + >"$target_report/sealed-loader-audit-validation.tsv" +fi +if [ "$target" = wasix ]; then + fresh_resolve_postgres_profiles "$runtime" "$durability" + fresh_capture_qualification_carrier_identity "$sealed_carrier" + module_sha="$(fresh_wasmer_bin_hash "$FRESH_ROOT/fake-postgres.wasm")" + printf 'schema_version\tpostgres_major\truntime_mode\tcarrier_closure_identity\tcarrier_manifest_sha256\tcarrier_receipt_sha256\tcarrier_payload_inventory_sha256\tcarrier_headless_sha256\twasmer_bin_sha256\tpostgres_module_sha256\truntime_footprint\truntime_footprint_sha256\tdurability_profile\tdurability_profile_sha256\tpostgres_profile_resolution_identity\n' >"$report/execution-identity.tsv" + printf 'oliphaunt.wasix-postmaster.execution-identity.v1\t18\tsealed-headless\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" \ + "$FRESH_QUALIFICATION_CARRIER_MANIFEST_SHA256" \ + "$FRESH_QUALIFICATION_CARRIER_RECEIPT_SHA256" \ + "$FRESH_QUALIFICATION_CARRIER_PAYLOAD_SHA256" \ + "$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" \ + "$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" "$module_sha" \ + "$runtime" "$FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256" \ + "$durability" "$FRESH_POSTGRES_DURABILITY_SHA256" \ + "$FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY" \ + >>"$report/execution-identity.tsv" +fi +profile_path="$FRESH_ROOT/profiles/fake-profile" +profile_sha="$(sha256sum "$profile_path" | awk '{ print $1 }')" +printf 'kind\tid\tpath\tsha256\nruntime-footprint\tembedded-concurrent\t%s\t%s\n' \ + "$profile_path" "$profile_sha" >"$report/postgres-profile-inputs.tsv" +printf 'name\tvalue\tsource\tprofile_id\tprofile_path\tprofile_sha256\tprecedence\nshared_buffers\t4096\truntime-footprint\tembedded-concurrent\t%s\t%s\t1\n' \ + "$profile_path" "$profile_sha" >"$report/postgres-profile-resolution.tsv" +settings="$target_report/effective-postgres-settings.tsv" +printf 'name\tsetting\tunit\tsource\n' >"$settings" +for name in autovacuum_worker_slots backend_flush_after bgwriter_flush_after checkpoint_flush_after checkpoint_timeout fsync full_page_writes io_method max_connections max_wal_senders max_worker_processes max_wal_size min_wal_size shared_buffers synchronous_commit wal_segment_size; do + printf '%s\tvalue\t\tcommand line\n' "$name" >>"$settings" +done + +libpq="$NATIVE_INSTALL_DIR/lib/libpq.so.5" +libpq_sha="$(sha256sum "$libpq" | awk '{ print $1 }')" +call_count="$(wc -l <"$FAKE_HARNESS_CALLS" | tr -d '[:space:]')" +probe_path="$RUN_DIR/concurrent-query-suite/$label/libpq-latency-probe" +mkdir -p "$(dirname "$probe_path")" +if [ "${FAKE_HARNESS_MUTATE_PROBE_AT:-0}" = "$call_count" ]; then + printf 'mutated fake probe\n' >"$probe_path" +else + printf 'exact fake probe\n' >"$probe_path" +fi +probe_sha="$(sha256sum "$probe_path" | awk '{ print $1 }')" +printf 'schema_version\ttarget\tmode\tstatus\tclock\twarmup_count\tsample_count\tp50_ns\tp95_ns\tp99_ns\tp50_ms\tp95_ms\tp99_ms\traw_tsv\tlibpq_path\tlibpq_sha256\tprobe_sha256\n' >"$report/libpq-latency-summary.tsv" +for mode in persistent reconnect; do + if [ "$mode" = persistent ]; then + [ "$target" = native ] && value=100000 || value=150000 + else + [ "$target" = native ] && value=5000000 || value=12000000 + fi + raw="$latency_dir/$mode.raw.tsv" + printf 'schema_version\tmode\tphase\tsample_index\tduration_ns\tstatus\n' >"$raw" + for ((sample = 1; sample <= warmup; sample++)); do + printf '1\t%s\twarmup\t%s\t%s\tok\n' "$mode" "$sample" "$value" >>"$raw" + done + for ((sample = 1; sample <= samples; sample++)); do + printf '1\t%s\tmeasure\t%s\t%s\tok\n' "$mode" "$sample" "$value" >>"$raw" + done + milliseconds="$(awk -v value="$value" 'BEGIN { printf "%.6f", value / 1000000 }')" + printf '1\t%s\t%s\tok\tCLOCK_MONOTONIC\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$mode" "$warmup" "$samples" "$value" "$value" "$value" \ + "$milliseconds" "$milliseconds" "$milliseconds" "$raw" "$libpq" \ + "$libpq_sha" "$probe_sha" >>"$report/libpq-latency-summary.tsv" +done +printf 'target\tmode\tbefore_open_fds\tafter_open_fds\tquiescent_open_fds\tquiescent_growth\tallowance\tstatus\n' >"$report/host-fd-churn-summary.tsv" +printf '%s\tpersistent\t10\t10\t10\t0\t0\tpassed\n%s\treconnect\t10\t10\t10\t0\t0\tpassed\n' "$target" "$target" >>"$report/host-fd-churn-summary.tsv" +printf 'target\trequested_soft_nofile\tpre_soft_nofile\tpre_hard_nofile\tactual_soft_nofile\tactual_hard_nofile\tstatus\tlaunch_record\n%s\t1024\t4096\t4096\t1024\t4096\tpassed\t%s/launch.tsv\n' "$target" "$report" >"$report/server-limits.tsv" +printf 'target\tserver_pid\tserver_pgid\tserver_birth_identity\tcgroup_path\tcgroup_identity\torderly_int\tforced\twait_status\tclean_shutdown_marker\tprocess_group_residue\tcgroup_residue\tport_residue\tstatus\treport\n%s\t100\t100\tbirth\t\t\t1\tnone\t0\t1\t0\t0\t0\tpassed\t%s/shutdown.tsv\n' "$target" "$report" >"$report/server-lifecycle.tsv" +printf 'schema_version\tlane\twasix_perf_stats\twait_dump_policy\twait_dump_interval_ms\twait_dump_max_per_wait\twait_dump_verbose\tfence_protocol\tsanitized_environment\noliphaunt.wasix-postmaster.instrumentation.v1\tbenchmark\t0\tprohibited\t0\t0\t0\tnone\tWASIX_PERF_WAIT_DUMP_INTERVAL_MS WASIX_PERF_WAIT_DUMP_FILE WASIX_PERF_WAIT_DUMP_MAX_PER_WAIT WASIX_PERF_WAIT_DUMP_VERBOSE WASIX_WAIT_DUMP_INTERVAL_MS WASIX_WAIT_DUMP_FILE WASIX_WAIT_DUMP_MAX_PER_WAIT WASIX_WAIT_DUMP_VERBOSE WASIX_WAIT_DUMP_FENCE_REQUEST_FILE WASIX_WAIT_DUMP_FENCE_ACK_FILE\n' >"$report/instrumentation-policy.tsv" +case "$label" in + receipt-mutation-*-p2-wasix) + printf 'mutated\n' >>"$immutable_carrier_receipt" + ;; +esac +EOF + +chmod +x "$FAKE_PROJECT/bin/"*.sh "$FAKE_PROJECT/bin/"*.py \ + "$FAKE_PROJECT/lib/"*.sh +printf 'shared_buffers=32MB\n' >"$FAKE_PROJECT/profiles/fake-profile" +printf 'fsync=on\n' >"$FAKE_PROJECT/profiles/fake-durability" +printf 'fake PostgreSQL guest module\n' >"$FAKE_PROJECT/fake-postgres.wasm" +printf '{"core-profile":"release-o3","guest-build-recipe-sha256":"%064d","schema":"fake"}\n' 0 >"$FAKE_CARRIER/manifest.json" +printf 'receipt\n' >"$FAKE_CARRIER/wasmer-build.receipt" +printf 'payload\n' >"$FAKE_CARRIER/payload.files" +printf '#!/usr/bin/env bash\nexit 0\n' >"$FAKE_CARRIER/bin/wasmer-headless" +chmod +x "$FAKE_CARRIER/bin/wasmer-headless" +for binary in postgres initdb psql; do + printf '#!/usr/bin/env bash\nexit 0\n' >"$FAKE_NATIVE/bin/$binary" + chmod +x "$FAKE_NATIVE/bin/$binary" +done +printf 'fake libpq\n' >"$FAKE_NATIVE/lib/libpq.so.5" +printf '{"schema":"fake-immutable"}\n' >"$FAKE_IMMUTABLE_RECEIPT" + +awk ' + /^classify_latency_result\(\) \{$/ { capture = 1 } + capture { print } + capture && /^}$/ { exit } +' "$PROJECT_ROOT/bin/qualify-wasix-libpq-latency.sh" \ + >"$TEST_ROOT/classify-latency-result.sh" +# shellcheck source=/dev/null +source "$TEST_ROOT/classify-latency-result.sh" +[ "$(classify_latency_result 0 10 100 1000)" = \ + latency-qualified-non-promotional ] +[ "$(classify_latency_result 1 10 100 1000)" = \ + failed-latency-qualification-non-promotional ] +[ "$(classify_latency_result 1 2 1 5)" = \ + latency-diagnostic-non-promotional ] + +calls="$TEST_ROOT/odd-block.calls" +: >"$calls" +if REPORT_DIR="$FAKE_REPORTS" RUN_DIR="$FAKE_RUNS" NATIVE_INSTALL_DIR="$FAKE_NATIVE" \ + FAKE_HARNESS_CALLS="$calls" FAKE_QUAL_LABEL=odd-block \ + "$FAKE_PROJECT/bin/qualify-wasix-libpq-latency.sh" \ + --sealed-carrier "$FAKE_CARRIER" --blocks 1 --warmup 1 --samples 5 \ + --label odd-block >/dev/null 2>&1 +then + echo 'odd ABBA/BAAB block count unexpectedly passed' >&2 + exit 1 +fi +[ ! -s "$calls" ] +[ ! -e "$FAKE_REPORTS/libpq-latency-qualification/odd-block" ] + +calls="$TEST_ROOT/success.calls" +: >"$calls" +REPORT_DIR="$FAKE_REPORTS" RUN_DIR="$FAKE_RUNS" NATIVE_INSTALL_DIR="$FAKE_NATIVE" \ +FAKE_HARNESS_CALLS="$calls" FAKE_QUAL_LABEL=success \ +WASIX_PERF_WAIT_DUMP_INTERVAL_MS=999 \ +WASIX_PERF_WAIT_DUMP_FILE=/ambient/perf.log \ +WASIX_PERF_WAIT_DUMP_MAX_PER_WAIT=999 \ +WASIX_PERF_WAIT_DUMP_VERBOSE=1 \ +WASIX_WAIT_DUMP_INTERVAL_MS=999 \ +WASIX_WAIT_DUMP_FILE=/ambient/wait.log \ +WASIX_WAIT_DUMP_MAX_PER_WAIT=999 \ +WASIX_WAIT_DUMP_VERBOSE=1 \ +WASIX_WAIT_DUMP_FENCE_REQUEST_FILE=/ambient/fence.request \ +WASIX_WAIT_DUMP_FENCE_ACK_FILE=/ambient/fence.ack \ +WASIX_CGROUP_MEMORY_MAX=ambient-max \ +WASIX_CGROUP_MEMORY_HIGH=ambient-high \ +WASIX_CGROUP_SWAP_MAX=ambient-swap \ +FAKE_EXPECT_CGROUP_BINDING=256M:224M:0 \ + "$FAKE_PROJECT/bin/qualify-wasix-libpq-latency.sh" \ + --sealed-carrier "$FAKE_CARRIER" --blocks 2 --warmup 1 --samples 5 \ + --cgroup-memory-max 256M --cgroup-memory-high 224M --cgroup-swap-max 0 \ + --label success >/dev/null +success_root="$FAKE_REPORTS/libpq-latency-qualification/success" +[ "$(cut -f1 "$calls" | paste -sd ' ' -)" = \ + 'native wasix wasix native wasix native native wasix' ] +grep -Fq $'oliphaunt.wasix-postmaster.latency-result.v4\tpassed\tlatency-diagnostic-non-promotional\tall-declared-gates-passed' \ + "$success_root/qualification-result.tsv" +[ "$(cut -f4-6 "$calls" | sort -u)" = $'256M\t224M\t0' ] +for receipt in "$success_root/qualification-plan.tsv" \ + "$success_root/qualification-result.tsv"; do + awk -F '\t' ' + NR == 1 { + for (column = 1; column <= NF; column++) column_index[$column] = column + next + } + NR == 2 { + if ($(column_index["cgroup_binding"]) != "dedicated-systemd-user-scope" || + $(column_index["cgroup_memory_max"]) != "256M" || + $(column_index["cgroup_memory_high"]) != "224M" || + $(column_index["cgroup_swap_max"]) != "0" || + $(column_index["cgroup_environment_action"]) != "ambient-sanitized-explicit-argv" || + $(column_index["immutable_verification_scope"]) != "full-per-check") exit 1 + valid = 1 + } + END { exit !valid } + ' "$receipt" +done +grep -Fq 'Server cgroup MemoryMax / MemoryHigh / MemorySwapMax: `256M / 224M / 0`' \ + "$success_root/summary.md" +[ -s "$success_root/wasix-execution-identity.tsv" ] +grep -Fq $'1\tpersistent\tpassed\t4\t4\t4' "$success_root/paired-summary.tsv" +[ "$(wc -l <"$success_root/samples.tsv" | tr -d '[:space:]')" -eq 17 ] +[ "$(wc -l <"$success_root/paired-samples.tsv" | tr -d '[:space:]')" -eq 9 ] + +calls="$TEST_ROOT/zero-write.calls" +: >"$calls" +REPORT_DIR="$FAKE_REPORTS" RUN_DIR="$FAKE_RUNS" NATIVE_INSTALL_DIR="$FAKE_NATIVE" \ +FAKE_HARNESS_CALLS="$calls" FAKE_QUAL_LABEL=zero-write \ +OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=ambient \ +OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE=/ambient/audit \ + "$FAKE_PROJECT/bin/qualify-wasix-libpq-latency.sh" \ + --sealed-carrier "$FAKE_CARRIER" --blocks 2 --warmup 1 --samples 5 \ + --require-zero-write-aot \ + --immutable-carrier-receipt "$FAKE_IMMUTABLE_RECEIPT" \ + --label zero-write >/dev/null +zero_write_root="$FAKE_REPORTS/libpq-latency-qualification/zero-write" +awk -F '\t' -v receipt="$FAKE_IMMUTABLE_RECEIPT" 'NR == 2 { if ($1 != "oliphaunt.wasix-postmaster.libpq-latency-plan.v4" || $22 != 1 || $23 != "direct-immutable-only" || $25 != receipt || $26 !~ /^[0-9a-f]{64}$/ || $27 !~ /^[0-9]+$/ || $28 !~ /^[0-9]+$/) exit 1; valid = 1 } END { exit !valid }' \ + "$zero_write_root/qualification-plan.tsv" +awk -F '\t' 'NR > 1 && $2 == "wasix" { if ($4 !~ /^[0-9a-f]{64}$/ || $6 !~ /^[0-9a-f]{64}$/ || $8 !~ /^[0-9a-f]{64}$/ || $9 != "passed") exit 1; wasix++ } END { exit wasix != 4 }' \ + "$zero_write_root/sealed-loader-verification.tsv" +awk -F '\t' ' + NR == 1 { + if (NF != 8 || $7 != "observed_receipt_ino" || $8 != "status") exit 1 + } + NR > 1 { + if (NF != 8 || $2 != $3 || $4 != $5 || $6 != $7 || $8 != "passed") exit 1 + rows++ + } + END { exit rows == 0 } +' "$zero_write_root/immutable-carrier-verification.tsv" + +calls="$TEST_ROOT/loader-gate-failure.calls" +: >"$calls" +set +e +REPORT_DIR="$FAKE_REPORTS" RUN_DIR="$FAKE_RUNS" NATIVE_INSTALL_DIR="$FAKE_NATIVE" \ +FAKE_HARNESS_CALLS="$calls" FAKE_QUAL_LABEL=loader-gate-failure \ + "$FAKE_PROJECT/bin/qualify-wasix-libpq-latency.sh" \ + --sealed-carrier "$FAKE_CARRIER" --blocks 2 --warmup 1 --samples 5 \ + --require-zero-write-aot \ + --immutable-carrier-receipt "$FAKE_IMMUTABLE_RECEIPT" \ + --label loader-gate-failure >/dev/null 2>&1 +loader_gate_status=$? +set -e +[ "$loader_gate_status" -ne 0 ] +loader_gate_root="$FAKE_REPORTS/libpq-latency-qualification/loader-gate-failure" +grep -Fq $'oliphaunt.wasix-postmaster.latency-result.v4\tfailed\tlatency-diagnostic-non-promotional\tone-or-more-declared-gates-failed' \ + "$loader_gate_root/qualification-result.tsv" +grep -Fq $'persistent\tpassed' "$loader_gate_root/paired-summary.tsv" + +calls="$TEST_ROOT/receipt-mutation.calls" +: >"$calls" +set +e +REPORT_DIR="$FAKE_REPORTS" RUN_DIR="$FAKE_RUNS" NATIVE_INSTALL_DIR="$FAKE_NATIVE" \ +FAKE_HARNESS_CALLS="$calls" FAKE_QUAL_LABEL=receipt-mutation \ + "$FAKE_PROJECT/bin/qualify-wasix-libpq-latency.sh" \ + --sealed-carrier "$FAKE_CARRIER" --blocks 2 --warmup 1 --samples 5 \ + --require-zero-write-aot \ + --immutable-carrier-receipt "$FAKE_IMMUTABLE_RECEIPT" \ + --label receipt-mutation >/dev/null 2>&1 +receipt_mutation_status=$? +set -e +[ "$receipt_mutation_status" -ne 0 ] +receipt_mutation_root="$FAKE_REPORTS/libpq-latency-qualification/receipt-mutation" +awk -F '\t' ' + NR == 1 { + if (NF != 8 || $7 != "observed_receipt_ino" || $8 != "status") exit 1 + } + NR > 1 && $8 == "identity-changed" { + if ($2 == $3 || $4 != $5 || $6 != $7) exit 1 + changed = 1 + } + END { exit !changed } +' "$receipt_mutation_root/immutable-carrier-verification.tsv" +printf '{"schema":"fake-immutable"}\n' >"$FAKE_IMMUTABLE_RECEIPT" + +calls="$TEST_ROOT/probe-failure.calls" +: >"$calls" +if REPORT_DIR="$FAKE_REPORTS" RUN_DIR="$FAKE_RUNS" NATIVE_INSTALL_DIR="$FAKE_NATIVE" \ + FAKE_HARNESS_CALLS="$calls" FAKE_QUAL_LABEL=probe-failure \ + FAKE_HARNESS_MUTATE_PROBE_AT=3 \ + "$FAKE_PROJECT/bin/qualify-wasix-libpq-latency.sh" \ + --sealed-carrier "$FAKE_CARRIER" --blocks 2 --warmup 1 --samples 5 \ + --label probe-failure >/dev/null 2>&1 +then + echo 'probe identity mutation unexpectedly passed' >&2 + exit 1 +fi +failure_root="$FAKE_REPORTS/libpq-latency-qualification/probe-failure" +grep -Fq $'oliphaunt.wasix-postmaster.latency-result.v4\tfailed\tlatency-diagnostic-non-promotional\tone-or-more-declared-gates-failed' \ + "$failure_root/qualification-result.tsv" +[ ! -e "$failure_root/samples.tsv" ] + +calls="$TEST_ROOT/partial-cgroup.calls" +: >"$calls" +if REPORT_DIR="$FAKE_REPORTS" RUN_DIR="$FAKE_RUNS" NATIVE_INSTALL_DIR="$FAKE_NATIVE" \ + FAKE_HARNESS_CALLS="$calls" FAKE_QUAL_LABEL=partial-cgroup \ + WASIX_CGROUP_MEMORY_MAX=256M \ + "$FAKE_PROJECT/bin/qualify-wasix-libpq-latency.sh" \ + --sealed-carrier "$FAKE_CARRIER" --blocks 2 --warmup 1 --samples 5 \ + --label partial-cgroup >"$TEST_ROOT/partial-cgroup.log" 2>&1 +then + echo 'partial cgroup binding unexpectedly passed' >&2 + exit 1 +fi +[ ! -s "$calls" ] +[ ! -e "$FAKE_REPORTS/libpq-latency-qualification/partial-cgroup" ] +grep -q 'must be configured together' "$TEST_ROOT/partial-cgroup.log" + +printf 'true-libpq latency qualifier tests passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-single-backend.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-single-backend.sh new file mode 100755 index 00000000..695ce008 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-single-backend.sh @@ -0,0 +1,2150 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" +source "$FRESH_ROOT/lib/sealed-carrier.sh" +source "$FRESH_ROOT/lib/postgres-profiles.sh" +source "$FRESH_ROOT/lib/qualification-identities.sh" + +usage() { + cat <<'USAGE' +Usage: qualify-wasix-single-backend.sh --sealed-carrier DIR [options] + +Run isolated native/WASIX PostgreSQL 18 comparisons in balanced ABBA/BAAB +order. Every target/workload sample gets a fresh cluster; successful generated +PGDATA is discarded by default, while failed samples remain available. + +Options: + --sealed-carrier DIR Compiler-free carrier to qualify. Required. + --require-zero-write-aot + Require and validate direct immutable AOT and memory + activation; reflink/streamed modes are forbidden. + --immutable-carrier-receipt FILE + Exact external Linux immutable-deployment receipt. + Required with --require-zero-write-aot. + --blocks N Balanced four-position blocks. Default: 5. At least + 10 may qualify this throughput lane statistically, + but never composes a product-promotion claim. + --connections N Concurrent clients. Default: 1. + --iterations N Operations per client. Default: 100000. + --rows N Seed rows. Default: 100000. + --workload NAME Workload to run. May repeat. Defaults to all four. + --workloads LIST Space-separated workload names. + --postgres-guc GUC Extra postmaster name=value setting. May repeat and + is applied identically to native and WASIX samples. + --runtime-footprint ID + Named runtime-footprint profile. Supported: + embedded-concurrent. + --durability ID Named durability profile. Supported: safe. + --shared-memory-provider ID + WASIX host backing mounted at guest /dev/shm. Supported: + portable-file-v1 and Linux-only linux-tmpfs-v1. + Default: portable-file-v1. Native samples receive no + provider option. + --cgroup-memory-max SIZE + Bind every measured postmaster to a dedicated systemd + user scope with MemoryMax=SIZE. + --cgroup-memory-high SIZE + Bind the same scope with MemoryHigh=SIZE. + --cgroup-swap-max SIZE + Bind the same scope with MemorySwapMax=SIZE. All three + cgroup limits must be configured together. Defaults are + inherited from WASIX_CGROUP_MEMORY_MAX, + WASIX_CGROUP_MEMORY_HIGH, and WASIX_CGROUP_SWAP_MAX; + an entirely unset triple disables the dedicated scope. + --adaptive-cache-evidence-policy POLICY + Adaptive cache evidence accepted from each WASIX + sample. Default: `portable-correctness-v1` (exact + active or observe-only fallback). Opt-in + `constrained-linux-wal-action-v1` requires the finite + Linux cgroup triple and proves adaptive-active class-6 + offers, advice calls/bytes, and zero errors. + --timeout SECONDS Per harness operation timeout. Default: 180. + --start-port PORT Reused after each verified shutdown. Default: 55820. + --label NAME Qualification label. Default: timestamped. + --min-ratio R Minimum median paired WASIX/native ratio. Defaults to + 0.70 for c1 and 0.75 for c4. + --min-lcb R Minimum one-sided bootstrap 95% lower bound. Defaults + to 0.65 for c1 and 0.70 for c4. + --max-batch-wall-p95 R + Maximum paired WASIX/native bulk-batch wall-time p95 + ratio. Default: 1.50. + --max-batch-wall-p99 R + Maximum paired WASIX/native bulk-batch wall-time p99 + ratio. Default: 2.00. + --max-batch-residual-delta-p95-ms N + Maximum p95 WASIX-minus-native bulk-batch residual + delta. Default: 15 ms. + --max-batch-residual-delta-p99-ms N + Maximum p99 WASIX-minus-native bulk-batch residual + delta. Default: 25 ms. + --keep-pgdata Retain successful generated PGDATA. + -h, --help Show this help. + +The timed lane disables background resource sampling and uses the controlled +checkpoint policy. Its wall and psql timing fields describe one complete bulk +client batch. The residual is batch wall time minus summed psql-timed statement +time; it is not per-query latency or isolated backend-launch time. Run the +underlying concurrent suite separately with --resource-detail full for +PSS/private/page-table qualification. +This runner is throughput-only and non-promotional: it neither runs nor binds +the independent lifecycle plateau and memory-budget evidence required for an +embedded viability or product-promotion decision. +USAGE +} + +sealed_carrier="" +blocks="${WASIX_QUALIFICATION_BLOCKS:-5}" +connections="${WASIX_QUALIFICATION_CONNECTIONS:-1}" +iterations="${WASIX_QUALIFICATION_ITERATIONS:-100000}" +row_count="${WASIX_QUALIFICATION_ROWS:-100000}" +timeout_seconds="${WASIX_QUALIFICATION_TIMEOUT:-180}" +start_port="${WASIX_QUALIFICATION_PORT:-55820}" +run_label="${WASIX_QUALIFICATION_LABEL:-$(date -u +%Y%m%dT%H%M%SZ)}" +min_ratio="${WASIX_QUALIFICATION_MIN_RATIO:-}" +min_lcb="${WASIX_QUALIFICATION_MIN_LCB:-}" +max_batch_wall_p95="${WASIX_QUALIFICATION_MAX_BATCH_WALL_P95:-1.50}" +max_batch_wall_p99="${WASIX_QUALIFICATION_MAX_BATCH_WALL_P99:-2.00}" +max_batch_residual_delta_p95_ms="${WASIX_QUALIFICATION_MAX_BATCH_RESIDUAL_DELTA_P95_MS:-15}" +max_batch_residual_delta_p99_ms="${WASIX_QUALIFICATION_MAX_BATCH_RESIDUAL_DELTA_P99_MS:-25}" +discard_pgdata=1 +workloads=() +postgres_gucs=() +runtime_footprint="${WASIX_RUNTIME_FOOTPRINT:-}" +durability_profile="${WASIX_DURABILITY_PROFILE:-}" +shared_memory_provider=portable-file-v1 +shared_memory_provider_explicit=0 +require_zero_write_aot=0 +immutable_carrier_receipt="" +cgroup_memory_max="${WASIX_CGROUP_MEMORY_MAX:-}" +cgroup_memory_high="${WASIX_CGROUP_MEMORY_HIGH:-}" +cgroup_swap_max="${WASIX_CGROUP_SWAP_MAX:-}" +adaptive_cache_evidence_policy=portable-correctness-v1 +adaptive_cache_evidence_policy_explicit=0 + +while [ "$#" -gt 0 ]; do + case "$1" in + --sealed-carrier) + shift + [ "$#" -gt 0 ] || { echo "--sealed-carrier requires a directory" >&2; exit 2; } + sealed_carrier="$1" + ;; + --require-zero-write-aot) + [ "$require_zero_write_aot" -eq 0 ] || { + echo '--require-zero-write-aot may only be specified once' >&2 + exit 2 + } + require_zero_write_aot=1 + ;; + --immutable-carrier-receipt) + shift + [ "$#" -gt 0 ] || { echo '--immutable-carrier-receipt requires a file' >&2; exit 2; } + [ -z "$immutable_carrier_receipt" ] || { + echo '--immutable-carrier-receipt may only be specified once' >&2 + exit 2 + } + immutable_carrier_receipt="$1" + ;; + --blocks) + shift + [ "$#" -gt 0 ] || { echo "--blocks requires a value" >&2; exit 2; } + blocks="$1" + ;; + --connections) + shift + [ "$#" -gt 0 ] || { echo "--connections requires a value" >&2; exit 2; } + connections="$1" + ;; + --iterations) + shift + [ "$#" -gt 0 ] || { echo "--iterations requires a value" >&2; exit 2; } + iterations="$1" + ;; + --rows) + shift + [ "$#" -gt 0 ] || { echo "--rows requires a value" >&2; exit 2; } + row_count="$1" + ;; + --workload) + shift + [ "$#" -gt 0 ] || { echo "--workload requires a value" >&2; exit 2; } + workloads+=("$1") + ;; + --workloads) + shift + [ "$#" -gt 0 ] || { echo "--workloads requires a value" >&2; exit 2; } + for workload in $1; do + workloads+=("$workload") + done + ;; + --postgres-guc) + shift + [ "$#" -gt 0 ] || { echo "--postgres-guc requires name=value" >&2; exit 2; } + postgres_gucs+=("$1") + ;; + --runtime-footprint) + shift + [ "$#" -gt 0 ] || { echo "--runtime-footprint requires an ID" >&2; exit 2; } + runtime_footprint="$1" + ;; + --durability) + shift + [ "$#" -gt 0 ] || { echo "--durability requires an ID" >&2; exit 2; } + durability_profile="$1" + ;; + --shared-memory-provider) + shift + [ "$#" -gt 0 ] || { echo "--shared-memory-provider requires an ID" >&2; exit 2; } + [ "$shared_memory_provider_explicit" -eq 0 ] || { + echo '--shared-memory-provider may only be specified once' >&2 + exit 2 + } + shared_memory_provider="$1" + shared_memory_provider_explicit=1 + ;; + --cgroup-memory-max) + shift + [ "$#" -gt 0 ] || { echo "--cgroup-memory-max requires a size" >&2; exit 2; } + cgroup_memory_max="$1" + ;; + --cgroup-memory-high) + shift + [ "$#" -gt 0 ] || { echo "--cgroup-memory-high requires a size" >&2; exit 2; } + cgroup_memory_high="$1" + ;; + --cgroup-swap-max) + shift + [ "$#" -gt 0 ] || { echo "--cgroup-swap-max requires a size" >&2; exit 2; } + cgroup_swap_max="$1" + ;; + --adaptive-cache-evidence-policy) + shift + [ "$#" -gt 0 ] || { + echo '--adaptive-cache-evidence-policy requires a policy ID' >&2 + exit 2 + } + [ "$adaptive_cache_evidence_policy_explicit" -eq 0 ] || { + echo '--adaptive-cache-evidence-policy may only be specified once' >&2 + exit 2 + } + adaptive_cache_evidence_policy="$1" + adaptive_cache_evidence_policy_explicit=1 + ;; + --timeout) + shift + [ "$#" -gt 0 ] || { echo "--timeout requires a value" >&2; exit 2; } + timeout_seconds="$1" + ;; + --start-port) + shift + [ "$#" -gt 0 ] || { echo "--start-port requires a value" >&2; exit 2; } + start_port="$1" + ;; + --label) + shift + [ "$#" -gt 0 ] || { echo "--label requires a value" >&2; exit 2; } + run_label="$1" + ;; + --min-ratio) + shift + [ "$#" -gt 0 ] || { echo "--min-ratio requires a value" >&2; exit 2; } + min_ratio="$1" + ;; + --min-lcb) + shift + [ "$#" -gt 0 ] || { echo "--min-lcb requires a value" >&2; exit 2; } + min_lcb="$1" + ;; + --max-batch-wall-p95) + shift + [ "$#" -gt 0 ] || { echo "--max-batch-wall-p95 requires a value" >&2; exit 2; } + max_batch_wall_p95="$1" + ;; + --max-batch-wall-p99) + shift + [ "$#" -gt 0 ] || { echo "--max-batch-wall-p99 requires a value" >&2; exit 2; } + max_batch_wall_p99="$1" + ;; + --max-batch-residual-delta-p95-ms) + shift + [ "$#" -gt 0 ] || { echo "--max-batch-residual-delta-p95-ms requires a value" >&2; exit 2; } + max_batch_residual_delta_p95_ms="$1" + ;; + --max-batch-residual-delta-p99-ms) + shift + [ "$#" -gt 0 ] || { echo "--max-batch-residual-delta-p99-ms requires a value" >&2; exit 2; } + max_batch_residual_delta_p99_ms="$1" + ;; + --keep-pgdata) + discard_pgdata=0 + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +is_positive_integer() { + [[ "$1" =~ ^[1-9][0-9]*$ ]] +} + +is_ratio() { + [[ "$1" =~ ^([0-9]+([.][0-9]*)?|[.][0-9]+)$ ]] && + awk -v value="$1" 'BEGIN { exit !(value >= 0 && value <= 1) }' +} + +is_nonnegative_number() { + [[ "$1" =~ ^([0-9]+([.][0-9]*)?|[.][0-9]+)$ ]] +} + +validate_cgroup_size() { + [[ "$1" =~ ^[0-9]+([KMGTPE]([i]?B)?)?$ ]] +} +cgroup_size_to_bytes() { + python3 - "$1" <<'PY' +import re +import sys + +match = re.fullmatch(r"([0-9]+)([KMGTPE])?(?:i?B)?", sys.argv[1]) +if match is None: + raise SystemExit(2) +value = int(match.group(1)) +suffix = match.group(2) +if suffix is not None: + value *= 1024 ** ("KMGTPE".index(suffix) + 1) +if value > 2**63 - 1: + raise SystemExit(2) +print(value) +PY +} + +[ -n "$sealed_carrier" ] || { echo "--sealed-carrier is required" >&2; exit 2; } +[ -d "$sealed_carrier" ] || { printf 'missing sealed carrier: %s\n' "$sealed_carrier" >&2; exit 2; } +sealed_carrier="$(cd "$sealed_carrier" && pwd -P)" +if [ "$require_zero_write_aot" -eq 1 ]; then + [ -n "$immutable_carrier_receipt" ] || { + echo '--require-zero-write-aot requires --immutable-carrier-receipt' >&2 + exit 2 + } +elif [ -n "$immutable_carrier_receipt" ]; then + echo '--immutable-carrier-receipt requires --require-zero-write-aot' >&2 + exit 2 +fi +is_positive_integer "$blocks" || { echo "--blocks requires a positive integer" >&2; exit 2; } +is_positive_integer "$connections" || { echo "--connections requires a positive integer" >&2; exit 2; } +is_positive_integer "$iterations" || { echo "--iterations requires a positive integer" >&2; exit 2; } +is_positive_integer "$row_count" || { echo "--rows requires a positive integer" >&2; exit 2; } +is_positive_integer "$timeout_seconds" || { echo "--timeout requires a positive integer" >&2; exit 2; } +if ! is_positive_integer "$start_port" || [ "$start_port" -gt 65535 ]; then + echo "--start-port requires a port number from 1 through 65535" >&2 + exit 2 +fi +case "$run_label" in + ""|*[!A-Za-z0-9._-]*) echo "--label contains unsupported characters" >&2; exit 2 ;; +esac +case "$shared_memory_provider" in + portable-file-v1) ;; + linux-tmpfs-v1) + [ "$(uname -s)" = Linux ] || { + echo 'linux-tmpfs-v1 requires Linux' >&2 + exit 2 + } + ;; + *) + echo '--shared-memory-provider requires portable-file-v1 or linux-tmpfs-v1' >&2 + exit 2 + ;; +esac + +cgroup_limit_count=0 +for cgroup_size in "$cgroup_memory_max" "$cgroup_memory_high" "$cgroup_swap_max"; do + if [ -n "$cgroup_size" ]; then + cgroup_limit_count=$((cgroup_limit_count + 1)) + validate_cgroup_size "$cgroup_size" || { + printf 'invalid cgroup size: %s\n' "$cgroup_size" >&2 + exit 2 + } + fi +done +case "$cgroup_limit_count" in + 0) + cgroup_binding=disabled + cgroup_environment_action=ambient-sanitized-disabled + ;; + 3) + [ "$(uname -s)" = Linux ] || { + echo 'cgroup memory controls require Linux' >&2 + exit 2 + } + command -v systemd-run >/dev/null 2>&1 || { + echo 'cgroup memory controls require systemd-run' >&2 + exit 127 + } + cgroup_binding=dedicated-systemd-user-scope + cgroup_environment_action=ambient-sanitized-explicit-argv + ;; + *) + echo '--cgroup-memory-max, --cgroup-memory-high, and --cgroup-swap-max must be configured together' >&2 + exit 2 + ;; +esac +case "$adaptive_cache_evidence_policy" in + portable-correctness-v1) ;; + constrained-linux-wal-action-v1) + [ "$cgroup_limit_count" -eq 3 ] || { + echo 'constrained-linux-wal-action-v1 requires finite cgroup MemoryMax, MemoryHigh, and MemorySwapMax' >&2 + exit 2 + } + if [[ "$cgroup_memory_max" =~ ^0+([KMGTPE]([i]?B)?)?$ ]] || + [[ "$cgroup_memory_high" =~ ^0+([KMGTPE]([i]?B)?)?$ ]]; then + echo 'constrained-linux-wal-action-v1 requires positive finite MemoryMax and MemoryHigh' >&2 + exit 2 + fi + ;; + *) + echo '--adaptive-cache-evidence-policy requires portable-correctness-v1 or constrained-linux-wal-action-v1' >&2 + exit 2 + ;; +esac +cgroup_memory_max_bytes=none +cgroup_memory_high_bytes=none +cgroup_swap_max_bytes=none +if [ "$cgroup_limit_count" -eq 3 ]; then + cgroup_memory_max_bytes="$(cgroup_size_to_bytes "$cgroup_memory_max")" || exit 2 + cgroup_memory_high_bytes="$(cgroup_size_to_bytes "$cgroup_memory_high")" || exit 2 + cgroup_swap_max_bytes="$(cgroup_size_to_bytes "$cgroup_swap_max")" || exit 2 +fi + +if [ -z "$min_ratio" ]; then + if [ "$connections" -eq 1 ]; then min_ratio="0.70"; else min_ratio="0.75"; fi +fi +if [ -z "$min_lcb" ]; then + if [ "$connections" -eq 1 ]; then min_lcb="0.65"; else min_lcb="0.70"; fi +fi +is_ratio "$min_ratio" || { echo "--min-ratio requires a number from 0 through 1" >&2; exit 2; } +is_ratio "$min_lcb" || { echo "--min-lcb requires a number from 0 through 1" >&2; exit 2; } +is_nonnegative_number "$max_batch_wall_p95" || { echo "--max-batch-wall-p95 requires a nonnegative number" >&2; exit 2; } +is_nonnegative_number "$max_batch_wall_p99" || { echo "--max-batch-wall-p99 requires a nonnegative number" >&2; exit 2; } +is_nonnegative_number "$max_batch_residual_delta_p95_ms" || { echo "--max-batch-residual-delta-p95-ms requires a nonnegative number" >&2; exit 2; } +is_nonnegative_number "$max_batch_residual_delta_p99_ms" || { echo "--max-batch-residual-delta-p99-ms requires a nonnegative number" >&2; exit 2; } +awk -v p95="$max_batch_wall_p95" -v p99="$max_batch_wall_p99" 'BEGIN { exit !(p99 >= p95) }' || { + echo "--max-batch-wall-p99 must be greater than or equal to --max-batch-wall-p95" >&2 + exit 2 +} +awk -v p95="$max_batch_residual_delta_p95_ms" -v p99="$max_batch_residual_delta_p99_ms" 'BEGIN { exit !(p99 >= p95) }' || { + echo "--max-batch-residual-delta-p99-ms must be greater than or equal to --max-batch-residual-delta-p95-ms" >&2 + exit 2 +} + +fresh_postgres_explicit_rows "${postgres_gucs[@]}" >/dev/null || exit + +profile_resolution_active=0 +if [ -n "$runtime_footprint$durability_profile" ]; then + profile_resolution_active=1 + fresh_resolve_postgres_profiles "$runtime_footprint" "$durability_profile" \ + "${postgres_gucs[@]}" || exit + if [ "${#FRESH_POSTGRES_PROFILE_OVERLAPPING_EXPLICIT[@]}" -gt 0 ]; then + printf 'named-profile qualification forbids explicit overrides of: %s\n' \ + "${FRESH_POSTGRES_PROFILE_OVERLAPPING_EXPLICIT[*]}" >&2 + exit 2 + fi +fi +for guc in "${postgres_gucs[@]}"; do + case "$guc" in + *=*) ;; + *) printf -- '--postgres-guc requires name=value, got: %s\n' "$guc" >&2; exit 2 ;; + esac + guc_name="$(printf '%s' "${guc%%=*}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" + case "$guc_name" in + checkpoint_timeout|max_wal_size|min_wal_size|fsync|synchronous_commit|full_page_writes) + printf 'controlled checkpoint policy owns PostgreSQL setting %s; remove the conflicting --postgres-guc\n' \ + "$guc_name" >&2 + exit 2 + ;; + esac +done + +normalize_workload() { + case "$1" in + read|indexed-read|iread) echo indexed-read ;; + mwrite|mixed-write|multi-write) echo mixed-write ;; + iupdate|indexed-update) echo indexed-update ;; + indexed|indexed-insert|iinsert) echo indexed-insert ;; + *) printf 'unknown workload: %s\n' "$1" >&2; return 2 ;; + esac +} + +if [ "${#workloads[@]}" -eq 0 ]; then + workloads=(indexed-read mixed-write indexed-update indexed-insert) +fi +normalized_workloads=() +seen_values="" +for workload in "${workloads[@]}"; do + workload="$(normalize_workload "$workload")" + case " $seen_values " in + *" $workload "*) printf 'duplicate workload: %s\n' "$workload" >&2; exit 2 ;; + esac + seen_values="$seen_values $workload" + normalized_workloads+=("$workload") +done +workloads=("${normalized_workloads[@]}") + +captured_carrier_closure_identity="" +captured_carrier_manifest_sha256="" +captured_carrier_receipt_sha256="" +captured_carrier_payload_sha256="" +captured_carrier_headless_sha256="" +captured_core_profile="" +captured_guest_build_recipe_sha256="" +captured_native_oracle_identity="" +immutable_carrier_receipt_sha256=none +immutable_carrier_receipt_dev=none +immutable_carrier_receipt_ino=none +immutable_carrier_closure_identity=none + +capture_verified_carrier_identity() { + fresh_capture_qualification_carrier_identity "$sealed_carrier" || return + captured_carrier_closure_identity="$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" + captured_carrier_manifest_sha256="$FRESH_QUALIFICATION_CARRIER_MANIFEST_SHA256" + captured_carrier_receipt_sha256="$FRESH_QUALIFICATION_CARRIER_RECEIPT_SHA256" + captured_carrier_payload_sha256="$FRESH_QUALIFICATION_CARRIER_PAYLOAD_SHA256" + captured_carrier_headless_sha256="$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" + captured_core_profile="$FRESH_QUALIFICATION_CORE_PROFILE" + captured_guest_build_recipe_sha256="$FRESH_QUALIFICATION_GUEST_BUILD_RECIPE_SHA256" +} + +frozen_carrier_closure_identity="" +assert_frozen_carrier() { + local stage="$1" + local verification="${2:-fast}" + local observed status=passed + + if [ "$require_zero_write_aot" -eq 1 ] && [ "$verification" = fast ]; then + # The paired immutable assertion performs the receipt/inode/+i syscall + # check. Its root +i proof makes this frozen closure identity constant. + observed="$frozen_carrier_closure_identity" + elif capture_verified_carrier_identity; then + observed="$captured_carrier_closure_identity" + else + observed="" + status=verification-failed + fi + if [ "$status" = passed ] && [ "$observed" != "$frozen_carrier_closure_identity" ]; then + status=identity-changed + fi + printf '%s\t%s\t%s\t%s\n' "$stage" "$frozen_carrier_closure_identity" \ + "$observed" "$status" >>"$carrier_verification_tsv" + if [ "$status" != passed ]; then + printf 'sealed carrier failed frozen-identity check at %s: %s\n' "$stage" "$status" >&2 + return 1 + fi +} + +preflight_immutable_carrier() { + local receipt_parent + + [ "$require_zero_write_aot" -eq 1 ] || return 0 + receipt_parent="$(dirname "$immutable_carrier_receipt")" + [ -d "$receipt_parent" ] && [ ! -L "$receipt_parent" ] || { + printf 'immutable carrier receipt parent must be a non-symlink directory: %s\n' \ + "$receipt_parent" >&2 + return 2 + } + immutable_carrier_receipt="$(cd "$receipt_parent" && pwd -P)/$(basename "$immutable_carrier_receipt")" + "$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier" \ + --receipt "$immutable_carrier_receipt" --fast || return + immutable_carrier_closure_identity="$captured_carrier_closure_identity" + fresh_capture_stable_regular_file_identity "$immutable_carrier_receipt" || return + immutable_carrier_receipt_sha256="$FRESH_QUALIFICATION_REGULAR_FILE_SHA256" + immutable_carrier_receipt_dev="$FRESH_QUALIFICATION_REGULAR_FILE_DEVICE" + immutable_carrier_receipt_ino="$FRESH_QUALIFICATION_REGULAR_FILE_INODE" +} + +assert_frozen_immutable_carrier() { + local stage="$1" observed_sha=none observed_dev=none observed_ino=none + local status=passed + + [ "$require_zero_write_aot" -eq 1 ] || return 0 + if ! "$FRESH_ROOT/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$sealed_carrier" \ + --receipt "$immutable_carrier_receipt" --fast; then + status=unreadable + fi + if fresh_capture_stable_regular_file_identity "$immutable_carrier_receipt"; then + observed_sha="$FRESH_QUALIFICATION_REGULAR_FILE_SHA256" + observed_dev="$FRESH_QUALIFICATION_REGULAR_FILE_DEVICE" + observed_ino="$FRESH_QUALIFICATION_REGULAR_FILE_INODE" + else + status=unreadable + fi + if [ "$status" = passed ] && { + [ "$observed_sha" != "$immutable_carrier_receipt_sha256" ] || + [ "$observed_dev" != "$immutable_carrier_receipt_dev" ] || + [ "$observed_ino" != "$immutable_carrier_receipt_ino" ] || + [ "$frozen_carrier_closure_identity" != "$immutable_carrier_closure_identity" ]; + }; then + status=identity-changed + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$stage" "$immutable_carrier_receipt_sha256" "$observed_sha" \ + "$immutable_carrier_receipt_dev" "$observed_dev" \ + "$immutable_carrier_receipt_ino" "$observed_ino" "$status" \ + >>"$immutable_carrier_verification_tsv" + [ "$status" = passed ] || { + printf 'immutable carrier deployment failed frozen-identity check at %s: %s\n' \ + "$stage" "$status" >&2 + return 1 + } +} + +write_native_oracle_manifest() { + local output="$1" + local relative path bytes digest link_target resolved + local libpq_files=0 + + [ -d "$NATIVE_INSTALL_DIR" ] && [ ! -L "$NATIVE_INSTALL_DIR" ] || { + printf 'native install is missing or is a symlink: %s\n' "$NATIVE_INSTALL_DIR" >&2 + return 1 + } + { + printf 'schema\tkind\tpath\tbytes\tsha256_or_target\n' + for relative in bin/postgres bin/initdb bin/psql; do + path="$NATIVE_INSTALL_DIR/$relative" + [ -f "$path" ] && [ ! -L "$path" ] && [ -x "$path" ] || { + printf 'native oracle requires an executable regular file: %s\n' "$path" >&2 + return 1 + } + bytes="$(wc -c <"$path" | tr -d '[:space:]')" + digest="$(fresh_wasmer_bin_hash "$path")" + printf 'oliphaunt.wasix-postmaster.native-oracle.v1\tfile\t%s\t%s\t%s\n' \ + "$relative" "$bytes" "$digest" + done + while IFS= read -r path; do + [ -n "$path" ] || continue + relative="${path#"$NATIVE_INSTALL_DIR"/}" + case "$relative" in + *$'\t'*|*$'\r'*|*$'\n'*) + printf 'unsafe native libpq path: %s\n' "$relative" >&2 + return 1 + ;; + esac + bytes="$(wc -c <"$path" | tr -d '[:space:]')" + digest="$(fresh_wasmer_bin_hash "$path")" + printf 'oliphaunt.wasix-postmaster.native-oracle.v1\tfile\t%s\t%s\t%s\n' \ + "$relative" "$bytes" "$digest" + libpq_files=$((libpq_files + 1)) + done < <(find "$NATIVE_INSTALL_DIR/lib" -maxdepth 1 -type f \ + \( -name 'libpq.a' -o -name 'libpq.*' \) -print | LC_ALL=C sort) + while IFS= read -r path; do + [ -n "$path" ] || continue + relative="${path#"$NATIVE_INSTALL_DIR"/}" + link_target="$(readlink "$path")" || return + case "$relative:$link_target" in + *$'\t'*|*$'\r'*|*$'\n'*|*:/*|*:*../*) + printf 'unsafe native libpq symlink: %s -> %s\n' "$relative" "$link_target" >&2 + return 1 + ;; + esac + resolved="$(realpath "$path")" || return + case "$resolved" in + "$NATIVE_INSTALL_DIR"/lib/*) ;; + *) printf 'native libpq symlink escapes install root: %s\n' "$path" >&2; return 1 ;; + esac + [ -f "$resolved" ] && [ ! -L "$resolved" ] || { + printf 'native libpq symlink target is not a regular file: %s\n' "$path" >&2 + return 1 + } + printf 'oliphaunt.wasix-postmaster.native-oracle.v1\tsymlink\t%s\t-\t%s\n' \ + "$relative" "$link_target" + done < <(find "$NATIVE_INSTALL_DIR/lib" -maxdepth 1 -type l -name 'libpq*' \ + -print | LC_ALL=C sort) + } >"$output" + [ "$libpq_files" -gt 0 ] || { + printf 'native oracle has no installed regular libpq artifact below %s/lib\n' \ + "$NATIVE_INSTALL_DIR" >&2 + return 1 + } +} + +capture_native_oracle_identity() { + local temporary_root first second + temporary_root="$(mktemp -d)" || return + if ! write_native_oracle_manifest "$temporary_root/first.tsv" || + ! write_native_oracle_manifest "$temporary_root/second.tsv" || + ! cmp -s "$temporary_root/first.tsv" "$temporary_root/second.tsv"; then + printf 'native oracle changed while its verified identity was captured\n' >&2 + rm -rf -- "$temporary_root" + return 1 + fi + first="$(fresh_wasmer_bin_hash "$temporary_root/first.tsv")" || { + rm -rf -- "$temporary_root" + return 1 + } + second="$(fresh_wasmer_bin_hash "$temporary_root/second.tsv")" || { + rm -rf -- "$temporary_root" + return 1 + } + rm -rf -- "$temporary_root" + [ "$first" = "$second" ] || return 1 + captured_native_oracle_identity="$second" +} + +frozen_native_oracle_identity="" +assert_frozen_native_oracle() { + local stage="$1" + local observed status=passed + + if capture_native_oracle_identity; then + observed="$captured_native_oracle_identity" + else + observed="" + status=verification-failed + fi + if [ "$status" = passed ] && [ "$observed" != "$frozen_native_oracle_identity" ]; then + status=identity-changed + fi + printf '%s\t%s\t%s\t%s\n' "$stage" "$frozen_native_oracle_identity" \ + "$observed" "$status" >>"$native_oracle_verification_tsv" + if [ "$status" != passed ]; then + printf 'native oracle failed frozen-identity check at %s: %s\n' "$stage" "$status" >&2 + return 1 + fi +} + +qualification_root="$REPORT_DIR/single-backend-qualification/$run_label" +raw_tsv="$qualification_root/samples.tsv" +paired_tsv="$qualification_root/paired-summary.tsv" +bulk_batch_raw_tsv="$qualification_root/bulk-batch-samples.tsv" +bulk_batch_tsv="$qualification_root/bulk-batch-summary.tsv" +summary="$qualification_root/summary.md" +profile_tsv="$qualification_root/profile-summary.tsv" +carrier_identity_tsv="$qualification_root/carrier-identity.tsv" +carrier_verification_tsv="$qualification_root/carrier-verification.tsv" +native_oracle_identity_tsv="$qualification_root/native-oracle-identity.tsv" +native_oracle_verification_tsv="$qualification_root/native-oracle-verification.tsv" +instrumentation_policy_tsv="$qualification_root/instrumentation-policy.tsv" +instrumentation_verification_tsv="$qualification_root/instrumentation-verification.tsv" +adaptive_cache_verification_tsv="$qualification_root/adaptive-cache-verification.tsv" +sealed_loader_verification_tsv="$qualification_root/sealed-loader-verification.tsv" +immutable_carrier_verification_tsv="$qualification_root/immutable-carrier-verification.tsv" +qualification_policy_tsv="$qualification_root/qualification-policy.tsv" +qualification_result_tsv="$qualification_root/qualification-result.tsv" +wasix_execution_identity_tsv="$qualification_root/wasix-execution-identity.tsv" +wasix_execution_identity_sha256=none +wasix_postgres_module_sha256=none +qualification_postgres_profile_inputs="$qualification_root/postgres-profile-inputs.tsv" +qualification_postgres_profile_resolution="$qualification_root/postgres-profile-resolution.tsv" +harness="$FRESH_ROOT/bin/bench-wasix-concurrent-query-suite.sh" +adaptive_cache_validator="$FRESH_ROOT/bin/validate-adaptive-file-cache-telemetry.py" +adaptive_cache_validator_sha256="$(fresh_wasmer_bin_hash "$adaptive_cache_validator")" +adaptive_cache_sample_contract_schema=oliphaunt.wasix-postmaster.adaptive-cache-sample-contract.v1 +IFS=$'\t' read -r adaptive_cache_validation_schema adaptive_cache_policy_id \ + adaptive_cache_config_id adaptive_cache_config_sha256 \ + adaptive_cache_warmup_samples < <( + python3 - "$adaptive_cache_validator" <<'PY' +import runpy +import sys + +constants = runpy.run_path(sys.argv[1]) +print( + constants["RESULT_SCHEMA"], + constants["POLICY_ID"], + constants["CONFIG_ID"], + constants["CONFIG_SHA256"], + constants["CONFIG"]["warmup-samples"], + sep="\t", +) +PY + ) +case "$adaptive_cache_validation_schema:$adaptive_cache_policy_id:$adaptive_cache_config_id:$adaptive_cache_config_sha256:$adaptive_cache_warmup_samples" in + *$'\t'*|*$'\r'*|*$'\n'*|*::*|*:|*[!A-Za-z0-9._:-]*) + echo 'adaptive cache validator exported an invalid evidence contract' >&2 + exit 125 + ;; +esac +wait_dump_env_unsets=( + -u WASIX_PERF_WAIT_DUMP_INTERVAL_MS + -u WASIX_PERF_WAIT_DUMP_FILE + -u WASIX_PERF_WAIT_DUMP_MAX_PER_WAIT + -u WASIX_PERF_WAIT_DUMP_VERBOSE + -u WASIX_WAIT_DUMP_INTERVAL_MS + -u WASIX_WAIT_DUMP_FILE + -u WASIX_WAIT_DUMP_MAX_PER_WAIT + -u WASIX_WAIT_DUMP_VERBOSE + -u WASIX_WAIT_DUMP_FENCE_REQUEST_FILE + -u WASIX_WAIT_DUMP_FENCE_ACK_FILE + -u OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT + -u OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE + -u WASIX_CGROUP_MEMORY_MAX + -u WASIX_CGROUP_MEMORY_HIGH + -u WASIX_CGROUP_SWAP_MAX +) +if [ -e "$qualification_root" ] || [ -L "$qualification_root" ]; then + printf 'qualification label already exists: %s\n' "$qualification_root" >&2 + exit 2 +fi +capture_verified_carrier_identity || { + printf 'sealed carrier verification failed before qualification: %s\n' "$sealed_carrier" >&2 + exit 1 +} +frozen_carrier_closure_identity="$captured_carrier_closure_identity" +adaptive_cache_runtime_abi_id="$( + python3 - "$sealed_carrier/manifest.json" <<'PY' +import json +import re +import sys +from pathlib import Path + +manifest = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) +runtime_abi_id = manifest.get("runtime-abi-id") +if not isinstance(runtime_abi_id, str) or re.fullmatch(r"[0-9a-f]{64}", runtime_abi_id) is None: + raise SystemExit(2) +print(runtime_abi_id) +PY +)" || { + echo 'sealed carrier manifest has no exact adaptive runtime ABI identity' >&2 + exit 125 +} +preflight_immutable_carrier || { + printf 'immutable carrier deployment verification failed before qualification: %s\n' \ + "$sealed_carrier" >&2 + exit 1 +} +[ "$require_zero_write_aot" -eq 0 ] || + [ "$frozen_carrier_closure_identity" = "$immutable_carrier_closure_identity" ] || { + echo 'immutable deployment closure differs from frozen qualification carrier' >&2 + exit 1 + } +capture_native_oracle_identity || { + printf 'native oracle verification failed before qualification: %s\n' "$NATIVE_INSTALL_DIR" >&2 + exit 1 +} +frozen_native_oracle_identity="$captured_native_oracle_identity" +mkdir -p "$qualification_root/logs" +mkdir -p "$qualification_root/profile-comparisons" \ + "$qualification_root/carrier-provenance" "$qualification_root/effective-settings" \ + "$qualification_root/adaptive-cache-validations" +printf 'schema_version\tscope\tblocks\tconnections\titerations\trows\tworkloads\tmin_ratio\tmin_lcb\tmax_batch_wall_p95\tmax_batch_wall_p99\tmax_batch_residual_delta_p95_ms\tmax_batch_residual_delta_p99_ms\truntime_footprint\truntime_footprint_sha256\tdurability_profile\tdurability_profile_sha256\tpostgres_profile_resolution_identity\tcarrier_closure_identity\trequire_zero_write_aot\tactivation_policy\timmutable_receipt_path\timmutable_receipt_sha256\timmutable_receipt_dev\timmutable_receipt_ino\tcore_profile\tguest_build_recipe_sha256\timmutable_verification_scope\tcgroup_binding\tcgroup_memory_max\tcgroup_memory_high\tcgroup_swap_max\tcgroup_environment_action\tadaptive_cache_evidence_policy\tadaptive_cache_validator_sha256\tadaptive_cache_validation_schema\tadaptive_cache_policy_id\tadaptive_cache_config_id\tadaptive_cache_config_sha256\tadaptive_cache_warmup_samples\tadaptive_cache_runtime_abi_id\tadaptive_cache_sample_contract_schema\twasix_shared_memory_provider\n' \ + >"$qualification_policy_tsv" +printf 'oliphaunt.wasix-postmaster.throughput-policy.v7\tthroughput-only-non-promotional\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$blocks" "$connections" "$iterations" "$row_count" \ + "$(IFS=,; printf '%s' "${workloads[*]}")" "$min_ratio" "$min_lcb" \ + "$max_batch_wall_p95" "$max_batch_wall_p99" \ + "$max_batch_residual_delta_p95_ms" "$max_batch_residual_delta_p99_ms" \ + "${runtime_footprint:-none}" \ + "${FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256:-none}" \ + "${durability_profile:-none}" "${FRESH_POSTGRES_DURABILITY_SHA256:-none}" \ + "${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-none}" \ + "$frozen_carrier_closure_identity" "$require_zero_write_aot" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf direct-immutable-only || printf compatibility)" \ + "${immutable_carrier_receipt:-none}" "$immutable_carrier_receipt_sha256" \ + "$immutable_carrier_receipt_dev" "$immutable_carrier_receipt_ino" \ + "$captured_core_profile" "$captured_guest_build_recipe_sha256" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf campaign-boundary-full-fast-samples || printf full-per-check)" \ + "$cgroup_binding" "${cgroup_memory_max:-none}" \ + "${cgroup_memory_high:-none}" "${cgroup_swap_max:-none}" \ + "$cgroup_environment_action" "$adaptive_cache_evidence_policy" \ + "$adaptive_cache_validator_sha256" \ + "$adaptive_cache_validation_schema" "$adaptive_cache_policy_id" \ + "$adaptive_cache_config_id" "$adaptive_cache_config_sha256" \ + "$adaptive_cache_warmup_samples" "$adaptive_cache_runtime_abi_id" \ + "$adaptive_cache_sample_contract_schema" "$shared_memory_provider" \ + >>"$qualification_policy_tsv" +qualification_policy_identity="$(fresh_wasmer_bin_hash "$qualification_policy_tsv")" +chmod 0444 "$qualification_policy_tsv" +assert_frozen_qualification_policy() { + [ "$(fresh_wasmer_bin_hash "$qualification_policy_tsv")" = \ + "$qualification_policy_identity" ] || { + echo 'pre-run throughput qualification policy changed during execution' >&2 + return 125 + } + [ "$(fresh_wasmer_bin_hash "$adaptive_cache_validator")" = \ + "$adaptive_cache_validator_sha256" ] || { + echo 'adaptive cache evidence validator changed during qualification' >&2 + return 125 + } +} +printf 'schema_version\tlane\twait_dump_policy\twait_dump_interval_ms\twait_dump_max_per_wait\twait_dump_verbose\tfence_protocol\tenvironment_action\tadaptive_cache_evidence_policy\twasix_sample_rule\tnative_sample_rule\n' \ + >"$instrumentation_policy_tsv" +printf 'oliphaunt.wasix-postmaster.qualification-instrumentation.v2\tbulk-throughput\tprohibited\t0\t0\t0\tnone\tunset-before-every-harness-launch\t%s\tselected-policy\tportable-correctness-v1\n' \ + "$adaptive_cache_evidence_policy" \ + >>"$instrumentation_policy_tsv" +chmod 0444 "$instrumentation_policy_tsv" +printf 'sample\treceipt\tsha256\tstatus\n' >"$instrumentation_verification_tsv" +printf 'sample\ttarget\tacceptance_policy\tpolicy_receipt\tpolicy_sha256\tsample_contract_receipt\tsample_contract_sha256\tserver_lifecycle_receipt\tserver_lifecycle_sha256\traw_telemetry\traw_telemetry_sha256\tinner_validation_receipt\tinner_validation_sha256\tqualification_validation_receipt\tqualification_validation_sha256\toutcome\tpolicy_id\tconfig_id\tconfig_sha256\truntime_abi_id\tcgroup_identity\tcgroup_initial_memory_max_bytes\tcgroup_initial_memory_high_bytes\tcgroup_initial_swap_max_bytes\tcgroup_final_memory_max_bytes\tcgroup_final_memory_high_bytes\tcgroup_final_swap_max_bytes\tsample_window_start_monotonic_ns\tsample_window_end_monotonic_ns\tmembership_leaf_identity\tpressure_source_identity\tlast_sample_monotonic_ns\tlast_sample_effective_limit_bytes\tclass6_offers\tclass6_advice_calls\tclass6_advised_bytes\tsample_errors\tclock_errors\tadvice_errors\tstatus\n' \ + >"$adaptive_cache_verification_tsv" +printf 'sample\ttarget\tpolicy_receipt\tpolicy_sha256\taudit_receipt\taudit_sha256\tvalidation_receipt\tvalidation_sha256\tstatus\n' \ + >"$sealed_loader_verification_tsv" +printf 'stage\texpected_receipt_sha256\tobserved_receipt_sha256\texpected_receipt_dev\tobserved_receipt_dev\texpected_receipt_ino\tobserved_receipt_ino\tstatus\n' \ + >"$immutable_carrier_verification_tsv" +if [ "$profile_resolution_active" -eq 1 ]; then + fresh_write_postgres_profile_evidence \ + "$qualification_postgres_profile_inputs" \ + "$qualification_postgres_profile_resolution" +fi +printf 'stage\texpected_closure_identity\tobserved_closure_identity\tstatus\n' >"$carrier_verification_tsv" +printf 'initial\t%s\t%s\tpassed\n' "$frozen_carrier_closure_identity" \ + "$frozen_carrier_closure_identity" >>"$carrier_verification_tsv" +printf 'stage\texpected_native_oracle_identity\tobserved_native_oracle_identity\tstatus\n' \ + >"$native_oracle_verification_tsv" +printf 'initial\t%s\t%s\tpassed\n' "$frozen_native_oracle_identity" \ + "$frozen_native_oracle_identity" >>"$native_oracle_verification_tsv" +assert_frozen_carrier provenance-copy:before || exit 1 +assert_frozen_immutable_carrier provenance-copy:before || exit 1 +assert_frozen_native_oracle provenance-copy:before || exit 1 +cp -p "$sealed_carrier/manifest.json" "$qualification_root/carrier-provenance/manifest.json" +cp -p "$sealed_carrier/wasmer-build.receipt" "$qualification_root/carrier-provenance/wasmer-build.receipt" +cp -p "$sealed_carrier/payload.files" "$qualification_root/carrier-provenance/payload.files" +chmod 0444 "$qualification_root/carrier-provenance/manifest.json" \ + "$qualification_root/carrier-provenance/wasmer-build.receipt" \ + "$qualification_root/carrier-provenance/payload.files" +[ "$(fresh_wasmer_bin_hash "$qualification_root/carrier-provenance/manifest.json")" = "$captured_carrier_manifest_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$qualification_root/carrier-provenance/wasmer-build.receipt")" = "$captured_carrier_receipt_sha256" ] && + [ "$(fresh_wasmer_bin_hash "$qualification_root/carrier-provenance/payload.files")" = "$captured_carrier_payload_sha256" ] || { + echo 'copied carrier provenance does not match the frozen carrier identity' >&2 + exit 1 + } +assert_frozen_carrier provenance-copy:after || exit 1 +assert_frozen_immutable_carrier provenance-copy:after || exit 1 +write_native_oracle_manifest "$native_oracle_identity_tsv" +[ "$(fresh_wasmer_bin_hash "$native_oracle_identity_tsv")" = \ + "$frozen_native_oracle_identity" ] || { + echo 'captured native oracle manifest does not match its frozen identity' >&2 + exit 1 +} +chmod 0444 "$native_oracle_identity_tsv" +assert_frozen_native_oracle provenance-copy:after || exit 1 +printf 'closure_identity\tmanifest_sha256\treceipt_sha256\tpayload_inventory_sha256\theadless_sha256\tcarrier_root\tcore_profile\tguest_build_recipe_sha256\n' >"$carrier_identity_tsv" +printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$frozen_carrier_closure_identity" \ + "$captured_carrier_manifest_sha256" "$captured_carrier_receipt_sha256" \ + "$captured_carrier_payload_sha256" "$captured_carrier_headless_sha256" \ + "$sealed_carrier" "$captured_core_profile" \ + "$captured_guest_build_recipe_sha256" >>"$carrier_identity_tsv" +chmod 0444 "$carrier_identity_tsv" +printf 'block\tworkload\tpair\tfirst_target\tsecond_target\tnative_settings\twasix_settings\tcomparison\tstatus\n' >"$profile_tsv" +printf 'block\tworkload\tpair\tposition\ttarget\trun_label\tworkload_status\tharness_status\teffective_status\tderived_metrics_valid\tthroughput_ops_per_sec\tfanout_wall_ms\toperation_count\treport_dir\teffective_settings\teffective_settings_sha256\tcarrier_closure_identity\tpostgres_profile_resolution_identity\tnative_oracle_identity\n' >"$raw_tsv" +printf 'block\tworkload\tpair\tposition\ttarget\tclient\tclient_status\tworkload_status\tharness_status\teffective_status\tderived_metrics_valid\tbulk_batch_wall_ms\tbulk_batch_psql_time_sum_ms\tbulk_batch_psql_time_count\tbulk_batch_residual_ms\treport_dir\teffective_settings_sha256\tcarrier_closure_identity\tpostgres_profile_resolution_identity\tnative_oracle_identity\n' >"$bulk_batch_raw_tsv" + +overall_status=0 +expected_sample_summary_header=$'target\tworkload\tstatus\tconnections\titerations\toperation_count\tverified_count\texpected_verify_count\tfanout_wall_ms\tthroughput_ops_per_sec\tok_clients\tfailed_clients\ttimed_out\tepoll_intr_count\tserver_log\treport_dir' +expected_client_summary_header=$'target\tworkload\tclient\tstatus\tbulk_batch_wall_ms\tbulk_batch_psql_time_sum_ms\tbulk_batch_psql_time_count\tlog' +for ((block = 1; block <= blocks; block++)); do + if [ $((block % 2)) -eq 1 ]; then + target_order=(native wasix wasix native) + else + target_order=(wasix native native wasix) + fi + workload_count="${#workloads[@]}" + workload_rotation=$(((block - 1) % workload_count)) + for ((workload_offset = 0; workload_offset < workload_count; workload_offset++)); do + workload_index=$(((workload_rotation + workload_offset) % workload_count)) + workload="${workloads[$workload_index]}" + pair_first_target="" + pair_first_settings="" + for position_index in "${!target_order[@]}"; do + position=$((position_index + 1)) + pair=$(((position_index / 2) + 1)) + target="${target_order[$position_index]}" + sample_label="$(printf '%s-b%02d-%s-p%d-%s' "$run_label" "$block" "$workload" "$position" "$target")" + sample_log="$qualification_root/logs/$sample_label.log" + args=( + --skip-build + --target "$target" + --workload "$workload" + --connections "$connections" + --iterations "$iterations" + --rows "$row_count" + --timeout "$timeout_seconds" + --start-port "$start_port" + --checkpoint-policy controlled + --resource-detail off + --label "$sample_label" + ) + sample_adaptive_cache_evidence_policy=portable-correctness-v1 + if [ "$target" = wasix ]; then + sample_adaptive_cache_evidence_policy="$adaptive_cache_evidence_policy" + fi + args+=( + --adaptive-cache-evidence-policy \ + "$sample_adaptive_cache_evidence_policy" + ) + if [ "$cgroup_binding" != disabled ]; then + args+=( + --cgroup-memory-max "$cgroup_memory_max" + --cgroup-memory-high "$cgroup_memory_high" + --cgroup-swap-max "$cgroup_swap_max" + ) + fi + if [ "$discard_pgdata" -eq 1 ]; then + args+=(--discard-pgdata) + fi + if [ "$target" = "wasix" ]; then + args+=( + --sealed-carrier "$sealed_carrier" + --shared-memory-provider "$shared_memory_provider" + ) + if [ "$require_zero_write_aot" -eq 1 ]; then + args+=( + --require-zero-write-aot + --immutable-carrier-receipt "$immutable_carrier_receipt" + --immutable-carrier-verification-scope campaign-fast + ) + fi + fi + for guc in "${postgres_gucs[@]}"; do + args+=(--postgres-guc "$guc") + done + if [ -n "$runtime_footprint" ]; then + args+=(--runtime-footprint "$runtime_footprint") + fi + if [ -n "$durability_profile" ]; then + args+=(--durability "$durability_profile") + fi + printf 'block=%s workload=%s position=%s target=%s\n' \ + "$block" "$workload" "$position" "$target" + assert_frozen_qualification_policy || exit 1 + assert_frozen_carrier "$sample_label:before" || exit 1 + assert_frozen_immutable_carrier "$sample_label:before" || exit 1 + assert_frozen_native_oracle "$sample_label:before" || exit 1 + if [ "$profile_resolution_active" -eq 1 ]; then + fresh_assert_postgres_profile_inputs || exit 1 + fi + set +e + env "${wait_dump_env_unsets[@]}" WASIX_PERF_STATS=0 \ + "$harness" "${args[@]}" >"$sample_log" 2>&1 + harness_status=$? + set -e + assert_frozen_carrier "$sample_label:after" || exit 1 + assert_frozen_immutable_carrier "$sample_label:after" || exit 1 + assert_frozen_native_oracle "$sample_label:after" || exit 1 + if [ "$profile_resolution_active" -eq 1 ]; then + fresh_assert_postgres_profile_inputs || exit 1 + fi + sample_report="$REPORT_DIR/concurrent-query-suite/$sample_label" + sample_summary="$sample_report/summary.tsv" + sample_clients="$sample_report/client-summary.tsv" + sample_instrumentation="$sample_report/instrumentation-policy.tsv" + sample_adaptive_policy="$sample_report/adaptive-cache-evidence-policy.tsv" + sample_adaptive_contract="$sample_report/$target/adaptive-cache-sample-contract.tsv" + sample_server_lifecycle="$sample_report/server-lifecycle.tsv" + sample_adaptive_telemetry="$sample_report/$target/cache-offers-postgres.adaptive.json" + sample_adaptive_inner_validation="$sample_report/$target/cache-offers-postgres-adaptive-validation.tsv" + sample_adaptive_validation="$qualification_root/adaptive-cache-validations/$sample_label.tsv" + sample_loader_policy="$sample_report/sealed-loader-policy.tsv" + sample_loader_audit="$sample_report/$target/sealed-loader-audit.jsonl" + sample_loader_validation="$sample_report/$target/sealed-loader-audit-validation.tsv" + sample_profile_inputs="$sample_report/postgres-profile-inputs.tsv" + sample_profile_resolution="$sample_report/postgres-profile-resolution.tsv" + sample_profile_evidence_status=0 + sample_instrumentation_status=1 + sample_instrumentation_sha256="" + sample_adaptive_status=1 + sample_adaptive_policy_sha256="" + sample_adaptive_contract_sha256="" + sample_server_lifecycle_sha256="" + sample_adaptive_telemetry_sha256="" + sample_adaptive_inner_validation_sha256="" + sample_adaptive_validation_sha256="" + sample_adaptive_outcome=not-applicable + sample_adaptive_policy_id=none + sample_adaptive_config_id=none + sample_adaptive_config_sha256=none + sample_adaptive_runtime_abi_id=none + sample_adaptive_server_pid=none + sample_adaptive_server_birth_identity=none + sample_adaptive_cgroup_unit=none + sample_adaptive_cgroup_identity=none + sample_adaptive_memory_max_bytes=none + sample_adaptive_memory_high_bytes=none + sample_adaptive_effective_limit_bytes=none + sample_adaptive_swap_max_bytes=none + sample_adaptive_final_memory_max_bytes=none + sample_adaptive_final_memory_high_bytes=none + sample_adaptive_final_swap_max_bytes=none + sample_adaptive_window_start_ns=none + sample_adaptive_window_end_ns=none + sample_adaptive_membership_identity=none + sample_adaptive_pressure_identity=none + sample_adaptive_last_sample_ns=none + sample_adaptive_last_sample_limit_bytes=none + sample_adaptive_class6_offers=0 + sample_adaptive_class6_advice_calls=0 + sample_adaptive_class6_advised_bytes=0 + sample_adaptive_sample_errors=0 + sample_adaptive_clock_errors=0 + sample_adaptive_advice_errors=0 + sample_loader_status=0 + sample_loader_policy_sha256="" + sample_loader_audit_sha256="" + sample_loader_validation_sha256="" + if [ "$require_zero_write_aot" -eq 1 ] && [ "$target" = wasix ]; then + sample_loader_status=1 + if [ -f "$sample_loader_policy" ] && [ ! -L "$sample_loader_policy" ] && + [ -f "$sample_loader_audit" ] && [ ! -L "$sample_loader_audit" ] && + [ -f "$sample_loader_validation" ] && [ ! -L "$sample_loader_validation" ] && + awk -F '\t' ' + NR == 1 { next } + NR == 2 && $1 == "oliphaunt.wasix-postmaster.sealed-loader-policy.v2" && + $3 == 1 && $4 == "campaign-fast" && + $5 == "direct-immutable-only" && + $6 == "OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1" && + $8 == "sanitized-then-explicit" && + $9 == "direct-immutable-inode" && + $10 == 0 && $11 == 0 && $12 == 0 && + $15 == receipt_path && $16 == receipt_sha && + $17 == receipt_dev && $18 == receipt_ino && + $19 == carrier_identity && $20 == core_profile && + $21 == guest_recipe { + valid = 1 + } + END { exit !(NR == 2 && valid) } + ' receipt_path="$immutable_carrier_receipt" \ + receipt_sha="$immutable_carrier_receipt_sha256" \ + receipt_dev="$immutable_carrier_receipt_dev" \ + receipt_ino="$immutable_carrier_receipt_ino" \ + carrier_identity="$immutable_carrier_closure_identity" \ + core_profile="$captured_core_profile" \ + guest_recipe="$captured_guest_build_recipe_sha256" \ + "$sample_loader_policy" && + awk -F '\t' ' + NR == 1 { next } + NR == 2 && $1 == "oliphaunt.wasix-postmaster.sealed-loader-audit-validation.v3" && + $2 == "passed" && $6 == 1 && $7 == 1 && + $10 == "direct-immutable-inode" { valid = 1 } + END { exit !(NR == 2 && valid) } + ' "$sample_loader_validation" + then + sample_loader_status=0 + sample_loader_policy_sha256="$(fresh_wasmer_bin_hash "$sample_loader_policy")" + sample_loader_audit_sha256="$(fresh_wasmer_bin_hash "$sample_loader_audit")" + sample_loader_validation_sha256="$(fresh_wasmer_bin_hash "$sample_loader_validation")" + fi + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$sample_label" "$target" "$sample_loader_policy" \ + "$sample_loader_policy_sha256" "$sample_loader_audit" \ + "$sample_loader_audit_sha256" "$sample_loader_validation" \ + "$sample_loader_validation_sha256" \ + "$([ "$sample_loader_status" -eq 0 ] && printf passed || printf failed)" \ + >>"$sealed_loader_verification_tsv" + + sample_adaptive_required_host=any + sample_adaptive_expected_claim_scope=portable-correctness + sample_adaptive_required_runtime=any + sample_adaptive_expected_runtime=compiler + sample_adaptive_expected_manifest=none + sample_adaptive_expected_outcome=adaptive-active-or-observe-only-fallback + sample_adaptive_expected_class=none + sample_adaptive_min_offers=0 + sample_adaptive_min_calls=0 + sample_adaptive_min_bytes=0 + sample_adaptive_max_sample_errors=unbounded + sample_adaptive_max_clock_errors=unbounded + sample_adaptive_max_advice_errors=unbounded + sample_adaptive_max_psi_breaker_trips=unbounded + sample_adaptive_max_refault_breaker_trips=unbounded + sample_adaptive_max_deferred_wal_pin_errors=unbounded + sample_adaptive_max_contended_wal_pin_failures=unbounded + sample_adaptive_terminal_receipt=active-finalized-or-admission-fallback + sample_adaptive_scope_contract=not-required + sample_adaptive_required_cgroup_binding=none + sample_adaptive_required_limit_binding=none + sample_adaptive_required_monotonic_window=none + if [ "$target" = wasix ]; then + sample_adaptive_expected_runtime=sealed-headless + sample_adaptive_expected_manifest="$captured_carrier_manifest_sha256" + fi + if [ "$sample_adaptive_cache_evidence_policy" = \ + constrained-linux-wal-action-v1 ]; then + sample_adaptive_required_host=Linux + sample_adaptive_expected_claim_scope=constrained-linux-performance + sample_adaptive_required_runtime=sealed-headless + sample_adaptive_expected_outcome=adaptive-active + sample_adaptive_expected_class=6 + sample_adaptive_min_offers=1 + sample_adaptive_min_calls=1 + sample_adaptive_min_bytes=1 + sample_adaptive_max_sample_errors=0 + sample_adaptive_max_clock_errors=0 + sample_adaptive_max_advice_errors=0 + sample_adaptive_max_psi_breaker_trips=0 + sample_adaptive_max_refault_breaker_trips=0 + sample_adaptive_max_deferred_wal_pin_errors=0 + sample_adaptive_max_contended_wal_pin_failures=0 + sample_adaptive_terminal_receipt=active-finalized + sample_adaptive_scope_contract=required + sample_adaptive_required_cgroup_binding=per-target-device-inode + sample_adaptive_required_limit_binding=requested-equals-leaf-and-effective-min + sample_adaptive_required_monotonic_window=launch-before-through-post-shutdown + fi + if [ -f "$sample_adaptive_policy" ] && + [ ! -L "$sample_adaptive_policy" ] && + awk -F '\t' \ + -v policy="$sample_adaptive_cache_evidence_policy" \ + -v claim_scope="$sample_adaptive_expected_claim_scope" \ + -v required_host="$sample_adaptive_required_host" \ + -v selected_host="$(uname -s)" \ + -v required_runtime="$sample_adaptive_required_runtime" \ + -v selected_runtime="$sample_adaptive_expected_runtime" \ + -v memory_max="${cgroup_memory_max:-none}" \ + -v memory_high="${cgroup_memory_high:-none}" \ + -v swap_max="${cgroup_swap_max:-none}" \ + -v required_outcome="$sample_adaptive_expected_outcome" \ + -v required_class="$sample_adaptive_expected_class" \ + -v min_offers="$sample_adaptive_min_offers" \ + -v min_calls="$sample_adaptive_min_calls" \ + -v min_bytes="$sample_adaptive_min_bytes" \ + -v max_sample_errors="$sample_adaptive_max_sample_errors" \ + -v max_clock_errors="$sample_adaptive_max_clock_errors" \ + -v max_advice_errors="$sample_adaptive_max_advice_errors" \ + -v max_psi_breakers="$sample_adaptive_max_psi_breaker_trips" \ + -v max_refault_breakers="$sample_adaptive_max_refault_breaker_trips" \ + -v max_deferred_pin_errors="$sample_adaptive_max_deferred_wal_pin_errors" \ + -v max_contended_pin_failures="$sample_adaptive_max_contended_wal_pin_failures" \ + -v terminal_receipt="$sample_adaptive_terminal_receipt" \ + -v validator_sha="$adaptive_cache_validator_sha256" \ + -v manifest_sha="$sample_adaptive_expected_manifest" \ + -v scope_contract="$sample_adaptive_scope_contract" \ + -v cgroup_binding="$sample_adaptive_required_cgroup_binding" \ + -v limit_binding="$sample_adaptive_required_limit_binding" \ + -v monotonic_window="$sample_adaptive_required_monotonic_window" ' + NR == 1 { + expected = "schema_version acceptance_policy claim_scope required_host selected_host required_runtime_mode selected_runtime_mode selected_memory_max selected_memory_high selected_swap_max required_outcome required_class min_class_offers min_class_advice_calls min_class_advised_bytes max_sample_errors max_clock_errors max_advice_errors max_psi_breaker_trips max_refault_breaker_trips max_deferred_wal_pin_errors max_contended_wal_pin_failures terminal_receipt validator_sha256 sealed_manifest_sha256 sample_scope_contract required_cgroup_binding required_limit_binding required_monotonic_window" + split(expected, names, " ") + if (NF != 29) exit 1 + for (field_index = 1; field_index <= NF; field_index++) { + if ($field_index != names[field_index]) exit 1 + } + next + } + NR == 2 { + if (NF != 29 || + $1 != "oliphaunt.wasix-postmaster.adaptive-cache-evidence-policy.v3" || + $2 != policy || $3 != claim_scope || + $4 != required_host || $5 != selected_host || + $6 != required_runtime || $7 != selected_runtime || + $8 != memory_max || $9 != memory_high || $10 != swap_max || + $11 != required_outcome || $12 != required_class || + $13 != min_offers || $14 != min_calls || $15 != min_bytes || + $16 != max_sample_errors || $17 != max_clock_errors || + $18 != max_advice_errors || $19 != max_psi_breakers || + $20 != max_refault_breakers || $21 != max_deferred_pin_errors || + $22 != max_contended_pin_failures || $23 != terminal_receipt || + $24 != validator_sha || $25 != manifest_sha || + $26 != scope_contract || $27 != cgroup_binding || + $28 != limit_binding || $29 != monotonic_window) exit 1 + valid = 1 + } + END { exit !(NR == 2 && valid) } + ' "$sample_adaptive_policy" + then + sample_adaptive_policy_sha256="$( + fresh_wasmer_bin_hash "$sample_adaptive_policy" + )" + if [ "$target" = native ]; then + [ ! -e "$sample_adaptive_contract" ] && + [ ! -e "$sample_adaptive_telemetry" ] && + [ ! -e "$sample_adaptive_inner_validation" ] && + [ ! -e "$sample_adaptive_validation" ] && + sample_adaptive_status=0 + elif [ -f "$sample_adaptive_contract" ] && + [ ! -L "$sample_adaptive_contract" ] && + [ -f "$sample_server_lifecycle" ] && + [ ! -L "$sample_server_lifecycle" ] && + [ -f "$sample_adaptive_telemetry" ] && + [ ! -L "$sample_adaptive_telemetry" ] && + [ -f "$sample_adaptive_inner_validation" ] && + [ ! -L "$sample_adaptive_inner_validation" ] + then + sample_adaptive_contract_result="$( + awk -F '\t' -v OFS='\t' \ + -v sample_label="$sample_label" -v target="$target" \ + -v acceptance="$sample_adaptive_cache_evidence_policy" \ + -v base_policy_sha="$sample_adaptive_policy_sha256" \ + -v validator_sha="$adaptive_cache_validator_sha256" \ + -v manifest_sha="$captured_carrier_manifest_sha256" \ + -v requested_max="$cgroup_memory_max" \ + -v requested_high="$cgroup_memory_high" \ + -v requested_swap="$cgroup_swap_max" \ + -v requested_max_bytes="$cgroup_memory_max_bytes" \ + -v requested_high_bytes="$cgroup_memory_high_bytes" \ + -v requested_swap_bytes="$cgroup_swap_max_bytes" ' + NR == 1 { + expected = "schema_version measurement_id target acceptance_policy contract_mode base_policy_sha256 validator_sha256 manifest_sha256 cgroup_path cgroup_identity server_pid server_birth_identity cgroup_unit requested_memory_max requested_memory_high requested_swap_max requested_memory_max_bytes requested_memory_high_bytes requested_swap_max_bytes observed_initial_memory_max_bytes observed_initial_memory_high_bytes observed_initial_swap_max_bytes observed_final_memory_max_bytes observed_final_memory_high_bytes observed_final_swap_max_bytes sample_window_start_monotonic_ns sample_window_end_monotonic_ns status" + split(expected, names, " ") + if (NF != 28) exit 1 + for (field_index = 1; field_index <= NF; field_index++) { + if ($field_index != names[field_index]) exit 1 + } + next + } + NR == 2 { + if (NF != 28 || + $1 != "oliphaunt.wasix-postmaster.adaptive-cache-sample-contract.v1" || + $2 != sample_label || $3 != target || $4 != acceptance || + $6 != base_policy_sha || $7 != validator_sha || + $8 != manifest_sha || $28 != "passed" || + $11 !~ /^[1-9][0-9]*$/ || $12 == "" || + $12 ~ /[\t\r\n]/) exit 1 + if (acceptance == "constrained-linux-wal-action-v1") { + if ($5 != "constrained-exact-cgroup-time" || + $9 !~ /^\// || + $10 !~ /^[1-9][0-9]*:[1-9][0-9]*$/ || + $13 !~ /^[A-Za-z0-9_.-]+$/ || + substr($9, length($9) - length($13) - 6) != "/" $13 ".scope" || + $14 != requested_max || $15 != requested_high || + $16 != requested_swap || $17 != requested_max_bytes || + $18 != requested_high_bytes || $19 != requested_swap_bytes || + $20 != $17 || $21 != $18 || $22 != $19 || + $23 != $17 || $24 != $18 || $25 != $19 || + $26 !~ /^[0-9]+$/ || $27 !~ /^[0-9]+$/ || + ($26 + 0) >= ($27 + 0)) exit 1 + } else { + if ($5 != "portable-not-required" || + $9 != "none" || $10 != "none" || + $14 != (requested_max == "" ? "none" : requested_max) || + $15 != (requested_high == "" ? "none" : requested_high) || + $16 != (requested_swap == "" ? "none" : requested_swap) || + $17 != "none" || $18 != "none" || $19 != "none" || + $20 != "none" || $21 != "none" || $22 != "none" || + $23 != "none" || $24 != "none" || $25 != "none" || + $26 != "none" || $27 != "none") exit 1 + } + print $9, $10, $11, $12, $13, $17, $18, $19, $20, $21, + $22, $23, $24, $25, $26, $27 + valid = 1 + } + END { exit !(NR == 2 && valid) } + ' "$sample_adaptive_contract" + )" || sample_adaptive_contract_result="" + if [ -n "$sample_adaptive_contract_result" ]; then + IFS=$'\t' read -r sample_adaptive_cgroup_path \ + sample_adaptive_cgroup_identity sample_adaptive_server_pid \ + sample_adaptive_server_birth_identity sample_adaptive_cgroup_unit \ + sample_requested_memory_max_bytes \ + sample_requested_memory_high_bytes sample_requested_swap_max_bytes \ + sample_adaptive_memory_max_bytes sample_adaptive_memory_high_bytes \ + sample_adaptive_swap_max_bytes sample_adaptive_final_memory_max_bytes \ + sample_adaptive_final_memory_high_bytes \ + sample_adaptive_final_swap_max_bytes sample_adaptive_window_start_ns \ + sample_adaptive_window_end_ns <<<"$sample_adaptive_contract_result" + if [ "$sample_adaptive_cache_evidence_policy" = \ + constrained-linux-wal-action-v1 ]; then + if [ "$sample_adaptive_memory_max_bytes" -le \ + "$sample_adaptive_memory_high_bytes" ]; then + sample_adaptive_effective_limit_bytes="$sample_adaptive_memory_max_bytes" + else + sample_adaptive_effective_limit_bytes="$sample_adaptive_memory_high_bytes" + fi + fi + sample_adaptive_contract_sha256="$( + fresh_wasmer_bin_hash "$sample_adaptive_contract" + )" + sample_server_lifecycle_sha256="$( + fresh_wasmer_bin_hash "$sample_server_lifecycle" + )" + sample_adaptive_telemetry_sha256="$( + fresh_wasmer_bin_hash "$sample_adaptive_telemetry" + )" + sample_adaptive_inner_validation_sha256="$( + fresh_wasmer_bin_hash "$sample_adaptive_inner_validation" + )" + lifecycle_bound=0 + if [ "$sample_adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ]; then + if awk -F '\t' \ + -v target="$target" -v path="$sample_adaptive_cgroup_path" \ + -v identity="$sample_adaptive_cgroup_identity" \ + -v server_pid="$sample_adaptive_server_pid" \ + -v birth_identity="$sample_adaptive_server_birth_identity" ' + NR == 1 { + expected = "target server_pid server_pgid server_birth_identity cgroup_path cgroup_identity orderly_int forced wait_status clean_shutdown_marker process_group_residue cgroup_residue port_residue status report" + split(expected, names, " ") + if (NF != 15) exit 1 + for (field_index = 1; field_index <= NF; field_index++) { + if ($field_index != names[field_index]) exit 1 + } + next + } + NR == 2 && NF == 15 && $1 == target && $2 == server_pid && + $4 == birth_identity && $5 == path && $6 == identity && + $14 == "passed" { valid = 1 } + END { exit !(NR == 2 && valid) } + ' "$sample_server_lifecycle"; then + lifecycle_bound=1 + fi + else + lifecycle_bound=1 + fi + inner_validation_bound=0 + if awk -F '\t' \ + -v validation_schema="$adaptive_cache_validation_schema" \ + -v acceptance="$sample_adaptive_cache_evidence_policy" \ + -v policy_id="$adaptive_cache_policy_id" \ + -v config_id="$adaptive_cache_config_id" \ + -v config_sha="$adaptive_cache_config_sha256" \ + -v runtime_abi="$adaptive_cache_runtime_abi_id" \ + -v telemetry_sha="$sample_adaptive_telemetry_sha256" \ + -v manifest_sha="$captured_carrier_manifest_sha256" \ + -v validator_sha="$adaptive_cache_validator_sha256" \ + -v cgroup_identity="$sample_adaptive_cgroup_identity" \ + -v memory_max="$sample_adaptive_memory_max_bytes" \ + -v memory_high="$sample_adaptive_memory_high_bytes" \ + -v effective_limit="$sample_adaptive_effective_limit_bytes" \ + -v swap_max="$sample_adaptive_swap_max_bytes" \ + -v window_start="$sample_adaptive_window_start_ns" \ + -v window_end="$sample_adaptive_window_end_ns" \ + -v sample_label="$sample_label" -v target="$target" ' + NR == 1 { + expected = "schema_version status outcome reason workload_id policy_id config_id config_sha256 acceptance_policy runtime_abi_id state sample_count valid_offers advice_calls advised_bytes class6_offers class6_advice_calls class6_advised_bytes class6_advice_errors sample_errors clock_errors advice_errors wal_dirty_veto_bypasses wal_dirty_veto_bypass_bytes telemetry_sha256 manifest_sha256 validator_sha256 cgroup_identity cgroup_memory_max_bytes cgroup_memory_high_bytes cgroup_swap_max_bytes sample_window_start_monotonic_ns sample_window_end_monotonic_ns membership_leaf_identity pressure_source_identity last_sample_monotonic_ns last_sample_effective_limit_bytes measurement_id target" + split(expected, names, " ") + if (NF != 39) exit 1 + for (field_index = 1; field_index <= NF; field_index++) { + if ($field_index != names[field_index]) exit 1 + } + next + } + NR == 2 { + if (NF != 39 || $1 != validation_schema || $2 != "passed" || + $5 != "runtime:postgres" || $6 != policy_id || + $7 != config_id || $8 != config_sha || $9 != acceptance || + $10 != runtime_abi || $25 != telemetry_sha || + $26 != manifest_sha || $27 != validator_sha || + $38 != sample_label || $39 != target) exit 1 + if (acceptance == "constrained-linux-wal-action-v1" && + ($28 != cgroup_identity || $29 != memory_max || + $30 != memory_high || $31 != swap_max || + $32 != window_start || $33 != window_end || + $34 != cgroup_identity || $35 != cgroup_identity || + $37 != effective_limit || + $38 != sample_label || $39 != target)) exit 1 + valid = 1 + } + END { exit !(NR == 2 && valid) } + ' "$sample_adaptive_inner_validation"; then + inner_validation_bound=1 + fi + adaptive_validator_args=( + --telemetry "$sample_adaptive_telemetry" + --manifest "$sealed_carrier/manifest.json" + --output "$sample_adaptive_validation" + --acceptance-policy "$sample_adaptive_cache_evidence_policy" + --measurement-id "$sample_label" + --target "$target" + ) + if [ "$sample_adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ]; then + adaptive_validator_args+=( + --cgroup-identity "$sample_adaptive_cgroup_identity" + --cgroup-memory-max-bytes "$sample_adaptive_memory_max_bytes" + --cgroup-memory-high-bytes "$sample_adaptive_memory_high_bytes" + --cgroup-swap-max-bytes "$sample_adaptive_swap_max_bytes" + --sample-window-start-monotonic-ns "$sample_adaptive_window_start_ns" + --sample-window-end-monotonic-ns "$sample_adaptive_window_end_ns" + ) + fi + if [ "$lifecycle_bound" -eq 1 ] && + [ "$inner_validation_bound" -eq 1 ] && + assert_frozen_qualification_policy && + assert_frozen_carrier "$sample_label:adaptive-revalidation-before" && + python3 "$adaptive_cache_validator" "${adaptive_validator_args[@]}" && + assert_frozen_carrier "$sample_label:adaptive-revalidation-after" + then + adaptive_result="$( + awk -F '\t' -v OFS='\t' \ + -v sample_label="$sample_label" -v target="$target" \ + -v validation_schema="$adaptive_cache_validation_schema" \ + -v acceptance="$sample_adaptive_cache_evidence_policy" \ + -v policy_id="$adaptive_cache_policy_id" \ + -v config_id="$adaptive_cache_config_id" \ + -v config_sha="$adaptive_cache_config_sha256" \ + -v runtime_abi="$adaptive_cache_runtime_abi_id" \ + -v warmup="$adaptive_cache_warmup_samples" \ + -v telemetry_sha="$sample_adaptive_telemetry_sha256" \ + -v manifest_sha="$captured_carrier_manifest_sha256" \ + -v validator_sha="$adaptive_cache_validator_sha256" \ + -v cgroup_identity="$sample_adaptive_cgroup_identity" \ + -v memory_max="$sample_adaptive_memory_max_bytes" \ + -v memory_high="$sample_adaptive_memory_high_bytes" \ + -v effective_limit="$sample_adaptive_effective_limit_bytes" \ + -v swap_max="$sample_adaptive_swap_max_bytes" \ + -v window_start="$sample_adaptive_window_start_ns" \ + -v window_end="$sample_adaptive_window_end_ns" ' + NR == 1 { + expected = "schema_version status outcome reason workload_id policy_id config_id config_sha256 acceptance_policy runtime_abi_id state sample_count valid_offers advice_calls advised_bytes class6_offers class6_advice_calls class6_advised_bytes class6_advice_errors sample_errors clock_errors advice_errors wal_dirty_veto_bypasses wal_dirty_veto_bypass_bytes telemetry_sha256 manifest_sha256 validator_sha256 cgroup_identity cgroup_memory_max_bytes cgroup_memory_high_bytes cgroup_swap_max_bytes sample_window_start_monotonic_ns sample_window_end_monotonic_ns membership_leaf_identity pressure_source_identity last_sample_monotonic_ns last_sample_effective_limit_bytes measurement_id target" + split(expected, names, " ") + if (NF != 39) exit 1 + for (field_index = 1; field_index <= NF; field_index++) { + if ($field_index != names[field_index]) exit 1 + } + next + } + NR == 2 { + if (NF != 39 || $1 != validation_schema || + $2 != "passed" || $5 != "runtime:postgres" || + $6 != policy_id || $7 != config_id || $8 != config_sha || + $9 != acceptance || $10 != runtime_abi || + $25 != telemetry_sha || + $26 != manifest_sha || $27 != validator_sha || + $38 != sample_label || $39 != target) exit 1 + if (acceptance == "constrained-linux-wal-action-v1") { + if ($3 != "adaptive-active" || $16 !~ /^[1-9][0-9]*$/ || + $17 !~ /^[1-9][0-9]*$/ || $18 !~ /^[1-9][0-9]*$/ || + $12 !~ /^[0-9]+$/ || ($12 + 0) < (warmup + 0) || + $19 != 0 || $20 != 0 || $21 != 0 || $22 != 0 || + $28 != cgroup_identity || $29 != memory_max || + $30 != memory_high || $31 != swap_max || + $32 != window_start || $33 != window_end || + $34 != cgroup_identity || $35 != cgroup_identity || + $36 !~ /^[0-9]+$/ || ($36 + 0) < (window_start + 0) || + ($36 + 0) > (window_end + 0) || + $37 != effective_limit) exit 1 + } else if ($3 != "adaptive-active" && + $3 != "observe-only-fallback") { + exit 1 + } + print $3, $6, $7, $8, $10, $28, $29, $30, $31, $32, + $33, $34, $35, $36, $37, $16, $17, $18, $20, $21, $22 + valid = 1 + } + END { exit !(NR == 2 && valid) } + ' "$sample_adaptive_validation" + )" || adaptive_result="" + if [ -n "$adaptive_result" ] && + [ "$(fresh_wasmer_bin_hash "$sample_adaptive_telemetry")" = \ + "$sample_adaptive_telemetry_sha256" ]; then + IFS=$'\t' read -r sample_adaptive_outcome \ + sample_adaptive_policy_id sample_adaptive_config_id \ + sample_adaptive_config_sha256 sample_adaptive_runtime_abi_id \ + sample_adaptive_cgroup_identity sample_adaptive_memory_max_bytes \ + sample_adaptive_memory_high_bytes sample_adaptive_swap_max_bytes \ + sample_adaptive_window_start_ns sample_adaptive_window_end_ns \ + sample_adaptive_membership_identity \ + sample_adaptive_pressure_identity sample_adaptive_last_sample_ns \ + sample_adaptive_last_sample_limit_bytes \ + sample_adaptive_class6_offers \ + sample_adaptive_class6_advice_calls \ + sample_adaptive_class6_advised_bytes \ + sample_adaptive_sample_errors sample_adaptive_clock_errors \ + sample_adaptive_advice_errors <<<"$adaptive_result" + sample_adaptive_validation_sha256="$( + fresh_wasmer_bin_hash "$sample_adaptive_validation" + )" + sample_adaptive_status=0 + fi + fi + fi + fi + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$sample_label" "$target" "$sample_adaptive_cache_evidence_policy" \ + "$sample_adaptive_policy" "$sample_adaptive_policy_sha256" \ + "$sample_adaptive_contract" "$sample_adaptive_contract_sha256" \ + "$sample_server_lifecycle" "$sample_server_lifecycle_sha256" \ + "$sample_adaptive_telemetry" "$sample_adaptive_telemetry_sha256" \ + "$sample_adaptive_inner_validation" \ + "$sample_adaptive_inner_validation_sha256" \ + "$sample_adaptive_validation" "$sample_adaptive_validation_sha256" \ + "$sample_adaptive_outcome" "$sample_adaptive_policy_id" \ + "$sample_adaptive_config_id" "$sample_adaptive_config_sha256" \ + "$sample_adaptive_runtime_abi_id" "$sample_adaptive_cgroup_identity" \ + "$sample_adaptive_memory_max_bytes" \ + "$sample_adaptive_memory_high_bytes" "$sample_adaptive_swap_max_bytes" \ + "$sample_adaptive_final_memory_max_bytes" \ + "$sample_adaptive_final_memory_high_bytes" \ + "$sample_adaptive_final_swap_max_bytes" \ + "$sample_adaptive_window_start_ns" "$sample_adaptive_window_end_ns" \ + "$sample_adaptive_membership_identity" \ + "$sample_adaptive_pressure_identity" "$sample_adaptive_last_sample_ns" \ + "$sample_adaptive_last_sample_limit_bytes" \ + "$sample_adaptive_class6_offers" \ + "$sample_adaptive_class6_advice_calls" \ + "$sample_adaptive_class6_advised_bytes" \ + "$sample_adaptive_sample_errors" "$sample_adaptive_clock_errors" \ + "$sample_adaptive_advice_errors" \ + "$([ "$sample_adaptive_status" -eq 0 ] && printf passed || printf failed)" \ + >>"$adaptive_cache_verification_tsv" + if [ -f "$sample_instrumentation" ] && [ ! -L "$sample_instrumentation" ] && + awk -F '\t' ' + NR == 1 { + if ($1 != "schema_version" || $2 != "lane" || + $3 != "wasix_perf_stats" || $4 != "wait_dump_policy" || + $5 != "wait_dump_interval_ms" || + $6 != "wait_dump_max_per_wait" || $7 != "wait_dump_verbose" || + $8 != "fence_protocol" || $9 != "sanitized_environment") exit 1 + } + NR == 2 { + if ($1 != "oliphaunt.wasix-postmaster.instrumentation.v1" || + $2 != "benchmark" || $3 != 0 || $4 != "prohibited" || + $5 != 0 || $6 != 0 || $7 != 0 || $8 != "none") exit 1 + valid = 1 + } + END { exit !(NR == 2 && valid) } + ' "$sample_instrumentation" + then + sample_instrumentation_status=0 + sample_instrumentation_sha256="$(fresh_wasmer_bin_hash "$sample_instrumentation")" + fi + printf '%s\t%s\t%s\t%s\n' "$sample_label" "$sample_instrumentation" \ + "$sample_instrumentation_sha256" \ + "$([ "$sample_instrumentation_status" -eq 0 ] && printf passed || printf failed)" \ + >>"$instrumentation_verification_tsv" + if [ "$profile_resolution_active" -eq 1 ]; then + if [ ! -f "$sample_profile_inputs" ] || [ -L "$sample_profile_inputs" ] || + [ ! -f "$sample_profile_resolution" ] || [ -L "$sample_profile_resolution" ] || + ! cmp -s "$qualification_postgres_profile_inputs" "$sample_profile_inputs" || + ! cmp -s "$qualification_postgres_profile_resolution" "$sample_profile_resolution"; then + printf 'sample PostgreSQL profile evidence does not match qualification inputs: %s\n' \ + "$sample_report" >&2 + sample_profile_evidence_status=1 + fi + fi + settings_source="$sample_report/$target/effective-postgres-settings.tsv" + settings_path="$qualification_root/effective-settings/$sample_label.tsv" + settings_sha256="" + if [ -f "$settings_source" ] && [ ! -L "$settings_source" ]; then + settings_source_sha256="$(fresh_wasmer_bin_hash "$settings_source")" + cp -p "$settings_source" "$settings_path" + settings_sha256="$(fresh_wasmer_bin_hash "$settings_path")" + if [ "$settings_source_sha256" != "$settings_sha256" ] || + [ "$(fresh_wasmer_bin_hash "$settings_source")" != "$settings_sha256" ]; then + printf 'effective PostgreSQL settings changed while captured: %s\n' \ + "$settings_source" >&2 + settings_sha256="" + rm -f "$settings_path" + else + chmod 0444 "$settings_path" + fi + fi + sample_profile_settings_status=0 + if [ "$profile_resolution_active" -eq 1 ]; then + profile_validation_path="$qualification_root/effective-settings/$sample_label.profile-validation.tsv" + if [ -z "$settings_sha256" ]; then + sample_profile_settings_status=1 + elif fresh_validate_postgres_profile_settings \ + "$settings_path" "$profile_validation_path"; then + sample_profile_settings_status=0 + else + sample_profile_settings_status=$? + fi + fi + row="" + if [ -s "$sample_summary" ] && [ ! -L "$sample_summary" ] && + [ "$(sed -n '1p' "$sample_summary")" = "$expected_sample_summary_header" ]; then + row="$(awk -F '\t' 'NR == 2 { print $3 "\t" $10 "\t" $9 "\t" $6 }' "$sample_summary")" + fi + if [ -n "$row" ]; then + IFS=$'\t' read -r workload_status throughput wall_ms operation_count <<<"$row" + else + workload_status=1 + throughput="" + wall_ms="" + operation_count="" + fi + + client_rows=0 + client_evidence_status=1 + if [ -s "$sample_clients" ] && [ ! -L "$sample_clients" ] && + [ "$(sed -n '1p' "$sample_clients")" = "$expected_client_summary_header" ]; then + read -r client_rows client_evidence_status < <( + awk -F '\t' ' + NR > 1 { + rows += 1 + if ($4 != "0") invalid = 1 + } + END { printf "%d %d\n", rows, (rows > 0 && !invalid) ? 0 : 1 } + ' "$sample_clients" + ) + fi + if [ "$client_rows" -ne "$connections" ]; then + client_evidence_status=1 + fi + + effective_status=0 + if [ "$harness_status" -ne 0 ]; then + effective_status="$harness_status" + elif [ "$workload_status" != "0" ] || [ "$client_evidence_status" -ne 0 ]; then + effective_status=1 + elif [ -z "$settings_sha256" ]; then + effective_status=1 + elif [ "$sample_profile_evidence_status" -ne 0 ] || + [ "$sample_profile_settings_status" -ne 0 ]; then + effective_status=1 + elif [ "$sample_instrumentation_status" -ne 0 ]; then + effective_status=1 + elif [ "$sample_adaptive_status" -ne 0 ]; then + effective_status=1 + elif [ "$sample_loader_status" -ne 0 ]; then + effective_status=1 + fi + derived_metrics_valid=1 + if [ "$effective_status" -ne 0 ] || [ -z "$throughput" ] || + [ -z "$wall_ms" ] || [ -z "$operation_count" ]; then + derived_metrics_valid=0 + throughput="" + wall_ms="" + fi + if [ "$derived_metrics_valid" -ne 1 ]; then + overall_status=1 + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$block" "$workload" "$pair" "$position" "$target" "$sample_label" \ + "$workload_status" "$harness_status" "$effective_status" \ + "$derived_metrics_valid" "$throughput" "$wall_ms" "$operation_count" \ + "$sample_report" "$settings_path" "$settings_sha256" \ + "$frozen_carrier_closure_identity" \ + "${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-}" \ + "$frozen_native_oracle_identity" >>"$raw_tsv" + if [ "$client_rows" -gt 0 ]; then + awk -F '\t' -v OFS='\t' \ + -v block="$block" -v workload="$workload" -v pair="$pair" \ + -v position="$position" -v target="$target" -v report="$sample_report" \ + -v workload_status="$workload_status" -v harness_status="$harness_status" \ + -v effective_status="$effective_status" \ + -v derived_metrics_valid="$derived_metrics_valid" \ + -v settings_sha256="$settings_sha256" \ + -v carrier_identity="$frozen_carrier_closure_identity" \ + -v profile_identity="${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-}" \ + -v native_oracle_identity="$frozen_native_oracle_identity" ' + NR > 1 { + residual = "" + if ($6 != "" && $7 > 0) { + residual = sprintf("%.3f", $5 - $6) + } + print block, workload, pair, position, target, $3, $4, + workload_status, harness_status, effective_status, + derived_metrics_valid, $5, $6, $7, residual, report, + settings_sha256, carrier_identity, profile_identity, + native_oracle_identity + } + ' "$sample_clients" >>"$bulk_batch_raw_tsv" + else + printf '%s\t%s\t%s\t%s\t%s\t\t1\t%s\t%s\t%s\t0\t\t\t0\t\t%s\t%s\t%s\t%s\t%s\n' \ + "$block" "$workload" "$pair" "$position" "$target" \ + "$workload_status" "$harness_status" "$effective_status" \ + "$sample_report" "$settings_sha256" \ + "$frozen_carrier_closure_identity" \ + "${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-}" \ + "$frozen_native_oracle_identity" \ + >>"$bulk_batch_raw_tsv" + overall_status=1 + fi + if [ $((position % 2)) -eq 1 ]; then + pair_first_target="$target" + pair_first_settings="$settings_path" + else + comparison="$qualification_root/profile-comparisons/$(printf 'b%02d-%s-pair%s.tsv' "$block" "$workload" "$pair")" + native_settings="" + wasix_settings="" + if [ "$pair_first_target" = native ]; then + native_settings="$pair_first_settings" + wasix_settings="$settings_path" + else + native_settings="$settings_path" + wasix_settings="$pair_first_settings" + fi + profile_status=failed + if [ -f "$native_settings" ] && [ ! -L "$native_settings" ] && + [ -f "$wasix_settings" ] && [ ! -L "$wasix_settings" ]; then + set +e + "$FRESH_ROOT/bin/compare-postgres-settings.py" \ + "$native_settings" "$wasix_settings" "$comparison" + compare_status=$? + set -e + [ "$compare_status" -eq 0 ] && profile_status=passed + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$block" "$workload" "$pair" "$pair_first_target" "$target" \ + "$native_settings" "$wasix_settings" "$comparison" "$profile_status" \ + >>"$profile_tsv" + if [ "$profile_status" != passed ]; then + overall_status=1 + fi + pair_first_target="" + pair_first_settings="" + fi + done + done +done + +if [ "$adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ] && + ! awk -F '\t' ' + NR == 1 { + if (NF != 40 || $1 != "sample" || $2 != "target" || + $3 != "acceptance_policy" || $40 != "status") exit 1 + next + } + $2 == "wasix" && $3 == "constrained-linux-wal-action-v1" { + if ($40 != "passed" || $7 !~ /^[0-9a-f]{64}$/ || + $11 !~ /^[0-9a-f]{64}$/ || + $21 !~ /^[1-9][0-9]*:[1-9][0-9]*$/ || + $28 !~ /^[0-9]+$/ || $29 !~ /^[0-9]+$/ || + contract[$7]++ || telemetry[$11]++ || cgroup[$21]++ || + window[$28 ":" $29]++) exit 1 + constrained++ + } + END { exit constrained == 0 } +' "$adaptive_cache_verification_tsv" +then + echo 'constrained adaptive evidence is missing or reuses a sample identity' >&2 + overall_status=1 +fi + +if ! assert_frozen_carrier final full; then + overall_status=1 +fi +if ! assert_frozen_immutable_carrier final; then + overall_status=1 +fi +if ! assert_frozen_qualification_policy; then + overall_status=1 +fi +if ! assert_frozen_native_oracle final; then + overall_status=1 +fi +if [ "$profile_resolution_active" -eq 1 ] && + ! fresh_assert_postgres_profile_inputs; then + overall_status=1 +fi +wasix_execution_reports=() +while IFS= read -r sample_report; do + [ -n "$sample_report" ] && wasix_execution_reports+=("$sample_report") +done < <(awk -F '\t' 'NR > 1 && $5 == "wasix" { print $14 }' "$raw_tsv" | LC_ALL=C sort -u) +if fresh_freeze_wasix_execution_identity \ + "$wasix_execution_identity_tsv" \ + "$frozen_carrier_closure_identity" \ + "$captured_carrier_manifest_sha256" \ + "$captured_carrier_receipt_sha256" \ + "$captured_carrier_payload_sha256" \ + "$captured_carrier_headless_sha256" \ + "${runtime_footprint:-none}" \ + "${FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256:-none}" \ + "${durability_profile:-none}" \ + "${FRESH_POSTGRES_DURABILITY_SHA256:-none}" \ + "${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-none}" \ + "${wasix_execution_reports[@]}"; then + wasix_execution_identity_sha256="$FRESH_QUALIFICATION_EXECUTION_IDENTITY_SHA256" + wasix_postgres_module_sha256="$FRESH_QUALIFICATION_POSTGRES_MODULE_SHA256" +else + overall_status=1 +fi +chmod 0444 "$carrier_verification_tsv" +chmod 0444 "$native_oracle_verification_tsv" +chmod 0444 "$immutable_carrier_verification_tsv" +chmod 0444 "$sealed_loader_verification_tsv" +chmod 0444 "$adaptive_cache_verification_tsv" + +set +e +perl - "$raw_tsv" "$blocks" "$min_ratio" "$min_lcb" >"$paired_tsv" <<'PERL' +use strict; +use warnings; + +my ($input, $blocks, $min_ratio, $min_lcb) = @ARGV; +open my $fh, '<', $input or die "open $input: $!"; +my $header = <$fh>; +my %pairs; +my %samples; +my %failed; +my %workloads; +while (my $line = <$fh>) { + chomp $line; + my ($block, $workload, $pair, $position, $target, $label, + $workload_status, $harness_status, $effective_status, $valid, $rate) = + split /\t/, $line, -1; + $workloads{$workload} = 1; + $failed{$workload} = 1 + if $workload_status ne '0' || $harness_status ne '0' || + $effective_status ne '0' || $valid ne '1' || $rate eq ''; + next if $valid ne '1' || $rate eq ''; + push @{$samples{$workload}{$target}}, 0 + $rate; + $pairs{$workload}{"$block:$pair"}{$target} = 0 + $rate; +} + +sub median { + my @values = sort { $a <=> $b } @_; + return 0 unless @values; + my $middle = int(@values / 2); + return @values % 2 ? $values[$middle] : ($values[$middle - 1] + $values[$middle]) / 2; +} + +sub bootstrap_lcb { + my ($values) = @_; + return 0 unless @$values; + srand(0x4f4c4950); + my @medians; + for (1 .. 20_000) { + my @sample = map { $values->[int(rand(@$values))] } 1 .. @$values; + push @medians, median(@sample); + } + @medians = sort { $a <=> $b } @medians; + return $medians[int(0.05 * $#medians)]; +} + +print join("\t", qw(workload paired_samples native_samples wasix_samples + native_median_ops_per_sec wasix_median_ops_per_sec ratio_of_medians + paired_ratio_median paired_ratio_lcb95 paired_ratio_min status)), "\n"; +my $exit = 0; +for my $workload (sort keys %workloads) { + my @ratios; + for my $pair_id (sort keys %{$pairs{$workload} // {}}) { + my $pair = $pairs{$workload}{$pair_id}; + if (!defined $pair->{native} || !defined $pair->{wasix} || $pair->{native} <= 0) { + $failed{$workload} = 1; + next; + } + push @ratios, $pair->{wasix} / $pair->{native}; + } + my @native = @{$samples{$workload}{native} // []}; + my @wasix = @{$samples{$workload}{wasix} // []}; + my $native_median = median(@native); + my $wasix_median = median(@wasix); + my $ratio_of_medians = $native_median > 0 ? $wasix_median / $native_median : 0; + my $paired_median = median(@ratios); + my $lcb = bootstrap_lcb(\@ratios); + my $minimum = @ratios ? (sort { $a <=> $b } @ratios)[0] : 0; + my $expected_pairs = 2 * $blocks; + my $status = (!$failed{$workload} && @ratios == $expected_pairs && + $paired_median >= $min_ratio && $lcb >= $min_lcb) ? 'passed' : 'failed'; + $exit = 1 if $status ne 'passed'; + printf "%s\t%d\t%d\t%d\t%.3f\t%.3f\t%.6f\t%.6f\t%.6f\t%.6f\t%s\n", + $workload, scalar(@ratios), scalar(@native), scalar(@wasix), + $native_median, $wasix_median, $ratio_of_medians, $paired_median, + $lcb, $minimum, $status; +} +exit $exit; +PERL +summary_status=$? +set -e +if [ "$summary_status" -ne 0 ]; then + overall_status=1 +fi + +set +e +perl - "$bulk_batch_raw_tsv" "$blocks" "$connections" \ + "$max_batch_wall_p95" "$max_batch_wall_p99" \ + "$max_batch_residual_delta_p95_ms" \ + "$max_batch_residual_delta_p99_ms" >"$bulk_batch_tsv" <<'PERL' +use strict; +use warnings; + +my ($input, $blocks, $connections, $max_wall_p95, $max_wall_p99, + $max_residual_delta_p95, $max_residual_delta_p99) = @ARGV; +open my $fh, '<', $input or die "open $input: $!"; +my $header = <$fh>; +my (%pairs, %failed, %workloads); +while (my $line = <$fh>) { + chomp $line; + my ($block, $workload, $pair, $position, $target, $client, + $client_status, $workload_status, $harness_status, $effective_status, + $valid, $wall, $query, $query_count, $residual) = split /\t/, $line, -1; + $workloads{$workload} = 1; + my $id = join ':', $block, $pair, $client; + $failed{$workload} = 1 + if $client_status ne '0' || $workload_status ne '0' || + $harness_status ne '0' || $effective_status ne '0' || $valid ne '1' || + $wall eq '' || $query eq '' || $query_count !~ /^[1-9][0-9]*$/ || + $residual eq ''; + next if $client_status ne '0' || $workload_status ne '0' || + $harness_status ne '0' || $effective_status ne '0' || $valid ne '1' || + $wall eq '' || $query eq '' || $query_count !~ /^[1-9][0-9]*$/ || + $residual eq ''; + $pairs{$workload}{$id}{$target} = { + wall => 0 + $wall, + query => 0 + $query, + residual => 0 + $residual, + }; +} + +sub quantile { + my ($values, $fraction) = @_; + return 0 unless @$values; + my @sorted = sort { $a <=> $b } @$values; + my $rank = int($fraction * @sorted + 0.999999999) - 1; + $rank = 0 if $rank < 0; + $rank = $#sorted if $rank > $#sorted; + return $sorted[$rank]; +} + +print join("\t", qw(workload paired_bulk_batch_samples + native_bulk_batch_wall_p50_ms native_bulk_batch_wall_p95_ms native_bulk_batch_wall_p99_ms + wasix_bulk_batch_wall_p50_ms wasix_bulk_batch_wall_p95_ms wasix_bulk_batch_wall_p99_ms + paired_bulk_batch_wall_ratio_p50 paired_bulk_batch_wall_ratio_p95 paired_bulk_batch_wall_ratio_p99 + paired_bulk_batch_psql_time_sum_ratio_p50 paired_bulk_batch_psql_time_sum_ratio_p95 paired_bulk_batch_psql_time_sum_ratio_p99 + paired_bulk_batch_residual_delta_p50_ms paired_bulk_batch_residual_delta_p95_ms paired_bulk_batch_residual_delta_p99_ms + status)), "\n"; +my $exit = 0; +for my $workload (sort keys %workloads) { + my (@native_wall, @wasix_wall, @wall_ratios, @query_ratios, @residual_deltas); + for my $id (sort keys %{$pairs{$workload} // {}}) { + my $pair = $pairs{$workload}{$id}; + if (!defined $pair->{native} || !defined $pair->{wasix} || + $pair->{native}{wall} <= 0 || $pair->{native}{query} <= 0) { + $failed{$workload} = 1; + next; + } + push @native_wall, $pair->{native}{wall}; + push @wasix_wall, $pair->{wasix}{wall}; + push @wall_ratios, $pair->{wasix}{wall} / $pair->{native}{wall}; + push @query_ratios, $pair->{wasix}{query} / $pair->{native}{query}; + push @residual_deltas, + $pair->{wasix}{residual} - $pair->{native}{residual}; + } + my $expected = 2 * $blocks * $connections; + my $wall_ratio_p95 = quantile(\@wall_ratios, 0.95); + my $wall_ratio_p99 = quantile(\@wall_ratios, 0.99); + my $residual_delta_p95 = quantile(\@residual_deltas, 0.95); + my $residual_delta_p99 = quantile(\@residual_deltas, 0.99); + my $status = (!$failed{$workload} && @wall_ratios == $expected && + $wall_ratio_p95 <= $max_wall_p95 && $wall_ratio_p99 <= $max_wall_p99 && + $residual_delta_p95 <= $max_residual_delta_p95 && + $residual_delta_p99 <= $max_residual_delta_p99) + ? 'passed' : 'failed'; + $exit = 1 if $status ne 'passed'; + printf "%s\t%d\t%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t%.6f\t%.6f\t%.6f\t%.6f\t%.6f\t%.6f\t%.3f\t%.3f\t%.3f\t%s\n", + $workload, scalar(@wall_ratios), + quantile(\@native_wall, 0.50), quantile(\@native_wall, 0.95), + quantile(\@native_wall, 0.99), quantile(\@wasix_wall, 0.50), + quantile(\@wasix_wall, 0.95), quantile(\@wasix_wall, 0.99), + quantile(\@wall_ratios, 0.50), $wall_ratio_p95, $wall_ratio_p99, + quantile(\@query_ratios, 0.50), quantile(\@query_ratios, 0.95), + quantile(\@query_ratios, 0.99), quantile(\@residual_deltas, 0.50), + $residual_delta_p95, $residual_delta_p99, $status; +} +exit $exit; +PERL +bulk_batch_status=$? +set -e +if [ "$bulk_batch_status" -ne 0 ]; then + overall_status=1 +fi + +classification="throughput-diagnostic-non-promotional" +if [ "$blocks" -ge 10 ] && [ "$overall_status" -eq 0 ]; then + classification="throughput-qualified-non-promotional" +elif [ "$blocks" -ge 10 ]; then + classification="failed-throughput-qualification-non-promotional" +fi +printf 'schema_version\tstatus\tclassification\tdetail\tcarrier_closure_identity\tnative_oracle_identity\tpostgres_profile_resolution_identity\truntime_footprint_sha256\tdurability_profile_sha256\tqualification_policy_sha256\tsamples_sha256\tpaired_summary_sha256\tbulk_batch_samples_sha256\tbulk_batch_summary_sha256\twasix_execution_identity_sha256\tpostgres_module_sha256\timmutable_carrier_verification_sha256\tsealed_loader_verification_sha256\tcore_profile\tguest_build_recipe_sha256\timmutable_verification_scope\tcgroup_binding\tcgroup_memory_max\tcgroup_memory_high\tcgroup_swap_max\tcgroup_environment_action\tadaptive_cache_evidence_policy\tadaptive_cache_validator_sha256\tadaptive_cache_verification_sha256\tadaptive_cache_validation_schema\tadaptive_cache_policy_id\tadaptive_cache_config_id\tadaptive_cache_config_sha256\tadaptive_cache_warmup_samples\tadaptive_cache_runtime_abi_id\tadaptive_cache_sample_contract_schema\twasix_shared_memory_provider\n' \ + >"$qualification_result_tsv" +printf 'oliphaunt.wasix-postmaster.throughput-result.v7\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$([ "$overall_status" -eq 0 ] && printf passed || printf failed)" \ + "$classification" \ + "$([ "$overall_status" -eq 0 ] && printf all-declared-throughput-gates-passed || printf one-or-more-throughput-gates-failed)" \ + "$frozen_carrier_closure_identity" \ + "$frozen_native_oracle_identity" \ + "${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-none}" \ + "${FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256:-none}" \ + "${FRESH_POSTGRES_DURABILITY_SHA256:-none}" \ + "$qualification_policy_identity" \ + "$(fresh_wasmer_bin_hash "$raw_tsv")" \ + "$(fresh_wasmer_bin_hash "$paired_tsv")" \ + "$(fresh_wasmer_bin_hash "$bulk_batch_raw_tsv")" \ + "$(fresh_wasmer_bin_hash "$bulk_batch_tsv")" \ + "$wasix_execution_identity_sha256" \ + "$wasix_postgres_module_sha256" \ + "$(fresh_wasmer_bin_hash "$immutable_carrier_verification_tsv")" \ + "$(fresh_wasmer_bin_hash "$sealed_loader_verification_tsv")" \ + "$captured_core_profile" "$captured_guest_build_recipe_sha256" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf campaign-boundary-full-fast-samples || printf full-per-check)" \ + "$cgroup_binding" "${cgroup_memory_max:-none}" \ + "${cgroup_memory_high:-none}" "${cgroup_swap_max:-none}" \ + "$cgroup_environment_action" "$adaptive_cache_evidence_policy" \ + "$adaptive_cache_validator_sha256" \ + "$(fresh_wasmer_bin_hash "$adaptive_cache_verification_tsv")" \ + "$adaptive_cache_validation_schema" "$adaptive_cache_policy_id" \ + "$adaptive_cache_config_id" "$adaptive_cache_config_sha256" \ + "$adaptive_cache_warmup_samples" "$adaptive_cache_runtime_abi_id" \ + "$adaptive_cache_sample_contract_schema" "$shared_memory_provider" \ + >>"$qualification_result_tsv" +chmod 0444 "$qualification_result_tsv" +fresh_write_report_header "$summary" "WASIX Single-Backend Qualification" +{ + printf -- '- Classification: `%s`\n' "$classification" + printf -- '- Composition scope: `throughput-only; lifecycle and memory evidence are not consumed, so this result cannot assert embedded viability or promotion`\n' + printf -- '- Pre-run throughput policy: `%s` (`%s`)\n' \ + "$qualification_policy_tsv" "$qualification_policy_identity" + printf -- '- Machine result receipt: `%s`\n' "$qualification_result_tsv" + printf -- '- Balanced blocks: `%s`\n' "$blocks" + printf -- '- Samples per target/workload: `%s`\n' "$((blocks * 2))" + printf -- '- Connections: `%s`\n' "$connections" + printf -- '- Iterations per client: `%s`\n' "$iterations" + printf -- '- Rows: `%s`\n' "$row_count" + printf -- '- WASIX shared-memory provider: `%s`\n' "$shared_memory_provider" + printf -- '- Workloads: `%s`\n' "${workloads[*]}" + printf -- '- Checkpoint policy: `controlled`\n' + printf -- '- Background resource sampling: `off`\n' + printf -- '- Server cgroup binding: `%s`\n' "$cgroup_binding" + printf -- '- Server cgroup MemoryMax / MemoryHigh / MemorySwapMax: `%s / %s / %s`\n' \ + "${cgroup_memory_max:-unset}" "${cgroup_memory_high:-unset}" \ + "${cgroup_swap_max:-unset}" + printf -- '- Adaptive cache evidence policy: `%s`\n' \ + "$adaptive_cache_evidence_policy" + printf -- '- Per-sample adaptive cache verification: `%s`\n' \ + "$adaptive_cache_verification_tsv" + printf -- '- Wait-dump instrumentation: `prohibited and removed from every harness environment`\n' + printf -- '- Instrumentation policy: `%s`\n' "$instrumentation_policy_tsv" + printf -- '- Per-sample instrumentation verification: `%s`\n' \ + "$instrumentation_verification_tsv" + printf -- '- Direct immutable activation required: `%s`\n' "$require_zero_write_aot" + printf -- '- Per-sample sealed-loader verification: `%s`\n' \ + "$sealed_loader_verification_tsv" + if [ "$require_zero_write_aot" -eq 1 ]; then + printf -- '- Immutable deployment receipt: `%s` (`%s`, dev `%s`, ino `%s`)\n' \ + "$immutable_carrier_receipt" "$immutable_carrier_receipt_sha256" \ + "$immutable_carrier_receipt_dev" "$immutable_carrier_receipt_ino" + printf -- '- Immutable deployment identity verification: `%s`\n' \ + "$immutable_carrier_verification_tsv" + fi + printf -- '- Extra PostgreSQL GUCs: `%s`\n' "${postgres_gucs[*]:-}" + printf -- '- Runtime footprint: `%s`\n' "${FRESH_POSTGRES_RUNTIME_FOOTPRINT_ID:-}" + printf -- '- Runtime-footprint SHA-256: `%s`\n' \ + "${FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256:-}" + printf -- '- Durability profile: `%s`\n' "${FRESH_POSTGRES_DURABILITY_ID:-}" + printf -- '- Durability-profile SHA-256: `%s`\n' \ + "${FRESH_POSTGRES_DURABILITY_SHA256:-}" + printf -- '- PostgreSQL profile resolution identity: `%s`\n' \ + "${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-}" + if [ "$profile_resolution_active" -eq 1 ]; then + printf -- '- PostgreSQL profile inputs: `%s`\n' \ + "$qualification_postgres_profile_inputs" + printf -- '- PostgreSQL profile resolution: `%s`\n' \ + "$qualification_postgres_profile_resolution" + fi + printf -- '- Paired median gate: `%s`\n' "$min_ratio" + printf -- '- One-sided bootstrap 95%% LCB gate: `%s`\n' "$min_lcb" + printf -- '- Paired bulk-batch wall p95/p99 ratio gates: `%s` / `%s`\n' \ + "$max_batch_wall_p95" "$max_batch_wall_p99" + printf -- '- Bulk-batch residual p95/p99 delta gates: `%s ms` / `%s ms`\n' \ + "$max_batch_residual_delta_p95_ms" \ + "$max_batch_residual_delta_p99_ms" + printf -- '- Bulk-batch residual definition: `batch wall minus summed psql-timed statements; not per-query latency or isolated backend launch`\n' + printf -- '- Successful PGDATA discarded: `%s`\n' "$discard_pgdata" + printf -- '- Sealed carrier: `%s`\n' "$sealed_carrier" + printf -- '- Frozen carrier closure identity: `%s`\n' "$frozen_carrier_closure_identity" + printf -- '- Carrier identity evidence: `%s`\n' "$carrier_identity_tsv" + printf -- '- Per-sample carrier verification: `%s`\n' "$carrier_verification_tsv" + printf -- '- Frozen native oracle identity: `%s`\n' "$frozen_native_oracle_identity" + printf -- '- Native oracle closure evidence: `%s`\n' "$native_oracle_identity_tsv" + printf -- '- Per-sample native oracle verification: `%s`\n' \ + "$native_oracle_verification_tsv" + printf -- '- Paired effective-settings comparison: `%s`\n' "$profile_tsv" + printf -- '- Raw samples: `%s`\n' "$raw_tsv" + printf -- '- Paired summary: `%s`\n' "$paired_tsv" + printf -- '- Raw bulk-batch samples: `%s`\n' "$bulk_batch_raw_tsv" + printf -- '- Bulk-batch summary: `%s`\n' "$bulk_batch_tsv" + printf -- '- Exit code: `%s`\n' "$overall_status" +} >>"$summary" + +if [ "$overall_status" -ne 0 ]; then + printf 'failed: single-backend qualification; see %s\n' "$summary" >&2 +else + printf 'passed: throughput-only single-backend qualification (non-promotional); see %s\n' "$summary" +fi +exit "$overall_status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-single-backend.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-single-backend.test.sh new file mode 100755 index 00000000..7fb86553 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-single-backend.test.sh @@ -0,0 +1,1174 @@ +#!/usr/bin/env bash + +set -euo pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +QUALIFIER="$PROJECT_ROOT/bin/qualify-wasix-single-backend.sh" +TEST_ROOT="$(mktemp -d)" +cleanup() { + if [ "${KEEP_WASIX_SINGLE_BACKEND_TEST_ROOT:-0}" = 1 ]; then + printf 'preserved single-backend fixture root: %s\n' "$TEST_ROOT" >&2 + else + rm -rf -- "$TEST_ROOT" + fi +} +trap cleanup EXIT + +FAKE_PROJECT="$TEST_ROOT/project" +FAKE_REPORTS="$TEST_ROOT/reports" +FAKE_CARRIER="$TEST_ROOT/carrier" +FAKE_NATIVE="$TEST_ROOT/native" +FAKE_IMMUTABLE_RECEIPT="$TEST_ROOT/immutable-carrier.receipt.json" +mkdir -p "$FAKE_PROJECT/bin" "$FAKE_PROJECT/lib" \ + "$FAKE_PROJECT/profiles/runtime-footprints" \ + "$FAKE_PROJECT/profiles/durability" "$FAKE_REPORTS" "$FAKE_CARRIER" \ + "$FAKE_NATIVE/bin" "$FAKE_NATIVE/lib" +cp "$QUALIFIER" "$FAKE_PROJECT/bin/qualify-wasix-single-backend.sh" +cp "$PROJECT_ROOT/bin/compare-postgres-settings.py" "$FAKE_PROJECT/bin/compare-postgres-settings.py" +cp "$PROJECT_ROOT/bin/validate-adaptive-file-cache-telemetry.py" \ + "$FAKE_PROJECT/bin/validate-adaptive-file-cache-telemetry.py" +cp "$PROJECT_ROOT/bin/validate-adaptive-file-cache-telemetry.test.py" \ + "$FAKE_PROJECT/bin/validate-adaptive-file-cache-telemetry.test.py" +cp "$PROJECT_ROOT/lib/postgres-profiles.sh" "$FAKE_PROJECT/lib/postgres-profiles.sh" +cp "$PROJECT_ROOT/lib/qualification-identities.sh" "$FAKE_PROJECT/lib/qualification-identities.sh" +cp "$PROJECT_ROOT/profiles/runtime-footprints/embedded-concurrent-v1.gucs" \ + "$FAKE_PROJECT/profiles/runtime-footprints/embedded-concurrent-v1.gucs" +cp "$PROJECT_ROOT/profiles/durability/safe-v1.gucs" \ + "$FAKE_PROJECT/profiles/durability/safe-v1.gucs" +chmod +x "$FAKE_PROJECT/bin/compare-postgres-settings.py" +printf '{"schema":"fake-immutable"}\n' >"$FAKE_IMMUTABLE_RECEIPT" + +cat >"$FAKE_PROJECT/lib/common.sh" <"\$report" +} +fresh_wasmer_bin_hash() { + sha256sum "\$1" | awk '{ print \$1 }' +} +fresh_sha256_stream() { + sha256sum | awk '{ print \$1 }' +} +EOF_COMMON + +cat >"$FAKE_PROJECT/lib/sealed-carrier.sh" <<'EOF_SEALED' +#!/usr/bin/env bash +fresh_verify_sealed_headless_carrier() { + local root="$1" + [ -d "$root" ] && [ ! -L "$root" ] && + [ -f "$root/manifest.json" ] && [ ! -L "$root/manifest.json" ] && + [ -f "$root/wasmer-build.receipt" ] && [ ! -L "$root/wasmer-build.receipt" ] && + [ -f "$root/payload.files" ] && [ ! -L "$root/payload.files" ] && + [ -x "$root/bin/wasmer-headless" ] && [ ! -L "$root/bin/wasmer-headless" ] +} +EOF_SEALED + +cat >"$FAKE_PROJECT/bin/verify-immutable-sealed-carrier.sh" <<'EOF_IMMUTABLE' +#!/usr/bin/env bash +set -euo pipefail +{ [ "$#" -eq 4 ] || { [ "$#" -eq 5 ] && [ "$5" = --fast ]; }; } && + [ "$1" = --sealed-carrier ] && [ -d "$2" ] && + [ "$3" = --receipt ] && [ -f "$4" ] && [ ! -L "$4" ] +EOF_IMMUTABLE +chmod +x "$FAKE_PROJECT/bin/verify-immutable-sealed-carrier.sh" + +mkdir -p "$FAKE_CARRIER/bin" +python3 - "$FAKE_PROJECT/bin/validate-adaptive-file-cache-telemetry.py" \ + "$FAKE_CARRIER/manifest.json" <<'PY' +import json +import runpy +import sys +from pathlib import Path + +constants = runpy.run_path(sys.argv[1]) +manifest = { + "core-profile": "release-o3", + "guest-build-recipe-sha256": "0" * 64, + "runtime-abi-id": "12" * 32, + "file-cache-policy": { + "requested-policy-id": constants["POLICY_ID"], + "approved-config-id": constants["CONFIG_ID"], + "config-sha256": constants["CONFIG_SHA256"], + "portable-fallback-mode": "observe-only", + }, + "schema": "fake", +} +Path(sys.argv[2]).write_text(json.dumps(manifest) + "\n", encoding="utf-8") +PY +printf 'schema=fake\n' >"$FAKE_CARRIER/wasmer-build.receipt" +printf 'schema=fake-payload\n' >"$FAKE_CARRIER/payload.files" +printf '#!/usr/bin/env bash\nexit 0\n' >"$FAKE_CARRIER/bin/wasmer-headless" +chmod +x "$FAKE_CARRIER/bin/wasmer-headless" +for binary in postgres initdb psql; do + printf '#!/usr/bin/env bash\nexit 0\n' >"$FAKE_NATIVE/bin/$binary" + chmod +x "$FAKE_NATIVE/bin/$binary" +done +printf 'fake libpq\n' >"$FAKE_NATIVE/lib/libpq.so.5.18" +ln -s libpq.so.5.18 "$FAKE_NATIVE/lib/libpq.so.5" +ln -s libpq.so.5 "$FAKE_NATIVE/lib/libpq.so" + +cat >"$FAKE_PROJECT/bin/bench-wasix-concurrent-query-suite.sh" <<'EOF_HARNESS' +#!/usr/bin/env bash +set -euo pipefail + +source "$FRESH_ROOT/lib/common.sh" +source "$FRESH_ROOT/lib/postgres-profiles.sh" +source "$FRESH_ROOT/lib/sealed-carrier.sh" +source "$FRESH_ROOT/lib/qualification-identities.sh" + +label="" +target="" +workload="" +connections=1 +iterations=1 +sealed_carrier="" +postgres_gucs=() +runtime_footprint="" +durability_profile="" +shared_memory_provider="" +require_zero_write_aot=0 +immutable_carrier_receipt="" +immutable_verification_scope=full +cgroup_memory_max="" +cgroup_memory_high="" +cgroup_swap_max="" +adaptive_cache_evidence_policy=portable-correctness-v1 +for wait_dump_name in \ + WASIX_PERF_WAIT_DUMP_INTERVAL_MS WASIX_PERF_WAIT_DUMP_FILE \ + WASIX_PERF_WAIT_DUMP_MAX_PER_WAIT WASIX_PERF_WAIT_DUMP_VERBOSE \ + WASIX_WAIT_DUMP_INTERVAL_MS WASIX_WAIT_DUMP_FILE \ + WASIX_WAIT_DUMP_MAX_PER_WAIT WASIX_WAIT_DUMP_VERBOSE \ + WASIX_WAIT_DUMP_FENCE_REQUEST_FILE WASIX_WAIT_DUMP_FENCE_ACK_FILE +do + if [[ -v $wait_dump_name ]]; then + printf 'fake timed harness inherited %s\n' "$wait_dump_name" >&2 + exit 65 + fi +done +[ ! -v OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT ] +[ ! -v OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE ] +[ ! -v WASIX_CGROUP_MEMORY_MAX ] +[ ! -v WASIX_CGROUP_MEMORY_HIGH ] +[ ! -v WASIX_CGROUP_SWAP_MAX ] +[ "${WASIX_PERF_STATS:-}" = 0 ] +while [ "$#" -gt 0 ]; do + case "$1" in + --label) shift; label="$1" ;; + --target) shift; target="$1" ;; + --workload) shift; workload="$1" ;; + --connections) shift; connections="$1" ;; + --iterations) shift; iterations="$1" ;; + --sealed-carrier) shift; sealed_carrier="$1" ;; + --postgres-guc) shift; postgres_gucs+=("$1") ;; + --runtime-footprint) shift; runtime_footprint="$1" ;; + --durability) shift; durability_profile="$1" ;; + --shared-memory-provider) shift; shared_memory_provider="$1" ;; + --require-zero-write-aot) require_zero_write_aot=1 ;; + --immutable-carrier-receipt) shift; immutable_carrier_receipt="$1" ;; + --immutable-carrier-verification-scope) shift; immutable_verification_scope="$1" ;; + --cgroup-memory-max) shift; cgroup_memory_max="$1" ;; + --cgroup-memory-high) shift; cgroup_memory_high="$1" ;; + --cgroup-swap-max) shift; cgroup_swap_max="$1" ;; + --adaptive-cache-evidence-policy) + shift + adaptive_cache_evidence_policy="$1" + ;; + --rows|--timeout|--start-port|--checkpoint-policy|--resource-detail) + shift + ;; + esac + shift +done + +if [ -n "${FAKE_EXPECT_CGROUP_BINDING:-}" ]; then + IFS=: read -r expected_max expected_high expected_swap \ + <<<"$FAKE_EXPECT_CGROUP_BINDING" + [ "$cgroup_memory_max" = "$expected_max" ] && + [ "$cgroup_memory_high" = "$expected_high" ] && + [ "$cgroup_swap_max" = "$expected_swap" ] +else + [ -z "$cgroup_memory_max$cgroup_memory_high$cgroup_swap_max" ] +fi + +if [ "$target" = wasix ]; then + [ "$shared_memory_provider" = \ + "${FAKE_EXPECT_SHARED_MEMORY_PROVIDER:-portable-file-v1}" ] +else + [ -z "$shared_memory_provider" ] +fi + +if [ "$require_zero_write_aot" -eq 1 ]; then + [ -f "$immutable_carrier_receipt" ] +fi + +report="$REPORT_DIR/concurrent-query-suite/$label" +mkdir -p "$report/$target" +printf '%s\n' "${shared_memory_provider:-not-applicable}" \ + >"$report/$target/shared-memory-provider.txt" +fake_carrier_closure=none +if [ -n "$sealed_carrier" ]; then + fresh_capture_qualification_carrier_identity "$sealed_carrier" + fake_carrier_closure="$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" +fi +adaptive_runtime_mode=compiler +adaptive_manifest_sha=none +if [ "$target" = wasix ]; then + adaptive_runtime_mode=sealed-headless + adaptive_manifest_sha="$FRESH_QUALIFICATION_CARRIER_MANIFEST_SHA256" +fi +adaptive_claim_scope=portable-correctness +adaptive_required_host=any +adaptive_required_runtime=any +adaptive_required_outcome=adaptive-active-or-observe-only-fallback +adaptive_required_class=none +adaptive_min_offers=0 +adaptive_min_calls=0 +adaptive_min_bytes=0 +adaptive_max_sample_errors=unbounded +adaptive_max_clock_errors=unbounded +adaptive_max_advice_errors=unbounded +adaptive_max_psi_breaker_trips=unbounded +adaptive_max_refault_breaker_trips=unbounded +adaptive_max_deferred_wal_pin_errors=unbounded +adaptive_max_contended_wal_pin_failures=unbounded +adaptive_terminal_receipt=active-finalized-or-admission-fallback +adaptive_scope_contract=not-required +adaptive_required_cgroup_binding=none +adaptive_required_limit_binding=none +adaptive_required_monotonic_window=none +if [ "$adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ]; then + [ "$target" = wasix ] + adaptive_claim_scope=constrained-linux-performance + adaptive_required_host=Linux + adaptive_required_runtime=sealed-headless + adaptive_required_outcome=adaptive-active + adaptive_required_class=6 + adaptive_min_offers=1 + adaptive_min_calls=1 + adaptive_min_bytes=1 + adaptive_max_sample_errors=0 + adaptive_max_clock_errors=0 + adaptive_max_advice_errors=0 + adaptive_max_psi_breaker_trips=0 + adaptive_max_refault_breaker_trips=0 + adaptive_max_deferred_wal_pin_errors=0 + adaptive_max_contended_wal_pin_failures=0 + adaptive_terminal_receipt=active-finalized + adaptive_scope_contract=required + adaptive_required_cgroup_binding=per-target-device-inode + adaptive_required_limit_binding=requested-equals-leaf-and-effective-min + adaptive_required_monotonic_window=launch-before-through-post-shutdown +fi +adaptive_validator_sha="$( + fresh_wasmer_bin_hash "$FRESH_ROOT/bin/validate-adaptive-file-cache-telemetry.py" +)" +IFS=$'\t' read -r adaptive_policy_id adaptive_config_id adaptive_config_sha < <( + python3 - "$FRESH_ROOT/bin/validate-adaptive-file-cache-telemetry.py" <<'PY' +import runpy +import sys + +constants = runpy.run_path(sys.argv[1]) +print( + constants["POLICY_ID"], + constants["CONFIG_ID"], + constants["CONFIG_SHA256"], + sep="\t", +) +PY +) +printf 'schema_version\tacceptance_policy\tclaim_scope\trequired_host\tselected_host\trequired_runtime_mode\tselected_runtime_mode\tselected_memory_max\tselected_memory_high\tselected_swap_max\trequired_outcome\trequired_class\tmin_class_offers\tmin_class_advice_calls\tmin_class_advised_bytes\tmax_sample_errors\tmax_clock_errors\tmax_advice_errors\tmax_psi_breaker_trips\tmax_refault_breaker_trips\tmax_deferred_wal_pin_errors\tmax_contended_wal_pin_failures\tterminal_receipt\tvalidator_sha256\tsealed_manifest_sha256\tsample_scope_contract\trequired_cgroup_binding\trequired_limit_binding\trequired_monotonic_window\n' \ + >"$report/adaptive-cache-evidence-policy.tsv" +printf 'oliphaunt.wasix-postmaster.adaptive-cache-evidence-policy.v3\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$adaptive_cache_evidence_policy" "$adaptive_claim_scope" \ + "$adaptive_required_host" "$(uname -s)" "$adaptive_required_runtime" \ + "$adaptive_runtime_mode" "${cgroup_memory_max:-none}" \ + "${cgroup_memory_high:-none}" "${cgroup_swap_max:-none}" \ + "$adaptive_required_outcome" "$adaptive_required_class" \ + "$adaptive_min_offers" "$adaptive_min_calls" "$adaptive_min_bytes" \ + "$adaptive_max_sample_errors" "$adaptive_max_clock_errors" \ + "$adaptive_max_advice_errors" "$adaptive_max_psi_breaker_trips" \ + "$adaptive_max_refault_breaker_trips" \ + "$adaptive_max_deferred_wal_pin_errors" \ + "$adaptive_max_contended_wal_pin_failures" "$adaptive_terminal_receipt" \ + "$adaptive_validator_sha" "$adaptive_manifest_sha" \ + "$adaptive_scope_contract" "$adaptive_required_cgroup_binding" \ + "$adaptive_required_limit_binding" "$adaptive_required_monotonic_window" \ + >>"$report/adaptive-cache-evidence-policy.tsv" +adaptive_policy_sha="$(fresh_wasmer_bin_hash "$report/adaptive-cache-evidence-policy.tsv")" +if [ "$target" = wasix ]; then + adaptive_raw="$report/$target/cache-offers-postgres.adaptive.json" + IFS=$'\t' read -r adaptive_cgroup_identity adaptive_window_start \ + adaptive_window_end < <( + python3 - \ + "$FRESH_ROOT/bin/validate-adaptive-file-cache-telemetry.test.py" \ + "$adaptive_raw" "$label" "$adaptive_cache_evidence_policy" <<'PY' +import hashlib +import json +from pathlib import Path +import runpy +import sys + +fixtures = runpy.run_path(sys.argv[1]) +output = Path(sys.argv[2]) +label = sys.argv[3] +policy = sys.argv[4] +seed = int(hashlib.sha256(label.encode()).hexdigest()[:12], 16) +device = 31 +inode = seed + 1 +sample_ns = 10_000_000_000 + seed +window_start = sample_ns - 1_000_000 +window_end = sample_ns + 1_000_000 +if policy == fixtures["CONSTRAINED_POLICY"]: + value = fixtures["active_wal_action"]() +else: + value = fixtures["fallback"]() +if label.startswith("adaptive-zero-action-"): + value = fixtures["active"]() +elif label.startswith("adaptive-fallback-") or ( + label.startswith("adaptive-mixed-receipt-") and "-p3-wasix" in label +): + value = fixtures["fallback"]() +elif label.startswith("adaptive-breaker-"): + value["psi-breaker-trips"] = 1 +elif label.startswith("adaptive-pin-failure-"): + wal_bytes = 1024 * 1024 + value["classes"][5]["offers"] += 1 + value["classes"][5]["offered-finite-bytes"] += wal_bytes + value["validation"][0] += 1 + value["range-offered-bytes"] += wal_bytes + value["range-aligned-bytes"] += wal_bytes + value["retain-reasons"][3]["calls"] += 1 + value["action-gate-contended-calls"] = 1 + value["action-gate-contended-retained"] = 1 + value["action-gate-contended-wal-pin-failures"] = 1 +if "sample-count" in value and value.get("last-sample") is not None: + # Qualification evidence is intentionally stricter than a structurally + # valid warmup receipt: model a run that reached the configured admission + # floor before it made a performance claim. + value["sample-count"] = value["config"]["warmup-samples"] +if "last-sample" in value and value["last-sample"] is not None: + sample = value["last-sample"] + sample["membership-leaf-device"] = device + sample["membership-leaf-inode"] = inode + sample["pressure-source-device"] = device + sample["pressure-source-inode"] = inode + sample["monotonic-ns"] = sample_ns + sample["effective-limit-bytes"] = 224 * 1024 * 1024 + if label.startswith("adaptive-cgroup-mismatch-"): + sample["membership-leaf-inode"] += 1 + sample["pressure-source-inode"] += 1 + if label.startswith("adaptive-time-mismatch-"): + sample["monotonic-ns"] = window_end + 1 + if label.startswith("adaptive-limit-mismatch-"): + sample["effective-limit-bytes"] = 256 * 1024 * 1024 +output.write_text(json.dumps(value) + "\n", encoding="utf-8") +print(f"{device}:{inode}", window_start, window_end, sep="\t") +PY + ) + adaptive_contract_mode=portable-not-required + adaptive_contract_path=none + adaptive_lifecycle_path="" + adaptive_contract_identity=none + adaptive_contract_unit=none + adaptive_requested_max_bytes=none + adaptive_requested_high_bytes=none + adaptive_requested_swap_bytes=none + adaptive_observed_max_bytes=none + adaptive_observed_high_bytes=none + adaptive_observed_swap_bytes=none + adaptive_final_max_bytes=none + adaptive_final_high_bytes=none + adaptive_final_swap_bytes=none + adaptive_contract_start=none + adaptive_contract_end=none + if [ "$adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ]; then + adaptive_contract_mode=constrained-exact-cgroup-time + adaptive_contract_identity="$adaptive_cgroup_identity" + adaptive_contract_unit="fake-$label" + adaptive_contract_path="/user.slice/user-1000.slice/user@1000.service/app.slice/$adaptive_contract_unit.scope" + adaptive_lifecycle_path="$adaptive_contract_path" + if [[ "$label" == adaptive-mixed-cgroup-path-* ]]; then + # Reproduce the historical bug exactly: the contract carried the + # controller filesystem path while lifecycle carried /proc/PID/cgroup's + # namespace-relative membership path. + adaptive_contract_path="/sys/fs/cgroup$adaptive_lifecycle_path" + fi + adaptive_requested_max_bytes=268435456 + adaptive_requested_high_bytes=234881024 + adaptive_requested_swap_bytes=0 + adaptive_observed_max_bytes="$adaptive_requested_max_bytes" + adaptive_observed_high_bytes="$adaptive_requested_high_bytes" + adaptive_observed_swap_bytes="$adaptive_requested_swap_bytes" + adaptive_final_max_bytes="$adaptive_observed_max_bytes" + adaptive_final_high_bytes="$adaptive_observed_high_bytes" + adaptive_final_swap_bytes="$adaptive_observed_swap_bytes" + if [[ "$label" == adaptive-cgroup-limit-drift-* ]]; then + adaptive_final_high_bytes=$((adaptive_final_high_bytes - 4096)) + fi + adaptive_contract_start="$adaptive_window_start" + adaptive_contract_end="$adaptive_window_end" + fi + adaptive_server_pid=202 + adaptive_server_birth_identity="birth-$label" + printf 'schema_version\tmeasurement_id\ttarget\tacceptance_policy\tcontract_mode\tbase_policy_sha256\tvalidator_sha256\tmanifest_sha256\tcgroup_path\tcgroup_identity\tserver_pid\tserver_birth_identity\tcgroup_unit\trequested_memory_max\trequested_memory_high\trequested_swap_max\trequested_memory_max_bytes\trequested_memory_high_bytes\trequested_swap_max_bytes\tobserved_initial_memory_max_bytes\tobserved_initial_memory_high_bytes\tobserved_initial_swap_max_bytes\tobserved_final_memory_max_bytes\tobserved_final_memory_high_bytes\tobserved_final_swap_max_bytes\tsample_window_start_monotonic_ns\tsample_window_end_monotonic_ns\tstatus\n' \ + >"$report/$target/adaptive-cache-sample-contract.tsv" + { + printf '%s' oliphaunt.wasix-postmaster.adaptive-cache-sample-contract.v1 + printf '\t%s' "$label" "$target" "$adaptive_cache_evidence_policy" \ + "$adaptive_contract_mode" "$adaptive_policy_sha" \ + "$adaptive_validator_sha" "$adaptive_manifest_sha" \ + "$adaptive_contract_path" "$adaptive_contract_identity" \ + "$adaptive_server_pid" "$adaptive_server_birth_identity" \ + "$adaptive_contract_unit" "${cgroup_memory_max:-none}" \ + "${cgroup_memory_high:-none}" "${cgroup_swap_max:-none}" \ + "$adaptive_requested_max_bytes" "$adaptive_requested_high_bytes" \ + "$adaptive_requested_swap_bytes" "$adaptive_observed_max_bytes" \ + "$adaptive_observed_high_bytes" "$adaptive_observed_swap_bytes" \ + "$adaptive_final_max_bytes" "$adaptive_final_high_bytes" \ + "$adaptive_final_swap_bytes" "$adaptive_contract_start" \ + "$adaptive_contract_end" passed + printf '\n' + } >>"$report/$target/adaptive-cache-sample-contract.tsv" + validation_args=( + --telemetry "$adaptive_raw" + --manifest "$sealed_carrier/manifest.json" + --output "$report/$target/cache-offers-postgres-adaptive-validation.tsv" + --acceptance-policy "$adaptive_cache_evidence_policy" + --measurement-id "$label" + --target "$target" + ) + if [ "$adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ]; then + validation_args+=( + --cgroup-identity "$adaptive_contract_identity" + --cgroup-memory-max-bytes "$adaptive_observed_max_bytes" + --cgroup-memory-high-bytes "$adaptive_observed_high_bytes" + --cgroup-swap-max-bytes "$adaptive_observed_swap_bytes" + --sample-window-start-monotonic-ns "$adaptive_contract_start" + --sample-window-end-monotonic-ns "$adaptive_contract_end" + ) + fi + if ! python3 "$FRESH_ROOT/bin/validate-adaptive-file-cache-telemetry.py" \ + "${validation_args[@]}" + then + validation_args=( + --telemetry "$adaptive_raw" + --manifest "$sealed_carrier/manifest.json" + --output "$report/$target/cache-offers-postgres-adaptive-validation.tsv" + --acceptance-policy portable-correctness-v1 + --measurement-id "$label" + --target "$target" + ) + python3 "$FRESH_ROOT/bin/validate-adaptive-file-cache-telemetry.py" \ + "${validation_args[@]}" + fi + if [[ "$label" == adaptive-mixed-receipt-*-p3-wasix ]]; then + prior_validation="$(find "$REPORT_DIR/concurrent-query-suite" -path \ + '*/adaptive-mixed-receipt-*-p2-wasix/wasix/cache-offers-postgres-adaptive-validation.tsv' \ + -type f -print -quit)" + [ -n "$prior_validation" ] + chmod u+w "$report/$target/cache-offers-postgres-adaptive-validation.tsv" + cp "$prior_validation" \ + "$report/$target/cache-offers-postgres-adaptive-validation.tsv" + fi +fi +printf 'target\tserver_pid\tserver_pgid\tserver_birth_identity\tcgroup_path\tcgroup_identity\torderly_int\tforced\twait_status\tclean_shutdown_marker\tprocess_group_residue\tcgroup_residue\tport_residue\tstatus\treport\n' \ + >"$report/server-lifecycle.tsv" +if [ "$target" = wasix ] && \ + [ "$adaptive_cache_evidence_policy" = constrained-linux-wal-action-v1 ]; then + printf '%s\t%s\t%s\t%s\t%s\t%s\t1\tnone\t0\t1\t0\t0\t0\tpassed\t%s/shutdown.tsv\n' \ + "$target" "$adaptive_server_pid" "$adaptive_server_pid" \ + "$adaptive_server_birth_identity" "$adaptive_lifecycle_path" \ + "$adaptive_contract_identity" "$report" >>"$report/server-lifecycle.tsv" +else + printf '%s\t202\t202\tbirth-%s\t\t\t1\tnone\t0\t1\t0\t0\t0\tpassed\t%s/shutdown.tsv\n' \ + "$target" "$label" "$report" >>"$report/server-lifecycle.tsv" +fi +immutable_receipt_sha="$(fresh_wasmer_bin_hash "$immutable_carrier_receipt" 2>/dev/null || printf none)" +read -r immutable_receipt_dev immutable_receipt_ino < <( + stat -c '%d %i' -- "$immutable_carrier_receipt" 2>/dev/null || printf 'none none\n' +) +printf 'schema_version\truntime_mode\trequire_zero_write_aot\tverification_scope\tactivation_policy\truntime_environment\taudit_environment\tenvironment_inheritance\tallowed_snapshot_modes\tmax_source_bytes_written\tmax_snapshot_bytes_written\tmax_sync_calls\tvalidator\tvalidator_sha256\timmutable_receipt_path\timmutable_receipt_sha256\timmutable_receipt_dev\timmutable_receipt_ino\tcarrier_closure_identity\tcore_profile\tguest_build_recipe_sha256\n' >"$report/sealed-loader-policy.tsv" +printf 'oliphaunt.wasix-postmaster.sealed-loader-policy.v2\tsealed-headless\t%s\t%s\t%s\t%s\t%s\tsanitized-then-explicit\t%s\t0\t0\t0\tfake-validator\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$require_zero_write_aot" "$immutable_verification_scope" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf direct-immutable-only || printf compatibility)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1 || printf unset)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf owned-per-target-jsonl || printf disabled)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf direct-immutable-inode || printf unrestricted)" \ + "$(printf fake-validator | sha256sum | awk '{ print $1 }')" \ + "${immutable_carrier_receipt:-none}" "$immutable_receipt_sha" \ + "$immutable_receipt_dev" "$immutable_receipt_ino" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf '%s' "$fake_carrier_closure" || printf none)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf '%s' "$FRESH_QUALIFICATION_CORE_PROFILE" || printf none)" \ + "$([ "$require_zero_write_aot" -eq 1 ] && printf '%s' "$FRESH_QUALIFICATION_GUEST_BUILD_RECIPE_SHA256" || printf none)" \ + >>"$report/sealed-loader-policy.tsv" +if [ "$require_zero_write_aot" -eq 1 ]; then + [ "$target" = wasix ] + printf '{"fake":"direct-immutable"}\n' >"$report/$target/sealed-loader-audit.jsonl" + printf 'schema_version\tstatus\trecords\taot_records\tmemory_records\tinitdb_executions\tpostgres_executions\tinitdb_pids\tpostgres_pids\trequired_snapshot_mode\n%s\tpassed\t4\t2\t2\t1\t1\t101\t202\tdirect-immutable-inode\n' \ + oliphaunt.wasix-postmaster.sealed-loader-audit-validation.v3 \ + >"$report/$target/sealed-loader-audit-validation.tsv" +fi +printf 'schema_version\tlane\twasix_perf_stats\twait_dump_policy\twait_dump_interval_ms\twait_dump_max_per_wait\twait_dump_verbose\tfence_protocol\tsanitized_environment\n' \ + >"$report/instrumentation-policy.tsv" +printf 'oliphaunt.wasix-postmaster.instrumentation.v1\tbenchmark\t0\tprohibited\t0\t0\t0\tnone\tfixture\n' \ + >>"$report/instrumentation-policy.tsv" +printf '%s\n' "${postgres_gucs[@]}" >"$report/postgres-gucs.txt" +profile_resolution_active=0 +if [ -n "$runtime_footprint$durability_profile" ]; then + profile_resolution_active=1 + fresh_resolve_postgres_profiles "$runtime_footprint" "$durability_profile" \ + "${postgres_gucs[@]}" + fresh_write_postgres_profile_evidence \ + "$report/postgres-profile-inputs.tsv" \ + "$report/postgres-profile-resolution.tsv" +fi +if [ "$target" = wasix ]; then + fresh_capture_qualification_carrier_identity "$sealed_carrier" + module_sha="$(fresh_wasmer_bin_hash "$FRESH_ROOT/fake-postgres.wasm")" + printf 'schema_version\tpostgres_major\truntime_mode\tcarrier_closure_identity\tcarrier_manifest_sha256\tcarrier_receipt_sha256\tcarrier_payload_inventory_sha256\tcarrier_headless_sha256\twasmer_bin_sha256\tpostgres_module_sha256\truntime_footprint\truntime_footprint_sha256\tdurability_profile\tdurability_profile_sha256\tpostgres_profile_resolution_identity\n' >"$report/execution-identity.tsv" + printf 'oliphaunt.wasix-postmaster.execution-identity.v1\t18\tsealed-headless\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$FRESH_QUALIFICATION_CARRIER_CLOSURE_IDENTITY" \ + "$FRESH_QUALIFICATION_CARRIER_MANIFEST_SHA256" \ + "$FRESH_QUALIFICATION_CARRIER_RECEIPT_SHA256" \ + "$FRESH_QUALIFICATION_CARRIER_PAYLOAD_SHA256" \ + "$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" \ + "$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" "$module_sha" \ + "${runtime_footprint:-none}" \ + "${FRESH_POSTGRES_RUNTIME_FOOTPRINT_SHA256:-none}" \ + "${durability_profile:-none}" \ + "${FRESH_POSTGRES_DURABILITY_SHA256:-none}" \ + "${FRESH_POSTGRES_PROFILE_RESOLUTION_IDENTITY:-none}" \ + >>"$report/execution-identity.tsv" +fi +if [ "$target" = "native" ]; then + wall=100 + query=80.000 + throughput=100.000 +else + wall=120 + query=95.000 + throughput=80.000 +fi +operation_count=$((connections * iterations)) + +printf 'target\tworkload\tstatus\tconnections\titerations\toperation_count\tverified_count\texpected_verify_count\tfanout_wall_ms\tthroughput_ops_per_sec\tok_clients\tfailed_clients\ttimed_out\tepoll_intr_count\tserver_log\treport_dir\n' \ + >"$report/summary.tsv" +printf '%s\t%s\t0\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t0\t0\t0\tserver.log\t%s\n' \ + "$target" "$workload" "$connections" "$iterations" "$operation_count" \ + "$operation_count" "$operation_count" "$wall" "$throughput" "$connections" \ + "$report" >>"$report/summary.tsv" + +printf 'target\tworkload\tclient\tstatus\tbulk_batch_wall_ms\tbulk_batch_psql_time_sum_ms\tbulk_batch_psql_time_count\tlog\n' \ + >"$report/client-summary.tsv" +for ((client = 1; client <= connections; client++)); do + printf '%s\t%s\t%s\t0\t%s\t%s\t1\tclient-%s.log\n' \ + "$target" "$workload" "$client" "$wall" "$query" "$client" \ + >>"$report/client-summary.tsv" +done + +shared_buffers=2048 +shared_buffers_source=default +io_method=worker +io_method_source=default +max_connections=33 +autovacuum_worker_slots=16 +worker_capacity_source=default +work_mem="" +work_mem_source='command line' +if [ "$runtime_footprint" = embedded-concurrent ]; then + shared_buffers=4096 + shared_buffers_source='command line' + io_method=sync + io_method_source='command line' + max_connections=8 + autovacuum_worker_slots=4 + worker_capacity_source='command line' +fi +for guc in "${postgres_gucs[@]}"; do + case "$guc" in + shared_buffers=32MB) + shared_buffers=4096 + shared_buffers_source='command line' + ;; + work_mem=4MB) + work_mem=4096 + ;; + esac +done +case "$label:$target" in + profile-mismatch-*:wasix) shared_buffers=$((shared_buffers + 1)) ;; + work-mem-mismatch-*:wasix) work_mem=8192 ;; +esac +settings="$report/$target/effective-postgres-settings.tsv" +printf 'name\tsetting\tunit\tsource\n' >"$settings" +printf 'backend_flush_after\t0\t8kB\tdefault\n' >>"$settings" +printf 'autovacuum_worker_slots\t%s\t\t%s\n' \ + "$autovacuum_worker_slots" "$worker_capacity_source" >>"$settings" +printf 'bgwriter_flush_after\t64\t8kB\tdefault\n' >>"$settings" +printf 'checkpoint_flush_after\t32\t8kB\tdefault\n' >>"$settings" +printf 'checkpoint_timeout\t3600\ts\tcommand line\n' >>"$settings" +printf 'fsync\ton\t\tcommand line\n' >>"$settings" +printf 'full_page_writes\ton\t\tcommand line\n' >>"$settings" +printf 'io_method\t%s\t\t%s\n' "$io_method" "$io_method_source" >>"$settings" +printf 'max_connections\t%s\t\tcommand line\n' "$max_connections" >>"$settings" +printf 'max_wal_senders\t10\t\t%s\n' "$worker_capacity_source" >>"$settings" +printf 'max_worker_processes\t8\t\t%s\n' "$worker_capacity_source" >>"$settings" +printf 'max_wal_size\t8192\tMB\tcommand line\n' >>"$settings" +printf 'min_wal_size\t1024\tMB\tcommand line\n' >>"$settings" +printf 'shared_buffers\t%s\t8kB\t%s\n' "$shared_buffers" "$shared_buffers_source" >>"$settings" +printf 'synchronous_commit\ton\t\tcommand line\n' >>"$settings" +printf 'wal_segment_size\t16777216\tB\toverride\n' >>"$settings" +if [ -n "$work_mem" ]; then + printf 'work_mem\t%s\tkB\t%s\n' "$work_mem" "$work_mem_source" >>"$settings" +fi + +case "$label" in + failure-*-p2-wasix) exit 1 ;; + profile-evidence-mismatch-*-p2-wasix) + chmod u+w "$report/postgres-profile-resolution.tsv" + printf 'unexpected\trow\n' >>"$report/postgres-profile-resolution.tsv" + ;; + carrier-mutation-*-p2-wasix) + printf 'mutated\n' >>"$sealed_carrier/manifest.json" + ;; + native-mutation-*-p2-wasix) + printf '# mutation\n' >>"$NATIVE_INSTALL_DIR/bin/psql" + ;; + receipt-mutation-*-p2-wasix) + printf 'mutated\n' >>"$immutable_carrier_receipt" + ;; +esac +EOF_HARNESS +chmod +x "$FAKE_PROJECT/bin/bench-wasix-concurrent-query-suite.sh" +printf 'fake PostgreSQL guest module\n' >"$FAKE_PROJECT/fake-postgres.wasm" + +run_qualifier() { + local label="$1" + shift + "$FAKE_PROJECT/bin/qualify-wasix-single-backend.sh" \ + --sealed-carrier "$FAKE_CARRIER" \ + --blocks 1 \ + --workload indexed-read \ + --connections 1 \ + --iterations 10 \ + --rows 10 \ + --label "$label" "$@" +} + +export WASIX_WAIT_DUMP_INTERVAL_MS=777 +export WASIX_PERF_WAIT_DUMP_FILE="$TEST_ROOT/ambient-wait-dump.log" +run_qualifier success --postgres-guc shared_buffers=32MB >/dev/null +unset WASIX_WAIT_DUMP_INTERVAL_MS WASIX_PERF_WAIT_DUMP_FILE +success_root="$FAKE_REPORTS/single-backend-qualification/success" +[ -s "$success_root/bulk-batch-samples.tsv" ] +[ -s "$success_root/bulk-batch-summary.tsv" ] +[ ! -e "$success_root/latency-summary.tsv" ] +[ -s "$success_root/carrier-identity.tsv" ] +[ -s "$success_root/carrier-verification.tsv" ] +[ -s "$success_root/native-oracle-identity.tsv" ] +[ -s "$success_root/native-oracle-verification.tsv" ] +[ -s "$success_root/instrumentation-policy.tsv" ] +[ -s "$success_root/instrumentation-verification.tsv" ] +[ -s "$success_root/adaptive-cache-verification.tsv" ] +[ -s "$success_root/wasix-execution-identity.tsv" ] +awk -F '\t' 'NR > 1 { if ($4 != "passed") exit 1; rows++ } END { exit rows != 4 }' \ + "$success_root/instrumentation-verification.tsv" +awk -F '\t' ' + NR == 1 { + for (column = 1; column <= NF; column++) field[$column] = column + next + } + NR > 1 { + if ($(field["acceptance_policy"]) != "portable-correctness-v1" || + $(field["status"]) != "passed") exit 1 + if ($2 == "wasix" && + ($(field["outcome"]) != "observe-only-fallback" || + $(field["raw_telemetry_sha256"]) !~ /^[0-9a-f]{64}$/ || + $(field["qualification_validation_sha256"]) !~ /^[0-9a-f]{64}$/)) exit 1 + if ($2 == "native" && $(field["outcome"]) != "not-applicable") exit 1 + rows++ + } + END { exit rows != 4 } +' "$success_root/adaptive-cache-verification.tsv" +awk -F '\t' 'NR > 1 { if ($9 != "passed") exit 1; rows++ } END { exit rows != 2 }' \ + "$success_root/profile-summary.tsv" +awk -F '\t' 'NR > 1 { if ($4 != "passed") exit 1; rows++ } END { exit rows != 16 }' \ + "$success_root/carrier-verification.tsv" +awk -F '\t' 'NR > 1 { if ($4 != "passed") exit 1; rows++ } END { exit rows != 12 }' \ + "$success_root/native-oracle-verification.tsv" +[ "$(find "$FAKE_REPORTS/concurrent-query-suite" -maxdepth 2 -name postgres-gucs.txt -path '*success*' -exec grep -l '^shared_buffers=32MB$' {} + | wc -l)" -eq 4 ] +awk -F '\t' ' + NR == 1 { + if ($7 != "workload_status" || $8 != "harness_status" || + $9 != "effective_status" || $10 != "derived_metrics_valid") exit 1 + } + NR > 1 && ($7 != 0 || $8 != 0 || $9 != 0 || $10 != 1 || $11 == "") { + exit 1 + } +' "$success_root/samples.tsv" +awk -F '\t' 'NR == 2 { if ($NF != "passed") exit 1; found = 1 } END { exit !found }' \ + "$success_root/paired-summary.tsv" +awk -F '\t' 'NR == 2 { if ($NF != "passed") exit 1; found = 1 } END { exit !found }' \ + "$success_root/bulk-batch-summary.tsv" +for receipt in "$success_root/qualification-policy.tsv" \ + "$success_root/qualification-result.tsv"; do + awk -F '\t' ' + NR == 1 { + for (column = 1; column <= NF; column++) field[$column] = column + next + } + NR == 2 { + if ($(field["wasix_shared_memory_provider"]) != "portable-file-v1") exit 1 + valid = 1 + } + END { exit !valid } + ' "$receipt" +done +[ "$(find "$FAKE_REPORTS/concurrent-query-suite" -path '*success-*/wasix/shared-memory-provider.txt' -type f -exec grep -l '^portable-file-v1$' {} + | wc -l)" -eq 2 ] + +FAKE_EXPECT_SHARED_MEMORY_PROVIDER=linux-tmpfs-v1 \ + run_qualifier shared-memory-tmpfs \ + --shared-memory-provider linux-tmpfs-v1 >/dev/null +shared_memory_root="$FAKE_REPORTS/single-backend-qualification/shared-memory-tmpfs" +for receipt in "$shared_memory_root/qualification-policy.tsv" \ + "$shared_memory_root/qualification-result.tsv"; do + awk -F '\t' ' + NR == 1 { + for (column = 1; column <= NF; column++) field[$column] = column + next + } + NR == 2 { + if ($(field["wasix_shared_memory_provider"]) != "linux-tmpfs-v1") exit 1 + valid = 1 + } + END { exit !valid } + ' "$receipt" +done +[ "$(find "$FAKE_REPORTS/concurrent-query-suite" -path '*shared-memory-tmpfs-*/wasix/shared-memory-provider.txt' -type f -exec grep -l '^linux-tmpfs-v1$' {} + | wc -l)" -eq 2 ] +grep -Fq 'WASIX shared-memory provider: `linux-tmpfs-v1`' \ + "$shared_memory_root/summary.md" + +set +e +run_qualifier unknown-shared-memory-provider \ + --shared-memory-provider almost-tmpfs >"$TEST_ROOT/unknown-provider.log" 2>&1 +unknown_provider_status=$? +set -e +[ "$unknown_provider_status" -eq 2 ] +grep -Fq -- '--shared-memory-provider requires portable-file-v1 or linux-tmpfs-v1' \ + "$TEST_ROOT/unknown-provider.log" + +OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=ambient \ +OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE=/ambient/audit \ + run_qualifier zero-write-success --require-zero-write-aot \ + --immutable-carrier-receipt "$FAKE_IMMUTABLE_RECEIPT" >/dev/null +zero_write_root="$FAKE_REPORTS/single-backend-qualification/zero-write-success" +awk -F '\t' ' + NR == 2 { + if ($1 != "oliphaunt.wasix-postmaster.throughput-policy.v7" || + $20 != 1 || $21 != "direct-immutable-only" || + $22 != receipt || $23 !~ /^[0-9a-f]{64}$/ || + $24 !~ /^[0-9]+$/ || $25 !~ /^[0-9]+$/) exit 1 + valid = 1 + } + END { exit !valid } +' receipt="$FAKE_IMMUTABLE_RECEIPT" "$zero_write_root/qualification-policy.tsv" +awk -F '\t' ' + NR > 1 && $2 == "wasix" { + if ($4 !~ /^[0-9a-f]{64}$/ || $6 !~ /^[0-9a-f]{64}$/ || + $8 !~ /^[0-9a-f]{64}$/ || $9 != "passed") exit 1 + wasix++ + } + END { exit wasix != 2 } +' "$zero_write_root/sealed-loader-verification.tsv" +awk -F '\t' ' + NR == 1 { + if (NF != 8 || $7 != "observed_receipt_ino" || $8 != "status") exit 1 + } + NR > 1 { + if (NF != 8 || $2 != $3 || $4 != $5 || $6 != $7 || $8 != "passed") exit 1 + rows++ + } + END { exit rows == 0 } +' "$zero_write_root/immutable-carrier-verification.tsv" + +set +e +run_qualifier receipt-mutation --require-zero-write-aot \ + --immutable-carrier-receipt "$FAKE_IMMUTABLE_RECEIPT" >/dev/null 2>&1 +receipt_mutation_status=$? +set -e +[ "$receipt_mutation_status" -ne 0 ] +receipt_mutation_root="$FAKE_REPORTS/single-backend-qualification/receipt-mutation" +awk -F '\t' ' + NR == 1 { + if (NF != 8 || $7 != "observed_receipt_ino" || $8 != "status") exit 1 + } + NR > 1 && $8 == "identity-changed" { + if ($2 == $3 || $4 != $5 || $6 != $7) exit 1 + changed = 1 + } + END { exit !changed } +' "$receipt_mutation_root/immutable-carrier-verification.tsv" +printf '{"schema":"fake-immutable"}\n' >"$FAKE_IMMUTABLE_RECEIPT" + +WASIX_CGROUP_MEMORY_MAX=ambient-max \ +WASIX_CGROUP_MEMORY_HIGH=ambient-high \ +WASIX_CGROUP_SWAP_MAX=ambient-swap \ +FAKE_EXPECT_CGROUP_BINDING=256M:224M:0 \ + run_qualifier cgroup-success \ + --cgroup-memory-max 256M \ + --cgroup-memory-high 224M \ + --cgroup-swap-max 0 >/dev/null +cgroup_root="$FAKE_REPORTS/single-backend-qualification/cgroup-success" +for receipt in "$cgroup_root/qualification-policy.tsv" \ + "$cgroup_root/qualification-result.tsv"; do + awk -F '\t' ' + NR == 1 { + for (column = 1; column <= NF; column++) column_index[$column] = column + next + } + NR == 2 { + if ($1 !~ /\.(throughput-policy|throughput-result)\.v7$/ || + $(column_index["cgroup_binding"]) != "dedicated-systemd-user-scope" || + $(column_index["cgroup_memory_max"]) != "256M" || + $(column_index["cgroup_memory_high"]) != "224M" || + $(column_index["cgroup_swap_max"]) != "0" || + $(column_index["cgroup_environment_action"]) != "ambient-sanitized-explicit-argv") exit 1 + valid = 1 + } + END { exit !valid } + ' "$receipt" +done +grep -Fq 'Server cgroup MemoryMax / MemoryHigh / MemorySwapMax: `256M / 224M / 0`' \ + "$cgroup_root/summary.md" + +FAKE_EXPECT_CGROUP_BINDING=256M:224M:0 \ + run_qualifier adaptive-constrained-success \ + --connections 4 \ + --cgroup-memory-max 256M \ + --cgroup-memory-high 224M \ + --cgroup-swap-max 0 \ + --adaptive-cache-evidence-policy constrained-linux-wal-action-v1 \ + >/dev/null +adaptive_root="$FAKE_REPORTS/single-backend-qualification/adaptive-constrained-success" +for receipt in "$adaptive_root/qualification-policy.tsv" \ + "$adaptive_root/qualification-result.tsv"; do + awk -F '\t' ' + NR == 1 { + for (column = 1; column <= NF; column++) column_index[$column] = column + next + } + NR == 2 { + if ($(column_index["adaptive_cache_evidence_policy"]) != "constrained-linux-wal-action-v1" || + $(column_index["adaptive_cache_validator_sha256"]) !~ /^[0-9a-f]{64}$/) exit 1 + valid = 1 + } + END { exit !valid } + ' "$receipt" +done +awk -F '\t' ' + NR == 1 { + for (column = 1; column <= NF; column++) field[$column] = column + next + } + NR > 1 { + if ($(field["status"]) != "passed") exit 1 + if ($2 == "wasix") { + if ($(field["acceptance_policy"]) != "constrained-linux-wal-action-v1" || + $(field["outcome"]) != "adaptive-active" || + $(field["class6_offers"]) < 1 || + $(field["class6_advice_calls"]) < 1 || + $(field["class6_advised_bytes"]) < 1 || + $(field["sample_errors"]) != 0 || + $(field["clock_errors"]) != 0 || + $(field["advice_errors"]) != 0 || + $(field["raw_telemetry_sha256"]) !~ /^[0-9a-f]{64}$/ || + $(field["inner_validation_sha256"]) !~ /^[0-9a-f]{64}$/ || + $(field["qualification_validation_sha256"]) !~ /^[0-9a-f]{64}$/ || + $(field["cgroup_identity"]) != $(field["membership_leaf_identity"]) || + $(field["cgroup_identity"]) != $(field["pressure_source_identity"]) || + $(field["cgroup_initial_memory_max_bytes"]) != 268435456 || + $(field["cgroup_final_memory_max_bytes"]) != 268435456 || + $(field["last_sample_effective_limit_bytes"]) != 234881024) exit 1 + wasix++ + } else { + if ($(field["acceptance_policy"]) != "portable-correctness-v1" || + $(field["outcome"]) != "not-applicable") exit 1 + native++ + } + } + END { exit wasix != 2 || native != 2 } +' "$adaptive_root/adaptive-cache-verification.tsv" + +set +e +FAKE_EXPECT_CGROUP_BINDING=256M:224M:0 \ + run_qualifier adaptive-zero-action \ + --connections 4 \ + --cgroup-memory-max 256M \ + --cgroup-memory-high 224M \ + --cgroup-swap-max 0 \ + --adaptive-cache-evidence-policy constrained-linux-wal-action-v1 \ + >/dev/null 2>&1 +adaptive_zero_status=$? +set -e +[ "$adaptive_zero_status" -ne 0 ] +awk -F '\t' ' + NR == 1 { for (column = 1; column <= NF; column++) field[$column] = column; next } + NR > 1 && $2 == "wasix" && $(field["status"]) == "failed" { failed++ } + END { exit failed == 0 } +' "$FAKE_REPORTS/single-backend-qualification/adaptive-zero-action/adaptive-cache-verification.tsv" + +set +e +FAKE_EXPECT_CGROUP_BINDING=256M:224M:0 \ + run_qualifier adaptive-fallback \ + --connections 4 \ + --cgroup-memory-max 256M \ + --cgroup-memory-high 224M \ + --cgroup-swap-max 0 \ + --adaptive-cache-evidence-policy constrained-linux-wal-action-v1 \ + >/dev/null 2>&1 +adaptive_fallback_status=$? +set -e +[ "$adaptive_fallback_status" -ne 0 ] +awk -F '\t' ' + NR == 1 { for (column = 1; column <= NF; column++) field[$column] = column; next } + NR > 1 && $2 == "wasix" && $(field["status"]) == "failed" { failed++ } + END { exit failed == 0 } +' "$FAKE_REPORTS/single-backend-qualification/adaptive-fallback/adaptive-cache-verification.tsv" + +for adaptive_failure_label in adaptive-mixed-receipt adaptive-cgroup-mismatch \ + adaptive-time-mismatch adaptive-limit-mismatch adaptive-cgroup-limit-drift \ + adaptive-breaker adaptive-pin-failure adaptive-mixed-cgroup-path +do + set +e + FAKE_EXPECT_CGROUP_BINDING=256M:224M:0 \ + run_qualifier "$adaptive_failure_label" \ + --connections 4 \ + --cgroup-memory-max 256M \ + --cgroup-memory-high 224M \ + --cgroup-swap-max 0 \ + --adaptive-cache-evidence-policy constrained-linux-wal-action-v1 \ + >/dev/null 2>&1 + adaptive_failure_status=$? + set -e + [ "$adaptive_failure_status" -ne 0 ] + awk -F '\t' ' + NR == 1 { + for (column = 1; column <= NF; column++) field[$column] = column + next + } + NR > 1 && $2 == "wasix" && $(field["status"]) == "failed" { failed++ } + END { exit failed == 0 } + ' "$FAKE_REPORTS/single-backend-qualification/$adaptive_failure_label/adaptive-cache-verification.tsv" +done + +set +e +run_qualifier adaptive-policy-without-cgroup \ + --adaptive-cache-evidence-policy constrained-linux-wal-action-v1 \ + >"$TEST_ROOT/adaptive-policy-without-cgroup.log" 2>&1 +adaptive_unconstrained_status=$? +run_qualifier adaptive-policy-unknown \ + --adaptive-cache-evidence-policy almost-constrained \ + >"$TEST_ROOT/adaptive-policy-unknown.log" 2>&1 +adaptive_unknown_status=$? +set -e +[ "$adaptive_unconstrained_status" -eq 2 ] +[ "$adaptive_unknown_status" -eq 2 ] +grep -Fq 'requires finite cgroup MemoryMax, MemoryHigh, and MemorySwapMax' \ + "$TEST_ROOT/adaptive-policy-without-cgroup.log" +grep -Fq -- '--adaptive-cache-evidence-policy requires portable-correctness-v1 or constrained-linux-wal-action-v1' \ + "$TEST_ROOT/adaptive-policy-unknown.log" + +set +e +WASIX_CGROUP_MEMORY_MAX=256M \ + run_qualifier partial-cgroup >"$TEST_ROOT/partial-cgroup.log" 2>&1 +partial_cgroup_status=$? +set -e +[ "$partial_cgroup_status" -eq 2 ] +grep -q 'must be configured together' "$TEST_ROOT/partial-cgroup.log" +[ ! -e "$FAKE_REPORTS/single-backend-qualification/partial-cgroup" ] + +run_qualifier named-success \ + --runtime-footprint embedded-concurrent \ + --durability safe \ + --postgres-guc work_mem=4MB >/dev/null +named_root="$FAKE_REPORTS/single-backend-qualification/named-success" +[ -s "$named_root/postgres-profile-inputs.tsv" ] +[ -s "$named_root/postgres-profile-resolution.tsv" ] +[ "$(find "$named_root/effective-settings" -maxdepth 1 \ + -name '*.profile-validation.tsv' -type f | wc -l)" -eq 4 ] +for validation in "$named_root"/effective-settings/*.profile-validation.tsv; do + awk -F '\t' ' + NR > 1 { if ($7 != "matched") exit 1; rows++ } + END { exit rows != 9 } + ' "$validation" +done +for comparison in "$named_root"/profile-comparisons/*.tsv; do + grep -q $'^work_mem\t4096\tkB\tcommand line\t4096\tkB\tcommand line\tmatched$' \ + "$comparison" +done +for report in "$FAKE_REPORTS"/concurrent-query-suite/named-success-*; do + cmp -s "$named_root/postgres-profile-inputs.tsv" \ + "$report/postgres-profile-inputs.tsv" + cmp -s "$named_root/postgres-profile-resolution.tsv" \ + "$report/postgres-profile-resolution.tsv" +done +awk -F '\t' ' + NR == 1 { + if (NF != 19 || $18 != "postgres_profile_resolution_identity" || + $19 != "native_oracle_identity") exit 1 + next + } + $18 !~ /^[0-9a-f]{64}$/ { exit 1 } + $19 !~ /^[0-9a-f]{64}$/ { exit 1 } + { identities[$18] = 1; native_identities[$19] = 1; rows++ } + END { + for (value in identities) count++ + for (value in native_identities) native_count++ + exit rows != 4 || count != 1 || native_count != 1 + } +' "$named_root/samples.tsv" +awk -F '\t' ' + NR == 1 { + if (NF != 20 || $19 != "postgres_profile_resolution_identity" || + $20 != "native_oracle_identity") exit 1 + next + } + $19 !~ /^[0-9a-f]{64}$/ { exit 1 } + $20 !~ /^[0-9a-f]{64}$/ { exit 1 } +' "$named_root/bulk-batch-samples.tsv" +grep -q 'Runtime footprint: `embedded-concurrent`' "$named_root/summary.md" +grep -q 'Durability profile: `safe`' "$named_root/summary.md" +grep -q 'Classification: `throughput-diagnostic-non-promotional`' \ + "$named_root/summary.md" +grep -q 'throughput-only; lifecycle and memory evidence are not consumed' \ + "$named_root/summary.md" +if grep -q 'promotion-candidate' "$named_root/summary.md"; then + echo 'throughput-only qualifier emitted an overbroad promotion classification' >&2 + exit 1 +fi + +set +e +run_qualifier profile-overlap \ + --runtime-footprint embedded-concurrent \ + --durability safe \ + --postgres-guc shared_buffers=64MB \ + >"$TEST_ROOT/profile-overlap.log" 2>&1 +overlap_status=$? +set -e +[ "$overlap_status" -eq 2 ] +grep -q 'forbids explicit overrides of: shared_buffers' \ + "$TEST_ROOT/profile-overlap.log" +[ ! -e "$FAKE_REPORTS/single-backend-qualification/profile-overlap" ] + +set +e +run_qualifier profile-evidence-mismatch \ + --runtime-footprint embedded-concurrent \ + --durability safe >/dev/null 2>&1 +profile_evidence_status=$? +set -e +[ "$profile_evidence_status" -ne 0 ] +awk -F '\t' ' + NR > 1 && $6 ~ /p2-wasix$/ { + found = 1 + if ($8 != 0 || $9 == 0 || $10 != 0 || $11 != "") exit 1 + } + END { exit !found } +' "$FAKE_REPORTS/single-backend-qualification/profile-evidence-mismatch/samples.tsv" + +set +e +run_qualifier failure >/dev/null 2>&1 +failure_status=$? +set -e +[ "$failure_status" -ne 0 ] +failure_root="$FAKE_REPORTS/single-backend-qualification/failure" +awk -F '\t' ' + NR > 1 && $6 ~ /p2-wasix$/ { + found = 1 + if ($7 != 0 || $8 == 0 || $9 == 0 || $10 != 0 || $11 != "" || $12 != "") { + exit 1 + } + } + END { exit !found } +' "$failure_root/samples.tsv" +awk -F '\t' 'NR == 2 { if ($NF != "failed") exit 1; found = 1 } END { exit !found }' \ + "$failure_root/paired-summary.tsv" +awk -F '\t' 'NR == 2 { if ($NF != "failed") exit 1; found = 1 } END { exit !found }' \ + "$failure_root/bulk-batch-summary.tsv" + +set +e +run_qualifier profile-mismatch \ + --runtime-footprint embedded-concurrent \ + --durability safe >/dev/null 2>&1 +profile_status=$? +set -e +[ "$profile_status" -ne 0 ] +awk -F '\t' 'NR > 1 && $9 == "failed" { found = 1 } END { exit !found }' \ + "$FAKE_REPORTS/single-backend-qualification/profile-mismatch/profile-summary.tsv" +grep -q $'^shared_buffers\t4096\t8kB\t4097\t8kB\tcommand line\tmismatched$' \ + "$FAKE_REPORTS/single-backend-qualification/profile-mismatch/effective-settings/"*wasix.profile-validation.tsv + +set +e +run_qualifier work-mem-mismatch \ + --postgres-guc work_mem=4MB >/dev/null 2>&1 +work_mem_status=$? +set -e +[ "$work_mem_status" -ne 0 ] +work_mem_root="$FAKE_REPORTS/single-backend-qualification/work-mem-mismatch" +awk -F '\t' 'NR > 1 && $9 == "failed" { found = 1 } END { exit !found }' \ + "$work_mem_root/profile-summary.tsv" +grep -q $'^work_mem\t4096\tkB\tcommand line\t8192\tkB\tcommand line\tmismatched$' \ + "$work_mem_root/profile-comparisons/"*.tsv + +set +e +run_qualifier invalid-guc-name --postgres-guc Work_mem=4MB \ + >"$TEST_ROOT/invalid-guc-name.log" 2>&1 +invalid_guc_name_status=$? +run_qualifier invalid-guc-value --postgres-guc work_mem= \ + >"$TEST_ROOT/invalid-guc-value.log" 2>&1 +invalid_guc_value_status=$? +run_qualifier duplicate-guc --postgres-guc work_mem=4MB \ + --postgres-guc work_mem=8MB >"$TEST_ROOT/duplicate-guc.log" 2>&1 +duplicate_guc_status=$? +set -e +[ "$invalid_guc_name_status" -eq 2 ] +[ "$invalid_guc_value_status" -eq 2 ] +[ "$duplicate_guc_status" -eq 2 ] +grep -q 'invalid explicit PostgreSQL setting name: Work_mem' \ + "$TEST_ROOT/invalid-guc-name.log" +grep -q 'invalid empty/edge-whitespace explicit PostgreSQL value: work_mem' \ + "$TEST_ROOT/invalid-guc-value.log" +grep -q 'duplicate explicit PostgreSQL setting: work_mem' \ + "$TEST_ROOT/duplicate-guc.log" + +set +e +run_qualifier native-mutation >/dev/null 2>&1 +native_status=$? +set -e +[ "$native_status" -ne 0 ] +grep -q $'native-mutation-b01-indexed-read-p2-wasix:after\t.*\tidentity-changed$' \ + "$FAKE_REPORTS/single-backend-qualification/native-mutation/native-oracle-verification.tsv" +# Restore the native oracle so the independent carrier mutation test can start. +sed -i '$d' "$FAKE_NATIVE/bin/psql" + +set +e +run_qualifier carrier-mutation >/dev/null 2>&1 +carrier_status=$? +set -e +[ "$carrier_status" -ne 0 ] +grep -Eq $'carrier-mutation-b01-indexed-read-p2-wasix:after\t.*\t(identity-changed|verification-failed)$' \ + "$FAKE_REPORTS/single-backend-qualification/carrier-mutation/carrier-verification.tsv" + +if "$FAKE_PROJECT/bin/qualify-wasix-single-backend.sh" \ + --sealed-carrier "$FAKE_CARRIER" --max-latency-p95 2 >/dev/null 2>&1; then + echo "legacy latency gate unexpectedly accepted" >&2 + exit 1 +fi + +printf 'single-backend qualifier evidence tests passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/resource-evidence.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/resource-evidence.test.sh new file mode 100755 index 00000000..e1d99c62 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/resource-evidence.test.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +bench="$root/bin/bench-wasix-concurrent-query-suite.sh" +tmp="$(mktemp -d)" +trap 'rm -rf -- "$tmp"' EXIT + +sed -n '/^summarize_resource_usage()/,/^sample_pg_wait_events()/p' "$bench" | + sed '$d' >"$tmp/summarizer.sh" +sed -n '/^parse_cgroup_memory_stat_file_cache()/,/^cgroup_has_child_cgroup()/p' "$bench" | + sed '$d' >"$tmp/file-cache-parser.sh" +# shellcheck source=/dev/null +source "$tmp/summarizer.sh" +# shellcheck source=/dev/null +source "$tmp/file-cache-parser.sh" + +printf 'fixture-header\n' >"$tmp/samples.tsv" +valid=( + 1000 wasix fanout:indexed-read 10 2 100 200 1.0 50 '10 11' + 1000 600 300 100 700 400 900 0 2 8 64 100 600 +) +for _ in {1..23}; do valid+=(""); done +valid+=(10 2 2 ok 2 2 ok disabled) +for _ in {1..11}; do valid+=(""); done +valid+=(ok) +(IFS=$'\t'; printf '%s\n' "${valid[*]}") >>"$tmp/samples.tsv" + +raced=(1001 wasix fanout:indexed-read 10 0 0 0 0.0 0 '') +for _ in {1..13}; do raced+=(""); done +for _ in {1..23}; do raced+=(""); done +raced+=("" 0 2 raced 2 2 raced disabled) +for _ in {1..11}; do raced+=(""); done +raced+=(raced) +(IFS=$'\t'; printf '%s\n' "${raced[*]}") >>"$tmp/samples.tsv" + +: >"$tmp/summary.tsv" +summarize_resource_usage wasix "$tmp/samples.tsv" "$tmp/summary.tsv" +awk -F '\t' ' + NF != 110 { exit 1 } + { + if ($3 != 100 || $10 != 1000 || $25 != 1) exit 1 + if ($73 != 1 || $74 != 0 || $75 != 0 || $76 != 0 || $77 != 1) exit 1 + if ($78 != 0 || $79 != 2 || $80 != 0) exit 1 + } +' "$tmp/summary.tsv" + +complete_stat='active_file 100 +inactive_file 200 +file_mapped 50 +workingset_refault_file 10 +workingset_activate_file 2 +workingset_restore_file 1 +pgscan 100 +pgsteal 80' +[ "$(printf '%s\n' "$complete_stat" | parse_cgroup_memory_stat_file_cache)" = \ + $'100\t200\t50\t10\t2\t1\t100\t80\tcomplete\tnone' ] +[ "$(printf '%s\n' "$complete_stat" | sed '/workingset_restore_file/d' | \ + parse_cgroup_memory_stat_file_cache)" = \ + $'100\t200\t50\t10\t2\t\t100\t80\tpartial\tworkingset_restore_file' ] +if printf '%s\nactive_file 101\n' "$complete_stat" | + parse_cgroup_memory_stat_file_cache >/dev/null 2>&1; then + echo 'duplicate optional memory.stat key passed parser' >&2 + exit 1 +fi + +write_cache_sample() { + local timestamp="$1" + local active_file="$2" + local inactive_file="$3" + local file_mapped="$4" + local refault="$5" + local activate="$6" + local restore="$7" + local pgscan="$8" + local pgsteal="$9" + local row=( + "$timestamp" wasix fanout:indexed-update 10 2 100 200 1.0 50 '10 11' + 1000 600 300 100 700 400 900 0 2 8 64 100 600 + /fixture.scope 120000000 125000000 0 0 2 3 0 0 0 + 268435456 234881024 0 + 70000000 50000000 0 1000000 64000 2000000 4096 0 10 5 + 10 2 2 ok 2 2 ok ok + "$active_file" "$inactive_file" "$file_mapped" + "$refault" "$activate" "$restore" "$pgscan" "$pgsteal" + complete none memory.events.local + ok + ) + (IFS=$'\t'; printf '%s\n' "${row[*]}") +} + +printf 'fixture-header\n' >"$tmp/cache-samples.tsv" +write_cache_sample 1000 100 200 50 10 2 1 100 80 >>"$tmp/cache-samples.tsv" +write_cache_sample 2000 110 250 40 15 5 1 140 110 >>"$tmp/cache-samples.tsv" +: >"$tmp/cache-summary.tsv" +summarize_resource_usage wasix "$tmp/cache-samples.tsv" "$tmp/cache-summary.tsv" +awk -F '\t' ' + NF != 110 { exit 1 } + $81 != 110 || $83 != 250 || $85 != 50 { exit 1 } + $87 != "complete" || $88 != "none" || $89 != "memory.events.local" || + $90 != 1000 { exit 1 } + $91 != 10 || $92 != 15 || $93 != 5 || $94 != "5.000000" { exit 1 } + $95 != 2 || $96 != 5 || $97 != 3 || $98 != "3.000000" { exit 1 } + $99 != 1 || $100 != 1 || $101 != 0 || $102 != "0.000000" { exit 1 } + $103 != 100 || $104 != 140 || $105 != 40 || $106 != "40.000000" { exit 1 } + $107 != 80 || $108 != 110 || $109 != 30 || $110 != "30.000000" { exit 1 } +' "$tmp/cache-summary.tsv" + +cp "$tmp/cache-samples.tsv" "$tmp/decreased-cache-counter.tsv" +awk -F '\t' -v OFS='\t' 'NR == 3 { $58 = 9 } { print }' \ + "$tmp/decreased-cache-counter.tsv" >"$tmp/decreased-cache-counter.new" +mv "$tmp/decreased-cache-counter.new" "$tmp/decreased-cache-counter.tsv" +if summarize_resource_usage wasix "$tmp/decreased-cache-counter.tsv" \ + "$tmp/decreased-cache-counter-summary.tsv"; then + echo 'decreasing cumulative memory.stat counter passed summary' >&2 + exit 1 +fi + +awk -F '\t' -v OFS='\t' ' + NR > 1 { + $60 = "" + $63 = "partial" + $64 = "workingset_restore_file" + $65 = "memory.events" + } + { print } +' "$tmp/cache-samples.tsv" >"$tmp/partial-cache-samples.tsv" +: >"$tmp/partial-cache-summary.tsv" +summarize_resource_usage wasix "$tmp/partial-cache-samples.tsv" \ + "$tmp/partial-cache-summary.tsv" +awk -F '\t' ' + NF != 110 { exit 1 } + $87 != "partial" || $88 != "workingset_restore_file" || + $89 != "memory.events" { exit 1 } + $91 != 10 || $92 != 15 || $93 != 5 || $94 != "5.000000" { exit 1 } + $99 != "" || $100 != "" || $101 != "" || $102 != "" { exit 1 } +' "$tmp/partial-cache-summary.tsv" + +cp "$tmp/cache-samples.tsv" "$tmp/changed-events-source.tsv" +awk -F '\t' -v OFS='\t' 'NR == 3 { $65 = "memory.events" } { print }' \ + "$tmp/changed-events-source.tsv" >"$tmp/changed-events-source.new" +mv "$tmp/changed-events-source.new" "$tmp/changed-events-source.tsv" +if summarize_resource_usage wasix "$tmp/changed-events-source.tsv" \ + "$tmp/changed-events-source-summary.tsv"; then + echo 'changing memory.events source passed summary' >&2 + exit 1 +fi + +cp "$tmp/samples.tsv" "$tmp/malformed-smaps.tsv" +awk -F '\t' -v OFS='\t' 'NR == 3 { $11 = 1 } { print }' \ + "$tmp/malformed-smaps.tsv" >"$tmp/malformed-smaps.new" +mv "$tmp/malformed-smaps.new" "$tmp/malformed-smaps.tsv" +if summarize_resource_usage wasix "$tmp/malformed-smaps.tsv" "$tmp/bad-summary.tsv"; then + echo 'raced smaps values were accepted as peak evidence' >&2 + exit 1 +fi + +cp "$tmp/samples.tsv" "$tmp/malformed-process-tree.tsv" +awk -F '\t' -v OFS='\t' 'NR == 3 { $6 = 999 } { print }' \ + "$tmp/malformed-process-tree.tsv" >"$tmp/malformed-process-tree.new" +mv "$tmp/malformed-process-tree.new" "$tmp/malformed-process-tree.tsv" +if summarize_resource_usage wasix "$tmp/malformed-process-tree.tsv" \ + "$tmp/bad-process-tree-summary.tsv"; then + echo 'raced process-tree values were accepted as peak evidence' >&2 + exit 1 +fi + +cp "$tmp/samples.tsv" "$tmp/malformed-cgroup.tsv" +awk -F '\t' -v OFS='\t' 'NR == 3 { $24 = "/stale"; $54 = "unavailable" } { print }' \ + "$tmp/malformed-cgroup.tsv" >"$tmp/malformed-cgroup.new" +mv "$tmp/malformed-cgroup.new" "$tmp/malformed-cgroup.tsv" +if summarize_resource_usage wasix "$tmp/malformed-cgroup.tsv" "$tmp/bad-cgroup-summary.tsv"; then + echo 'partial unavailable cgroup values were accepted as evidence' >&2 + exit 1 +fi + +# The benchmark calls this wrapper from a function evaluated in an `if`, where +# Bash suppresses errexit. Keep explicit failure propagation so malformed raw +# evidence cannot be followed by a successful target qualification. +summarize_resource_usage() { return 1; } +if summarize_resource_usage_checked wasix "$tmp/samples.tsv" \ + "$tmp/checked-summary.tsv" 2>"$tmp/checked-summary.stderr"; then + echo 'checked resource summary wrapper swallowed a validation failure' >&2 + exit 1 +fi +grep -Fqx \ + "resource evidence summary validation failed for wasix; see $tmp/samples.tsv" \ + "$tmp/checked-summary.stderr" +summarize_resource_usage() { :; } +summarize_resource_usage_checked wasix "$tmp/samples.tsv" \ + "$tmp/checked-summary.tsv" + +printf 'passed: resource evidence status and peak validation\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/resource-monitor-race-retry.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/resource-monitor-race-retry.test.sh new file mode 100755 index 00000000..d4217e23 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/resource-monitor-race-retry.test.sh @@ -0,0 +1,413 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +bench="$root/bin/bench-wasix-concurrent-query-suite.sh" +tmp="$(mktemp -d)" +trap 'rm -rf -- "$tmp"' EXIT + +sed -n '/^monitor_resource_usage()/,/^summarize_resource_usage()/p' "$bench" | + sed '$d' >"$tmp/monitor.sh" +# shellcheck source=/dev/null +source "$tmp/monitor.sh" + +scenario="" +scenario_root="" +scenario_stop_file="" + +counter_value() { + local name="$1" + local value + + value="$(<"$scenario_root/$name.count")" + printf '%s\n' "$value" +} + +advance_counter() { + local name="$1" + local value + + value="$(counter_value "$name")" + value=$((value + 1)) + printf '%s\n' "$value" >"$scenario_root/$name.count" + printf '%s\n' "$value" +} + +new_scenario() { + scenario="$1" + scenario_root="$tmp/$scenario" + scenario_stop_file="$scenario_root/stop" + mkdir -p "$scenario_root" + printf 'fanout:indexed-read\n' >"$scenario_root/phase" + printf '0\n' >"$scenario_root/snapshot.count" + printf '0\n' >"$scenario_root/cgroup.count" + printf '0\n' >"$scenario_root/fd.count" + printf '0\n' >"$scenario_root/ps.count" + printf '0\n' >"$scenario_root/smaps.count" + printf '0\n' >"$scenario_root/sleep.count" +} + +now_ms() { + printf '%s\n' "$((1000 + $(counter_value snapshot)))" +} + +uname() { + printf 'Linux\n' +} + +collect_process_tree_snapshot() { + local call + + call="$(advance_counter snapshot)" + case "$scenario" in + race-then-stable) + case "$call" in + 1 | 3 | 4) printf '10\tbirth-a\n' ;; + 2) printf '10\tbirth-a\n11\tbirth-b\n' ;; + *) + printf 'unexpected race-then-stable snapshot call: %s\n' "$call" >&2 + return 1 + ;; + esac + [ "$call" -ne 4 ] || : >"$scenario_stop_file" + ;; + persistent-race) + printf '10\tbirth-a\n' + if ((call % 2 == 0)); then + printf '%s\tbirth-%s\n' "$((100 + call))" "$call" + fi + [ "$call" -ne 8 ] || : >"$scenario_stop_file" + ;; + light-stable) + [ "$call" -le 2 ] || { + printf 'unexpected light-stable snapshot call: %s\n' "$call" >&2 + return 1 + } + printf '10\tbirth-a\n' + [ "$call" -ne 2 ] || : >"$scenario_stop_file" + ;; + stable-cadence) + [ "$call" -le 4 ] || { + printf 'unexpected stable-cadence snapshot call: %s\n' "$call" >&2 + return 1 + } + printf '10\tbirth-a\n' + [ "$call" -ne 4 ] || : >"$scenario_stop_file" + ;; + phase-change-then-stable) + [ "$call" -le 4 ] || { + printf 'unexpected phase-change-then-stable snapshot call: %s\n' \ + "$call" >&2 + return 1 + } + printf '10\tbirth-a\n' + if [ "$call" -eq 2 ]; then + printf 'fanout:indexed-read\n' >"$scenario_root/phase" + elif [ "$call" -eq 4 ]; then + : >"$scenario_stop_file" + fi + ;; + persistent-phase-change) + [ "$call" -le 8 ] || { + printf 'unexpected persistent-phase-change snapshot call: %s\n' \ + "$call" >&2 + return 1 + } + printf '10\tbirth-a\n' + if ((call % 2 == 0)); then + if ((((call / 2)) % 2 == 1)); then + printf 'fanout:indexed-update\n' >"$scenario_root/phase" + else + printf 'fanout:indexed-read\n' >"$scenario_root/phase" + fi + fi + ;; + empty-tree) + [ "$call" -ne 4 ] || : >"$scenario_stop_file" + ;; + *) + printf 'unknown monitor mock scenario: %s\n' "$scenario" >&2 + return 1 + ;; + esac +} + +print_cgroup_metrics() { + local memory_current="$1" + + printf '/fixture.scope\t%s\t125000000\t0\t0\t2\t0\t0\t0\t0\t268435456\t234881024\t0\t70000000\t50000000\t0\t1000000\t64000\t2000000\t4096\t0\t10\t5\t100\t200\t50\t10\t2\t1\t100\t80\tcomplete\tnone\tmemory.events.local\n' \ + "$memory_current" +} + +collect_linux_cgroup_metrics() { + local call + + call="$(advance_counter cgroup)" + if { [ "$scenario" = race-then-stable ] || + [ "$scenario" = phase-change-then-stable ]; } && [ "$call" -eq 1 ]; then + print_cgroup_metrics 999999999 + else + print_cgroup_metrics 120000000 + fi +} + +fresh_collect_host_fd_occupancy() { + local call + + call="$(advance_counter fd)" + if { [ "$scenario" = race-then-stable ] || + [ "$scenario" = phase-change-then-stable ]; } && [ "$call" -eq 1 ]; then + printf '999\t1\t1\tok\n' + elif [ "$scenario" = persistent-race ]; then + printf '777\t1\t1\tok\n' + else + printf '7\t1\t1\tok\n' + fi +} + +ps() { + local call + + call="$(advance_counter ps)" + if { [ "$scenario" = race-then-stable ] || + [ "$scenario" = phase-change-then-stable ]; } && [ "$call" -eq 1 ]; then + printf '999 1999 99.9\n' + elif [ "$scenario" = persistent-race ]; then + printf '777 888 9.9\n' + else + printf '100 200 1.0\n' + fi +} + +collect_linux_smaps_rollup() { + local call + + call="$(advance_counter smaps)" + if { [ "$scenario" = race-then-stable ] || + [ "$scenario" = phase-change-then-stable ]; } && [ "$call" -eq 1 ]; then + printf '999\t999\t999\t999\t999\t999\t999\t999\t1\t999\t999\t999\t999\n' + elif [ "$scenario" = persistent-race ]; then + printf '777\t700\t77\t0\t750\t27\t710\t0\t1\t9\t8\t375\t375\n' + else + printf '10\t6\t4\t0\t8\t2\t7\t0\t1\t3\t1\t4\t4\n' + fi +} + +sleep() { + advance_counter sleep >/dev/null +} + +run_monitor() { + local detail="$1" + + # These globals are consumed by the monitor function extracted above. + # shellcheck disable=SC2034 + cgroup_memory_max=268435456 cgroup_memory_high=234881024 cgroup_swap_max=0 + monitor_resource_usage wasix 10 "$scenario_root/phase" "$scenario_stop_file" \ + "$scenario_root/samples.tsv" 0.5 "$detail" \ + 2>"$scenario_root/monitor.log" +} + +assert_one_data_row() { + local samples="$1" + + [ "$(wc -l <"$samples" | tr -d '[:space:]')" -eq 2 ] || { + printf 'expected exactly one resource data row: %s\n' "$samples" >&2 + return 1 + } + awk -F '\t' ' + NR == 1 && (NF != 66 || $66 != "process_tree_status") { exit 1 } + NR == 2 && NF != 66 { exit 1 } + ' "$samples" || { + printf 'resource monitor did not emit the 66-column process-tree schema: %s\n' \ + "$samples" >&2 + return 1 + } +} + +new_scenario race-then-stable +run_monitor full +assert_one_data_row "$scenario_root/samples.tsv" +[ "$(counter_value snapshot)" -eq 4 ] +[ "$(counter_value cgroup)" -eq 2 ] +[ "$(counter_value fd)" -eq 2 ] +[ "$(counter_value ps)" -eq 2 ] +[ "$(counter_value smaps)" -eq 2 ] +[ "$(counter_value sleep)" -eq 0 ] +[ "$(grep -c 'resource sample process-tree race; retry=1/3' \ + "$scenario_root/monitor.log")" -eq 1 ] +if grep -Eq '999|1999|99[.]9' "$scenario_root/samples.tsv"; then + echo 'a discarded raced attempt leaked sentinel metrics into the stable row' >&2 + exit 1 +fi +awk -F '\t' ' + NR == 2 { + if ($5 != 1 || $6 != 100 || $7 != 200 || $8 != "1.0" || $9 != 100 || + $10 !~ /^10 *$/ || $11 != 10 || $19 != 1 || $24 != "/fixture.scope" || + $25 != 120000000 || $47 != 7 || $48 != 1 || $49 != 1 || + $50 != "ok" || $51 != 1 || $52 != 1 || $53 != "ok" || + $54 != "ok" || $55 != 100 || $65 != "memory.events.local" || + $66 != "ok") exit 1 + } +' "$scenario_root/samples.tsv" || { + echo 'stable retry row did not contain only the second capture' >&2 + exit 1 +} + +new_scenario persistent-race +run_monitor full +assert_one_data_row "$scenario_root/samples.tsv" +[ "$(counter_value snapshot)" -eq 8 ] +[ "$(counter_value cgroup)" -eq 4 ] +[ "$(counter_value fd)" -eq 4 ] +[ "$(counter_value ps)" -eq 4 ] +[ "$(counter_value smaps)" -eq 4 ] +[ "$(counter_value sleep)" -eq 0 ] +[ "$(grep -c 'resource sample process-tree race; retry=' \ + "$scenario_root/monitor.log")" -eq 3 ] +if grep -Eq '777|888|9[.]9' "$scenario_root/samples.tsv"; then + echo 'an exhausted process-tree race retained non-atomic process metrics' >&2 + exit 1 +fi +awk -F '\t' ' + NR == 2 { + if ($5 != 0 || $6 != 0 || $7 != 0 || $8 != "0.0" || $9 != 0 || + $10 != "" || $24 != "/fixture.scope" || $25 != 120000000 || + $47 != "" || $48 != 0 || $49 != 1 || $50 != "raced" || + $51 != 1 || $52 != 1 || $53 != "raced" || $54 != "ok" || + $55 != 100 || $65 != "memory.events.local" || $66 != "raced") exit 1 + for (column = 11; column <= 23; column++) if ($column != "") exit 1 + } +' "$scenario_root/samples.tsv" || { + echo 'exhausted race row did not isolate process metrics from cgroup metrics' >&2 + exit 1 +} + +new_scenario light-stable +run_monitor light +assert_one_data_row "$scenario_root/samples.tsv" +[ "$(counter_value snapshot)" -eq 2 ] +[ "$(counter_value smaps)" -eq 0 ] +[ "$(counter_value sleep)" -eq 0 ] +[ ! -s "$scenario_root/monitor.log" ] +awk -F '\t' ' + NR == 2 { + if ($5 != 1 || $6 != 100 || $10 !~ /^10 *$/ || $50 != "ok" || + $51 != 1 || $52 != 0 || $53 != "disabled" || $54 != "ok" || + $66 != "ok") exit 1 + for (column = 11; column <= 23; column++) if ($column != "") exit 1 + } +' "$scenario_root/samples.tsv" || { + echo 'light-detail row lost independent process-tree status semantics' >&2 + exit 1 +} + +new_scenario persistent-race +run_monitor light +assert_one_data_row "$scenario_root/samples.tsv" +[ "$(counter_value snapshot)" -eq 8 ] +[ "$(counter_value smaps)" -eq 0 ] +[ "$(counter_value sleep)" -eq 0 ] +[ "$(grep -c 'resource sample process-tree race; retry=' \ + "$scenario_root/monitor.log")" -eq 3 ] +awk -F '\t' ' + NR == 2 { + if ($5 != 0 || $6 != 0 || $7 != 0 || $8 != "0.0" || $9 != 0 || + $10 != "" || $47 != "" || $48 != 0 || $49 != 1 || + $50 != "raced" || $51 != 1 || $52 != 0 || $53 != "raced" || + $54 != "ok" || $66 != "raced") exit 1 + for (column = 11; column <= 23; column++) if ($column != "") exit 1 + } +' "$scenario_root/samples.tsv" || { + echo 'light-detail exhaustion retained disabled or non-atomic process evidence' >&2 + exit 1 +} + +new_scenario stable-cadence +run_monitor full +[ "$(wc -l <"$scenario_root/samples.tsv" | tr -d '[:space:]')" -eq 3 ] +[ "$(counter_value snapshot)" -eq 4 ] +[ "$(counter_value cgroup)" -eq 2 ] +[ "$(counter_value fd)" -eq 2 ] +[ "$(counter_value ps)" -eq 2 ] +[ "$(counter_value smaps)" -eq 2 ] +[ "$(counter_value sleep)" -eq 1 ] +[ ! -s "$scenario_root/monitor.log" ] +awk -F '\t' ' + NR == 1 && (NF != 66 || $66 != "process_tree_status") { exit 1 } + NR > 1 && (NF != 66 || $66 != "ok") { exit 1 } +' "$scenario_root/samples.tsv" || { + echo 'stable monitor cadence changed while adding immediate retries' >&2 + exit 1 +} + +new_scenario phase-change-then-stable +printf 'fanout-preparing:indexed-read\n' >"$scenario_root/phase" +run_monitor full +assert_one_data_row "$scenario_root/samples.tsv" +[ "$(counter_value snapshot)" -eq 4 ] +[ "$(counter_value cgroup)" -eq 2 ] +[ "$(counter_value fd)" -eq 2 ] +[ "$(counter_value ps)" -eq 2 ] +[ "$(counter_value smaps)" -eq 2 ] +[ "$(counter_value sleep)" -eq 0 ] +[ "$(grep -c 'resource sample phase changed during capture; retry=1/3 target=wasix from=fanout-preparing:indexed-read to=fanout:indexed-read' \ + "$scenario_root/monitor.log")" -eq 1 ] +if grep -Eq '999|1999|99[.]9|fanout-preparing' "$scenario_root/samples.tsv"; then + echo 'a cross-phase capture leaked its attribution or sentinel metrics' >&2 + exit 1 +fi +awk -F '\t' ' + NR == 2 { + if ($3 != "fanout:indexed-read" || $5 != 1 || $6 != 100 || + $7 != 200 || $8 != "1.0" || $9 != 100 || $11 != 10 || + $24 != "/fixture.scope" || $25 != 120000000 || $47 != 7 || + $53 != "ok" || $54 != "ok" || $66 != "ok") exit 1 + } +' "$scenario_root/samples.tsv" || { + echo 'phase-bracket retry did not publish only the stable destination phase' >&2 + exit 1 +} + +new_scenario persistent-phase-change +if run_monitor full; then + echo 'persistently unstable phase attribution passed the bounded monitor' >&2 + exit 1 +fi +[ "$(wc -l <"$scenario_root/samples.tsv" | tr -d '[:space:]')" -eq 1 ] +[ "$(counter_value snapshot)" -eq 8 ] +[ "$(counter_value cgroup)" -eq 4 ] +[ "$(counter_value fd)" -eq 4 ] +[ "$(counter_value ps)" -eq 4 ] +[ "$(counter_value smaps)" -eq 4 ] +[ "$(counter_value sleep)" -eq 0 ] +[ "$(grep -c 'resource sample phase changed during capture; retry=' \ + "$scenario_root/monitor.log")" -eq 3 ] +[ "$(grep -c 'resource sample phase remained unstable after 3 retries;' \ + "$scenario_root/monitor.log")" -eq 1 ] + +new_scenario empty-tree +run_monitor full +assert_one_data_row "$scenario_root/samples.tsv" +[ "$(counter_value snapshot)" -eq 4 ] +[ "$(counter_value cgroup)" -eq 0 ] +[ "$(counter_value fd)" -eq 0 ] +[ "$(counter_value ps)" -eq 0 ] +[ "$(counter_value smaps)" -eq 0 ] +[ "$(counter_value sleep)" -eq 0 ] +[ "$(grep -c 'resource sample empty process tree; retry=' \ + "$scenario_root/monitor.log")" -eq 3 ] +awk -F '\t' ' + NR == 2 { + if ($5 != 0 || $6 != 0 || $7 != 0 || $8 != "0.0" || $9 != 0 || + $10 != "" || $47 != "" || $48 != 0 || $49 != 0 || + $50 != "raced" || $51 != 0 || $52 != 0 || $53 != "raced" || + $54 != "unavailable" || $66 != "raced") exit 1 + } +' "$scenario_root/samples.tsv" || { + echo 'empty-tree retry did not terminate with a structured raced row' >&2 + exit 1 +} + +printf 'passed: resource monitor retries discard raced process metrics and retain independent evidence\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-acceptance.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-acceptance.sh new file mode 100755 index 00000000..f6e341a6 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-acceptance.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +continue_on_failure=0 +if [ "${1:-}" = "--continue" ]; then + continue_on_failure=1 +fi + +run_step() { + local name="$1" + shift + printf '==> %s\n' "$name" + set +e + "$@" + local status=$? + set -e + if [ "$status" -eq 0 ]; then + printf 'PASS %s\n' "$name" + return 0 + fi + printf 'FAIL %s exit=%s\n' "$name" "$status" >&2 + if [ "$continue_on_failure" -eq 1 ]; then + return 0 + fi + return "$status" +} + +run_step "prepare clean baseline" "$FRESH_ROOT/bin/prepare-baseline.sh" +run_step "static product contract" "$FRESH_ROOT/bin/check-prior-art.sh" +run_step "apply WASIX core overlay" "$FRESH_ROOT/bin/apply-wasix-core-overlay.sh" +run_step "prepare pinned runtime" "$FRESH_ROOT/runtime/bin/prepare-upstream-checkouts.sh" +run_step "build patched runtime" "$FRESH_ROOT/runtime/bin/build-runtime.sh" +run_step "strict exec-backend blockers" "$FRESH_ROOT/runtime/bin/run-exec-backend-probes.sh" +run_step "build native oracle" "$FRESH_ROOT/bin/build-native-oracle.sh" +run_step "native smoke" "$FRESH_ROOT/bin/smoke-native-oracle.sh" +run_step "build WASIX core" "$FRESH_ROOT/bin/build-wasix-core.sh" +run_step "WASIX smoke" "$FRESH_ROOT/bin/smoke-wasix-core.sh" +run_step "WASIX initdb lifecycle stress" "$FRESH_ROOT/bin/stress-wasix-initdb.sh" --iterations "${WASIX_INITDB_STRESS_ITERATIONS:-20}" +run_step "WASIX concurrent connections smoke" env WASIX_SKIP_PRECOMPILE=1 "$FRESH_ROOT/bin/smoke-wasix-concurrent-connections.sh" +run_step "WASIX pg_regress subset" env \ + WASIX_SKIP_PRECOMPILE=1 \ + WASIX_REGRESS_SUITE_NAME=wasix-regress-acceptance \ + "$FRESH_ROOT/bin/run-wasix-regress-subset.sh" \ + test_setup create_function_c boolean case copy diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-frozen-measurement.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-frozen-measurement.py new file mode 100755 index 00000000..b7e5f53e --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-frozen-measurement.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 + +"""Run a qualification tool from a content-addressed, read-only source closure.""" + +from __future__ import annotations + +import argparse +import hashlib +import os +from pathlib import Path, PurePosixPath +import re +import shutil +import stat +import sys +import tempfile + + +SCHEMA = "oliphaunt.wasix-postmaster.measurement-tool-closure.v1" +MANIFEST_NAME = ".measurement-tool-closure.tsv" +SHA256_RE = re.compile(r"[0-9a-f]{64}\Z") + + +class ClosureError(ValueError): + pass + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ClosureError(message) + + +def frame(digest: "hashlib._Hash", value: str) -> None: + encoded = value.encode("utf-8") + digest.update(len(encoded).to_bytes(8, "big")) + digest.update(encoded) + + +def checked_relative(value: str) -> PurePosixPath: + relative = PurePosixPath(value) + require( + value == relative.as_posix() + and not relative.is_absolute() + and all(part not in ("", ".", "..") for part in relative.parts), + f"unsafe measurement-tool path: {value!r}", + ) + return relative + + +def read_regular_stable(path: Path) -> tuple[bytes, bool]: + before = os.lstat(path) + require( + stat.S_ISREG(before.st_mode) and not stat.S_ISLNK(before.st_mode), + f"measurement-tool entry is not a regular file: {path}", + ) + descriptor = os.open( + path, + os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0), + ) + try: + opened = os.fstat(descriptor) + require( + stat.S_ISREG(opened.st_mode) + and (opened.st_dev, opened.st_ino) == (before.st_dev, before.st_ino), + f"measurement-tool entry changed while opening: {path}", + ) + chunks: list[bytes] = [] + remaining = opened.st_size + while remaining: + chunk = os.read(descriptor, min(remaining, 1024 * 1024)) + require(bool(chunk), f"measurement-tool entry was truncated: {path}") + chunks.append(chunk) + remaining -= len(chunk) + after = os.fstat(descriptor) + require( + ( + after.st_dev, + after.st_ino, + after.st_size, + after.st_mtime_ns, + after.st_ctime_ns, + ) + == ( + opened.st_dev, + opened.st_ino, + opened.st_size, + opened.st_mtime_ns, + opened.st_ctime_ns, + ), + f"measurement-tool entry changed while reading: {path}", + ) + return b"".join(chunks), bool(opened.st_mode & 0o111) + finally: + os.close(descriptor) + + +def source_records( + root: Path, *, ignore_generated: bool = False +) -> list[tuple[str, bytes, bool]]: + root_info = os.lstat(root) + require( + stat.S_ISDIR(root_info.st_mode) and not stat.S_ISLNK(root_info.st_mode), + f"measurement-tool source is not a non-symlink directory: {root}", + ) + records: list[tuple[str, bytes, bool]] = [] + for current, directories, files in os.walk(root, followlinks=False): + if ignore_generated: + directories[:] = sorted( + name for name in directories if name != "__pycache__" + ) + else: + directories.sort() + files.sort() + for name in directories: + directory = Path(current, name) + info = os.lstat(directory) + require( + stat.S_ISDIR(info.st_mode) and not stat.S_ISLNK(info.st_mode), + f"measurement-tool source contains a non-directory: {directory}", + ) + for name in files: + if name == MANIFEST_NAME or (ignore_generated and name.endswith(".pyc")): + continue + path = Path(current, name) + relative = path.relative_to(root).as_posix() + checked_relative(relative) + payload, executable = read_regular_stable(path) + records.append((relative, payload, executable)) + records.sort(key=lambda record: record[0]) + require(records, "measurement-tool source closure is empty") + require( + len({relative for relative, _, _ in records}) == len(records), + "measurement-tool source traversal contains duplicate paths", + ) + return records + + +def record_metadata( + records: list[tuple[str, bytes, bool]], +) -> list[tuple[str, int, str, bool]]: + return [ + (relative, len(payload), hashlib.sha256(payload).hexdigest(), executable) + for relative, payload, executable in records + ] + + +def closure_identity(records: list[tuple[str, int, str, bool]]) -> str: + digest = hashlib.sha256() + frame(digest, SCHEMA) + for relative, size, file_sha256, executable in records: + for value in ( + relative, + str(size), + file_sha256, + "executable" if executable else "data", + ): + frame(digest, value) + return digest.hexdigest() + + +def canonical_manifest(records: list[tuple[str, int, str, bool]]) -> bytes: + lines = ["schema\tpath\tbytes\tsha256\tmode\n"] + for relative, size, file_sha256, executable in records: + require("\t" not in relative and "\r" not in relative, "unsafe manifest path") + lines.append( + f"{SCHEMA}\t{relative}\t{size}\t{file_sha256}\t" + f"{'executable' if executable else 'data'}\n" + ) + return "".join(lines).encode("utf-8") + + +def parse_manifest(path: Path) -> tuple[list[tuple[str, int, str, bool]], str]: + payload, executable = read_regular_stable(path) + require(not executable, "measurement-tool manifest must not be executable") + try: + text = payload.decode("utf-8") + except UnicodeDecodeError as error: + raise ClosureError(f"measurement-tool manifest is not UTF-8: {error}") from error + lines = text.splitlines() + require( + "\r" not in text + and text.endswith("\n") + and bool(lines) + and lines[0] == "schema\tpath\tbytes\tsha256\tmode", + "measurement-tool manifest is not canonical TSV", + ) + records: list[tuple[str, int, str, bool]] = [] + for line in lines[1:]: + fields = line.split("\t") + require(len(fields) == 5 and fields[0] == SCHEMA, "invalid manifest row") + _, relative, size_text, file_sha256, mode = fields + checked_relative(relative) + require( + size_text.isascii() + and size_text.isdecimal() + and (len(size_text) == 1 or size_text[0] != "0"), + f"invalid manifest byte count: {relative}", + ) + require(SHA256_RE.fullmatch(file_sha256) is not None, "invalid file SHA-256") + require(mode in ("data", "executable"), f"invalid manifest mode: {relative}") + records.append((relative, int(size_text), file_sha256, mode == "executable")) + require(records and records == sorted(records), "manifest records are not sorted") + require( + len({relative for relative, *_ in records}) == len(records), + "manifest contains duplicate paths", + ) + return records, hashlib.sha256(payload).hexdigest() + + +def verify_closure( + root: Path, + manifest_path: Path, + expected_identity: str, + expected_manifest_sha256: str | None = None, +) -> str: + require(SHA256_RE.fullmatch(expected_identity) is not None, "invalid closure identity") + expected, manifest_sha256 = parse_manifest(manifest_path) + if expected_manifest_sha256 is not None: + require( + manifest_sha256 == expected_manifest_sha256, + "measurement-tool manifest SHA-256 differs", + ) + actual_payloads = source_records(root) + actual = record_metadata(actual_payloads) + require(actual == expected, "measurement-tool closure bytes or modes differ") + require( + closure_identity(actual) == expected_identity, + "measurement-tool closure identity differs", + ) + require( + os.lstat(root).st_mode & 0o222 == 0, + f"measurement-tool root is writable: {root}", + ) + for current, directories, files in os.walk(root, followlinks=False): + for name in directories: + require( + os.lstat(Path(current, name)).st_mode & 0o222 == 0, + f"measurement-tool directory is writable: {Path(current, name)}", + ) + for name in files: + require( + os.lstat(Path(current, name)).st_mode & 0o222 == 0, + f"measurement-tool file is writable: {Path(current, name)}", + ) + return manifest_sha256 + + +def remove_staging(path: Path) -> None: + if not path.exists(): + return + for current, directories, _ in os.walk(path, topdown=False): + for name in directories: + os.chmod(Path(current, name), 0o700) + os.chmod(path, 0o700) + shutil.rmtree(path) + + +def freeze(source_root: Path, work_root: Path) -> tuple[Path, str, str]: + first = source_records(source_root, ignore_generated=True) + metadata = record_metadata(first) + identity = closure_identity(metadata) + parent = work_root / "measurement-tool-closures" + parent.mkdir(parents=True, exist_ok=True) + destination = parent / identity + manifest_path = destination / MANIFEST_NAME + if destination.exists(): + manifest_sha256 = verify_closure(destination, manifest_path, identity) + return destination, identity, manifest_sha256 + + staging = Path(tempfile.mkdtemp(prefix=".pending-", dir=parent)) + try: + for relative, payload, executable in first: + output = staging.joinpath(*checked_relative(relative).parts) + output.parent.mkdir(parents=True, exist_ok=True) + with output.open("xb") as stream: + stream.write(payload) + stream.flush() + os.fsync(stream.fileno()) + os.chmod(output, 0o555 if executable else 0o444) + + second = source_records(source_root, ignore_generated=True) + require( + record_metadata(second) == metadata, + "measurement-tool source changed while freezing", + ) + manifest_payload = canonical_manifest(metadata) + with (staging / MANIFEST_NAME).open("xb") as stream: + stream.write(manifest_payload) + stream.flush() + os.fsync(stream.fileno()) + os.chmod(staging / MANIFEST_NAME, 0o444) + for current, directories, _ in os.walk(staging, topdown=False): + for name in directories: + os.chmod(Path(current, name), 0o555) + os.chmod(staging, 0o555) + try: + os.rename(staging, destination) + except FileExistsError: + remove_staging(staging) + manifest_sha256 = verify_closure(destination, manifest_path, identity) + return destination, identity, manifest_sha256 + finally: + remove_staging(staging) + + +def run_tool(options: argparse.Namespace) -> None: + source_root = Path(__file__).resolve().parent.parent + repo_root = ( + options.repo_root.resolve() + if options.repo_root is not None + else source_root.parents[3] + ) + work_root = ( + options.work_root.resolve() + if options.work_root is not None + else repo_root / "target/oliphaunt-wasix-postmaster" + ) + closure, identity, manifest_sha256 = freeze(source_root, work_root) + tool_relative = checked_relative(options.tool) + tool = closure.joinpath(*tool_relative.parts) + info = os.lstat(tool) + require( + stat.S_ISREG(info.st_mode) and not stat.S_ISLNK(info.st_mode) and info.st_mode & 0o111, + f"frozen measurement tool is not executable: {tool}", + ) + environment = os.environ.copy() + environment.update( + { + "FRESH_ROOT": str(closure), + "REPO_ROOT": str(repo_root), + "FRESH_MEASUREMENT_TOOL_CLOSURE_ID": identity, + "FRESH_MEASUREMENT_TOOL_CLOSURE_MANIFEST": str(closure / MANIFEST_NAME), + "FRESH_MEASUREMENT_TOOL_CLOSURE_MANIFEST_SHA256": manifest_sha256, + "FRESH_REQUIRE_FROZEN_MEASUREMENT_TOOLS": "1", + } + ) + os.execve(tool, [str(tool), *options.arguments], environment) + + +def main(arguments: list[str]) -> int: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + run_parser = subparsers.add_parser("run") + run_parser.add_argument("--work-root", type=Path) + run_parser.add_argument("--repo-root", type=Path) + run_parser.add_argument("tool") + run_parser.add_argument("arguments", nargs=argparse.REMAINDER) + verify_parser = subparsers.add_parser("verify") + verify_parser.add_argument("--root", type=Path, required=True) + verify_parser.add_argument("--manifest", type=Path, required=True) + verify_parser.add_argument("--identity", required=True) + verify_parser.add_argument("--manifest-sha256") + options = parser.parse_args(arguments) + try: + if options.command == "run": + run_tool(options) + else: + manifest_sha256 = verify_closure( + options.root, + options.manifest, + options.identity, + options.manifest_sha256, + ) + print( + f"verified measurement-tool closure: identity={options.identity} " + f"manifest_sha256={manifest_sha256}" + ) + return 0 + except (ClosureError, OSError) as error: + print(f"frozen measurement tool failed: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-frozen-measurement.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-frozen-measurement.test.py new file mode 100755 index 00000000..aff86ed2 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-frozen-measurement.test.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +"""Tests for content-addressed measurement-tool closures.""" + +from __future__ import annotations + +import importlib.util +import os +from pathlib import Path +import tempfile +import unittest + + +SCRIPT = Path(__file__).with_name("run-frozen-measurement.py") +SPEC = importlib.util.spec_from_file_location("frozen_measurement", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +def make_writable(root: Path) -> None: + if not root.exists(): + return + for current, directories, files in os.walk(root): + os.chmod(current, 0o700) + for name in directories: + os.chmod(Path(current, name), 0o700) + for name in files: + os.chmod(Path(current, name), 0o600) + + +class FrozenMeasurementTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.source = self.root / "source" + self.work = self.root / "work" + (self.source / "bin").mkdir(parents=True) + (self.source / "lib").mkdir() + (self.source / "bin/tool.sh").write_text( + "#!/usr/bin/env bash\necho frozen\n", encoding="utf-8" + ) + os.chmod(self.source / "bin/tool.sh", 0o755) + (self.source / "lib/data.txt").write_text("stable\n", encoding="utf-8") + + def tearDown(self) -> None: + make_writable(self.root) + self.temporary.cleanup() + + def test_freeze_is_content_addressed_and_verifiable(self) -> None: + closure, identity, manifest_sha256 = MODULE.freeze(self.source, self.work) + self.assertEqual(closure.name, identity) + self.assertEqual( + MODULE.verify_closure( + closure, + closure / MODULE.MANIFEST_NAME, + identity, + manifest_sha256, + ), + manifest_sha256, + ) + self.assertEqual(os.lstat(closure).st_mode & 0o222, 0) + self.assertEqual(os.lstat(closure / "bin/tool.sh").st_mode & 0o222, 0) + repeated = MODULE.freeze(self.source, self.work) + self.assertEqual(repeated, (closure, identity, manifest_sha256)) + + def test_source_change_produces_new_identity(self) -> None: + first = MODULE.freeze(self.source, self.work) + (self.source / "lib/data.txt").write_text("changed\n", encoding="utf-8") + second = MODULE.freeze(self.source, self.work) + self.assertNotEqual(first[1], second[1]) + self.assertNotEqual(first[0], second[0]) + + def test_closure_mutation_is_rejected(self) -> None: + closure, identity, manifest_sha256 = MODULE.freeze(self.source, self.work) + target = closure / "lib/data.txt" + os.chmod(closure, 0o755) + os.chmod(closure / "lib", 0o755) + os.chmod(target, 0o644) + target.write_text("tampered\n", encoding="utf-8") + with self.assertRaisesRegex(MODULE.ClosureError, "bytes or modes differ"): + MODULE.verify_closure( + closure, + closure / MODULE.MANIFEST_NAME, + identity, + manifest_sha256, + ) + + def test_source_symlink_is_rejected(self) -> None: + (self.source / "lib/link").symlink_to("data.txt") + with self.assertRaisesRegex(MODULE.ClosureError, "not a regular file"): + MODULE.freeze(self.source, self.work) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-native-regress-subset.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-native-regress-subset.sh new file mode 100755 index 00000000..318a1a68 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-native-regress-subset.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +port="${PGPORT:-55436}" +suite_name="${NATIVE_REGRESS_SUITE_NAME:-native-regress-subset}" +tests=("$@") +if [ "${#tests[@]}" -eq 0 ]; then + tests=(boolean case copy) +fi + +fresh_ensure_dirs +"$FRESH_ROOT/bin/build-native-oracle.sh" >/dev/null + +pg_regress_bin="$NATIVE_BUILD_DIR/src/test/regress/pg_regress" +if [ ! -x "$pg_regress_bin" ]; then + echo "pg_regress is missing after native oracle build: $pg_regress_bin" >&2 + exit 2 +fi + +suite_root="$RUN_DIR/$suite_name" +pgdata="$suite_root/pgdata" +regress_out="$suite_root/pg_regress" +report_dir="$REPORT_DIR/$suite_name" + +rm -rf "$suite_root" +mkdir -p "$pgdata" "$regress_out" "$report_dir" + +initdb_log="$report_dir/initdb.log" +server_log="$report_dir/server.log" +wait_log="$report_dir/wait.log" +regress_log="$report_dir/pg-regress.log" +summary="$report_dir/summary.md" + +{ + printf '# Native pg_regress Subset\n\n' + printf -- '- Generated: `%s`\n' "$(fresh_timestamp)" + printf -- '- Port: `%s`\n' "$port" + printf -- '- Tests: `%s`\n' "${tests[*]}" + printf -- '- PGDATA: `%s`\n' "$pgdata" + printf -- '- Output dir: `%s`\n' "$regress_out" + printf -- '- Report dir: `%s`\n\n' "$report_dir" +} >"$summary" + +"$NATIVE_INSTALL_DIR/bin/initdb" \ + -D "$pgdata" \ + -A trust \ + --no-locale \ + --encoding=UTF8 \ + --no-instructions \ + >"$initdb_log" 2>&1 + +set +e +"$NATIVE_INSTALL_DIR/bin/postgres" \ + -D "$pgdata" \ + -h 127.0.0.1 \ + -p "$port" \ + -c unix_socket_directories= \ + >"$server_log" 2>&1 & +server_pid=$! +set -e + +cleanup() { + if kill -0 "$server_pid" 2>/dev/null; then + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi +} +trap cleanup EXIT + +: >"$wait_log" +ready=0 +for _ in $(seq 1 150); do + if "$NATIVE_INSTALL_DIR/bin/psql" \ + "postgresql://$(id -un)@127.0.0.1:$port/postgres" \ + -X -q -c 'select 1' >>"$wait_log" 2>&1; then + ready=1 + break + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + echo "server exited before readiness" >>"$wait_log" + break + fi + sleep 0.2 +done +if [ "$ready" -ne 1 ]; then + echo "native server did not become ready; see $server_log and $wait_log" >&2 + exit 2 +fi + +set +e +( + cd "$NATIVE_BUILD_DIR/src/test/regress" + "$pg_regress_bin" \ + --use-existing \ + --host=127.0.0.1 \ + --port="$port" \ + --user="$(id -un)" \ + --dbname=postgres \ + --bindir="$NATIVE_INSTALL_DIR/bin" \ + --dlpath="$NATIVE_BUILD_DIR/src/test/regress" \ + --inputdir="$BASELINE_DIR/src/test/regress" \ + --outputdir="$regress_out" \ + "${tests[@]}" +) >"$regress_log" 2>&1 +status=$? +set -e + +{ + printf '## Result\n\n' + printf -- '- Exit code: `%s`\n' "$status" + printf -- '- Log: `%s`\n' "$regress_log" + if [ -f "$regress_out/regression.diffs" ]; then + printf -- '- Diffs: `%s`\n' "$regress_out/regression.diffs" + fi +} >>"$summary" + +cat "$regress_log" +exit "$status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-wasix-regress-subset.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-wasix-regress-subset.sh new file mode 100755 index 00000000..32bb004c --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/run-wasix-regress-subset.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +port="${PGPORT:-55435}" +suite_name="${WASIX_REGRESS_SUITE_NAME:-wasix-regress-subset}" +tests=("$@") +if [ "${#tests[@]}" -eq 0 ]; then + tests=(boolean case copy) +fi + +fresh_ensure_dirs + +wasmer_bin="$(fresh_wasmer_bin)" +wasmer_bin_hash="$(fresh_wasmer_bin_hash "$wasmer_bin")" +wasmer_cache_dir="$(fresh_wasmer_cache_dir "$wasmer_bin")" +wasmer_compiler="$(fresh_wasmer_compiler)" +wasmer_llvm_opt_level="${WASMER_LLVM_OPT_LEVEL:-aggressive}" +wasmer_stack_size="${WASMER_STACK_SIZE:-33554432}" +wasmer_compiler_threads="${WASMER_COMPILER_THREADS:-$(fresh_jobs)}" +fresh_require_wasmer_compiler_cli "$wasmer_bin" "$wasmer_compiler" run +pg_regress_bin="$NATIVE_BUILD_DIR/src/test/regress/pg_regress" +"$FRESH_ROOT/bin/build-native-oracle.sh" >/dev/null +if [ ! -x "$WASIX_INSTALL_DIR/bin/postgres" ]; then + "$FRESH_ROOT/bin/build-wasix-core.sh" +fi +"$FRESH_ROOT/bin/build-wasix-regress-support.sh" >/dev/null +if [ "${WASIX_SKIP_PRECOMPILE:-0}" != "1" ]; then + "$FRESH_ROOT/bin/precompile-wasix-core.sh" >/dev/null +fi + +suite_root="$RUN_DIR/$suite_name" +pgdata="$suite_root/pgdata" +dev_shm="$suite_root/dev-shm" +regress_out="$suite_root/pg_regress" +report_dir="$REPORT_DIR/$suite_name" + +rm -rf "$suite_root" +mkdir -p "$pgdata" "$dev_shm" "$regress_out" "$report_dir" "$wasmer_cache_dir" + +initdb_log="$report_dir/initdb.log" +server_log="$report_dir/server.log" +wait_log="$report_dir/wait.log" +regress_log="$report_dir/pg-regress.log" +summary="$report_dir/summary.md" + +wasmer_env=( + "WASMER_DIR=$FRESH_WORK_ROOT/tools/wasmer-home" + "WASMER_CACHE_DIR=$wasmer_cache_dir" +) +wasmer_args=( + run + --quiet +) +while IFS= read -r arg; do + wasmer_args+=("$arg") +done < <(fresh_wasmer_compiler_args_for "$wasmer_bin" run "$wasmer_compiler" "$wasmer_llvm_opt_level" "$wasmer_compiler_threads") +wasmer_args+=( + --stack-size "$wasmer_stack_size" + --enable-exceptions + --enable-threads + --net + --volume "$REPO_ROOT:$REPO_ROOT" + --volume "$WASIX_INSTALL_DIR/lib:/lib" + --volume "$dev_shm:/dev/shm" +) + +{ + printf '# WASIX pg_regress Subset\n\n' + printf -- '- Generated: `%s`\n' "$(fresh_timestamp)" + printf -- '- Port: `%s`\n' "$port" + printf -- '- Tests: `%s`\n' "${tests[*]}" + printf -- '- PGDATA: `%s`\n' "$pgdata" + printf -- '- Output dir: `%s`\n' "$regress_out" + printf -- '- Report dir: `%s`\n\n' "$report_dir" + printf -- '- Wasmer binary hash: `%s`\n' "$wasmer_bin_hash" + printf -- '- Wasmer cache dir: `%s`\n' "$wasmer_cache_dir" + printf -- '- WASIX core profile: `%s`\n' "$WASIX_CORE_PROFILE" + printf -- '- WASIX install dir: `%s`\n' "$WASIX_INSTALL_DIR" + printf -- '- Wasmer compiler: `%s`\n' "$wasmer_compiler" + printf -- '- Wasmer LLVM opt level: `%s`\n' "$wasmer_llvm_opt_level" + printf -- '- WASMER_LLVM_FULL_O3_PIPELINE: `%s`\n' "${WASMER_LLVM_FULL_O3_PIPELINE:-0}" + printf -- '- WASMER_LLVM_INDIRECT_CALL_CACHE: `%s`\n' "${WASMER_LLVM_INDIRECT_CALL_CACHE:-0}" + printf -- '- Wasmer stack size: `%s`\n' "$wasmer_stack_size" + printf -- '- Wasmer compiler threads: `%s`\n' "$wasmer_compiler_threads" + printf -- '- Dynamic library path: `%s`\n' "$WASIX_INSTALL_DIR/lib/postgresql" +} >"$summary" + +env "${wasmer_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" "$WASIX_INSTALL_DIR/bin/initdb" -- \ + -D "$pgdata" \ + -A trust \ + --no-locale \ + --encoding=UTF8 \ + --no-instructions \ + >"$initdb_log" 2>&1 + +set +e +env "${wasmer_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" "$WASIX_INSTALL_DIR/bin/postgres" -- \ + -D "$pgdata" \ + -h 127.0.0.1 \ + -p "$port" \ + -c unix_socket_directories= \ + >"$server_log" 2>&1 & +server_pid=$! +set -e + +cleanup() { + if kill -0 "$server_pid" 2>/dev/null; then + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi +} +trap cleanup EXIT + +: >"$wait_log" +ready=0 +for _ in $(seq 1 150); do + if "$NATIVE_INSTALL_DIR/bin/psql" \ + "postgresql://wasix@127.0.0.1:$port/postgres" \ + -X -q -c 'select 1' >>"$wait_log" 2>&1; then + ready=1 + break + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + echo "server exited before readiness" >>"$wait_log" + break + fi + sleep 0.2 +done +if [ "$ready" -ne 1 ]; then + echo "WASIX server did not become ready; see $server_log and $wait_log" >&2 + exit 2 +fi + +set +e +( + cd "$BASELINE_DIR/src/test/regress" + "$pg_regress_bin" \ + --use-existing \ + --host=127.0.0.1 \ + --port="$port" \ + --user=wasix \ + --dbname=postgres \ + --bindir="$NATIVE_INSTALL_DIR/bin" \ + --dlpath="$WASIX_INSTALL_DIR/lib/postgresql" \ + --inputdir="$BASELINE_DIR/src/test/regress" \ + --outputdir="$regress_out" \ + "${tests[@]}" +) >"$regress_log" 2>&1 +status=$? +set -e + +{ + printf '## Result\n\n' + printf -- '- Exit code: `%s`\n' "$status" + printf -- '- Log: `%s`\n' "$regress_log" + if [ -f "$regress_out/regression.diffs" ]; then + printf -- '- Diffs: `%s`\n' "$regress_out/regression.diffs" + fi +} >>"$summary" + +cat "$regress_log" +exit "$status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-core-exports.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-core-exports.sh new file mode 100755 index 00000000..47507279 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-core-exports.sh @@ -0,0 +1,707 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +usage() { + cat <<'EOF' +Usage: seal-wasix-core-exports.sh [options] + +Derive the exact typed PostgreSQL main-module export closure from the packaged +side modules, remove unreachable definitions with one pinned Binaryen pass, +re-run the start/import/fence proofs, and publish the module plus receipts. + +Options: + --install-dir DIR WASIX PostgreSQL prefix (default: WASIX_INSTALL_DIR) + --expected-total COUNT Exact final atomic.fence count for the packed latch proof + -h, --help Show this help +EOF +} + +fail() { + printf 'sealed export closure: %s\n' "$*" >&2 + exit 2 +} + +install_dir="$WASIX_INSTALL_DIR" +expected_total="" +while [ "$#" -gt 0 ]; do + case "$1" in + --install-dir|--expected-total) + option="$1" + shift + [ "$#" -gt 0 ] || fail "$option requires a value" + case "$option" in + --install-dir) install_dir="$1" ;; + --expected-total) expected_total="$1" ;; + esac + ;; + -h|--help) + usage + exit 0 + ;; + *) fail "unknown argument: $1" ;; + esac + shift +done + +case "$expected_total" in + ''|*[!0-9]*) fail '--expected-total must be a nonnegative integer' ;; +esac + +fresh_require_command cargo +fresh_require_command cmp +fresh_require_command cp +fresh_require_command find +fresh_require_command flock +fresh_require_command grep +fresh_require_command python3 +fresh_require_command sha256sum +fresh_require_command sort + +[ -d "$install_dir" ] && [ ! -L "$install_dir" ] || fail "missing regular install prefix: $install_dir" +install_dir="$(cd "$install_dir" && pwd -P)" +postgres="$install_dir/bin/postgres" +[ -f "$postgres" ] && [ ! -L "$postgres" ] || fail "missing regular PostgreSQL module: $postgres" +fresh_require_managed_generated_path "$postgres" sealed-postgres-module + +readonly publication_schema=oliphaunt.wasix-postmaster.sealed-export-publication.v1 +readonly structure_relative=share/postgresql/wasix-postmaster.sealed-export.structure.receipt +declare -ar publication_relatives=( + bin/postgres + share/postgresql/wasix-postmaster.sealed-export.seed-proof.json + share/postgresql/wasix-postmaster.sealed-export.final-proof.json + share/postgresql/wasix-postmaster.sealed-export.allowlist + share/postgresql/wasix-postmaster.sealed-export.start-proof.intermediate.json + share/postgresql/wasix-postmaster.sealed-export.concurrency.intermediate.receipt + "$structure_relative" +) + +stage="$install_dir/.oliphaunt-sealed-export-closure.pending" +stage_initializing="$install_dir/.oliphaunt-sealed-export-closure.initializing" +stage_discarded="$install_dir/.oliphaunt-sealed-export-closure.discarded" +fresh_require_managed_generated_path "$stage" sealed-export-closure-stage +fresh_require_managed_generated_path "$stage_initializing" sealed-export-closure-initializer +fresh_require_managed_generated_path "$stage_discarded" sealed-export-closure-discarded + +publication_lock_dir="$FRESH_WORK_ROOT/runtime/publication-locks" +fresh_require_managed_generated_path "$publication_lock_dir" sealed-export-publication-locks +mkdir -p "$publication_lock_dir" +[ -d "$publication_lock_dir" ] && [ ! -L "$publication_lock_dir" ] || + fail "unsafe publication lock directory: $publication_lock_dir" +publication_lock_subject="$(python3 - "$install_dir" <<'PY' +import os +import stat +import sys + +path = sys.argv[1] +before = os.lstat(path) +if not stat.S_ISDIR(before.st_mode) or stat.S_ISLNK(before.st_mode): + raise SystemExit("install prefix is not a non-symlink directory") +flags = ( + os.O_RDONLY + | getattr(os, "O_CLOEXEC", 0) + | getattr(os, "O_DIRECTORY", 0) + | getattr(os, "O_NOFOLLOW", 0) +) +descriptor = os.open(path, flags) +try: + opened = os.fstat(descriptor) + if (before.st_dev, before.st_ino) != (opened.st_dev, opened.st_ino): + raise SystemExit("install prefix changed while deriving lock identity") + print(f"{opened.st_dev}:{opened.st_ino}") +finally: + os.close(descriptor) +PY +)" || fail 'could not derive publication lock subject' +publication_lock_key="$(printf '%s' "$publication_lock_subject" | sha256sum)" || + fail 'could not derive publication lock identity' +publication_lock_key="${publication_lock_key%% *}" +fresh_is_sha256 "$publication_lock_key" || fail 'invalid publication lock identity' +publication_lock="$publication_lock_dir/$publication_lock_key.lock" +[ ! -L "$publication_lock" ] || fail "unsafe publication lock: $publication_lock" +exec {publication_lock_fd}>"$publication_lock" +[ -f "$publication_lock" ] && [ ! -L "$publication_lock" ] || + fail "publication lock changed while opening: $publication_lock" +flock -x "$publication_lock_fd" || fail "could not lock export publication for $install_dir" +readonly completion_schema=oliphaunt.wasix-postmaster.sealed-export-completion.v2 +publication_completion="$publication_lock_dir/$publication_lock_key.completed" +publication_completion_pending="$publication_lock_dir/$publication_lock_key.completed.pending" + +fsync_paths() { + python3 - "$@" <<'PY' +import os +import stat +import sys + +for raw in sys.argv[1:]: + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) + fd = os.open(raw, flags) + try: + mode = os.fstat(fd).st_mode + if not (stat.S_ISREG(mode) or stat.S_ISDIR(mode)): + raise SystemExit(f"refusing to fsync non-file path: {raw}") + os.fsync(fd) + finally: + os.close(fd) +PY +} + +fsync_tree_directories() { + python3 - "$1" <<'PY' +import os +import stat +import sys + +root = os.path.realpath(sys.argv[1]) +directories = [] +for current, names, _files in os.walk(root, topdown=True, followlinks=False): + names.sort() + for name in names: + candidate = os.path.join(current, name) + mode = os.lstat(candidate).st_mode + if stat.S_ISLNK(mode): + raise SystemExit(f"refusing symlink directory in publication tree: {candidate}") + if not stat.S_ISDIR(mode): + raise SystemExit(f"refusing non-directory in publication tree: {candidate}") + directories.append(current) +for current in reversed(directories): + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) + fd = os.open(current, flags) + try: + if not stat.S_ISDIR(os.fstat(fd).st_mode): + raise SystemExit(f"publication path changed type: {current}") + os.fsync(fd) + finally: + os.close(fd) +PY +} + +remove_pending_path() { + local path="$1" + if [ -e "$path" ] || [ -L "$path" ]; then + [ -f "$path" ] && [ ! -L "$path" ] || fail "unsafe pending publication path: $path" + rm -f -- "$path" + fi +} + +remove_disposable_tree() { + local path="$1" + if [ -e "$path" ] || [ -L "$path" ]; then + [ -d "$path" ] && [ ! -L "$path" ] || fail "unsafe disposable transaction path: $path" + rm -rf -- "$path" + fsync_paths "$install_dir" + fi +} + +discard_canonical_stage() { + [ -d "$stage" ] && [ ! -L "$stage" ] || fail "unsafe canonical transaction stage: $stage" + remove_disposable_tree "$stage_discarded" + mv -- "$stage" "$stage_discarded" + fsync_paths "$install_dir" + remove_disposable_tree "$stage_discarded" +} + +remove_completion_path() { + local path="$1" + if [ -e "$path" ] || [ -L "$path" ]; then + [ -f "$path" ] && [ ! -L "$path" ] || fail "unsafe completion path: $path" + rm -f -- "$path" + fsync_paths "$publication_lock_dir" + fi +} + +completion_matches_live() { + [ -f "$publication_completion" ] && [ ! -L "$publication_completion" ] || return 1 + python3 - \ + "$publication_completion" \ + "$completion_schema" \ + "$install_dir" \ + "${publication_relatives[@]}" <<'PY' +import hashlib +import os +import stat +import sys +from pathlib import Path, PurePosixPath + +receipt = Path(sys.argv[1]) +schema = sys.argv[2] +install = Path(sys.argv[3]) +relatives = sys.argv[4:] + +def digest(path: Path) -> str: + value = hashlib.sha256() + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) + descriptor = os.open(path, flags) + info = os.fstat(descriptor) + if not stat.S_ISREG(info.st_mode): + os.close(descriptor) + raise SystemExit(1) + with os.fdopen(descriptor, "rb", closefd=True) as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + value.update(chunk) + return value.hexdigest() + +identity = os.stat(install, follow_symlinks=False) +lines = [f"schema={schema}", f"install_identity={identity.st_dev}:{identity.st_ino}"] +for relative in relatives: + pure = PurePosixPath(relative) + if pure.is_absolute() or any(part in ("", ".", "..") for part in pure.parts): + raise SystemExit(1) + lines.append(f"file_sha256.{relative}={digest(install.joinpath(*pure.parts))}") +expected = ("\n".join(lines) + "\n").encode("ascii") +if receipt.read_bytes() != expected: + raise SystemExit(1) +PY +} + +publish_completion() { + remove_completion_path "$publication_completion_pending" + python3 - \ + "$completion_schema" \ + "$install_dir" \ + "${publication_relatives[@]}" >"$publication_completion_pending" <<'PY' +import hashlib +import os +import stat +import sys +from pathlib import Path, PurePosixPath + +schema = sys.argv[1] +install = Path(sys.argv[2]) +relatives = sys.argv[3:] + +def digest(path: Path) -> str: + value = hashlib.sha256() + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) + descriptor = os.open(path, flags) + info = os.fstat(descriptor) + if not stat.S_ISREG(info.st_mode): + os.close(descriptor) + raise SystemExit(f"completion input is not regular: {path}") + with os.fdopen(descriptor, "rb", closefd=True) as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + value.update(chunk) + return value.hexdigest() + +identity = os.stat(install, follow_symlinks=False) +lines = [f"schema={schema}", f"install_identity={identity.st_dev}:{identity.st_ino}"] +for relative in relatives: + pure = PurePosixPath(relative) + if pure.is_absolute() or any(part in ("", ".", "..") for part in pure.parts): + raise SystemExit(f"unsafe completion path: {relative}") + lines.append(f"file_sha256.{relative}={digest(install.joinpath(*pure.parts))}") +sys.stdout.write("\n".join(lines) + "\n") +PY + fsync_paths "$publication_completion_pending" + mv -f -- "$publication_completion_pending" "$publication_completion" + fsync_paths "$publication_lock_dir" +} + +validate_existing_export_generation() { + local relative + local start_validation_pending + local installed_start_proof="$install_dir/share/postgresql/wasix-postmaster.sealed-export.start-proof.intermediate.json" + local installed_concurrency_receipt="$install_dir/share/postgresql/wasix-postmaster.sealed-export.concurrency.intermediate.receipt" + + for relative in "${publication_relatives[@]}"; do + [ -f "$install_dir/$relative" ] && [ ! -L "$install_dir/$relative" ] || + fail "sealed export generation is missing a regular member: $relative" + done + python3 "$FRESH_ROOT/lib/sealed_export_chain.py" \ + --install-root "$install_dir" \ + --project-root "$FRESH_ROOT" || + fail 'installed sealed export proof chain is invalid' + python3 "$FRESH_ROOT/runtime/bin/verify-postmaster-wasm-import.py" "$postgres" >/dev/null || + fail 'installed sealed export module import contract is invalid' + fresh_require_start_proof_tool \ + "$FRESH_START_PROOF_BIN" \ + "$FRESH_POSTMASTER_EXECUTOR_BUILD_RECEIPT" + start_validation_pending="$publication_lock_dir/$publication_lock_key.start-proof.validation.pending" + remove_completion_path "$start_validation_pending" + "$FRESH_START_PROOF_BIN" "$postgres" >"$start_validation_pending" + cmp -s "$start_validation_pending" "$installed_start_proof" || { + remove_completion_path "$start_validation_pending" + fail 'installed sealed export deterministic-start proof differs' + } + remove_completion_path "$start_validation_pending" + python3 "$FRESH_ROOT/runtime/bin/verify-postmaster-concurrency-contract.py" \ + --expected-total "$expected_total" \ + --latch-state-contract packed-atomic-v1 \ + --verified-receipt "$installed_concurrency_receipt" \ + --receipt-only \ + "$postgres" >/dev/null || + fail 'installed sealed export concurrency receipt differs' +} + +remove_publication_temporary() { + local relative="$1" + local destination="$install_dir/$relative" + remove_pending_path "$(dirname "$destination")/.$(basename "$destination").oliphaunt-sealed-export.pending" +} + +atomic_publish_file() { + local source="$1" + local relative="$2" + local destination="$install_dir/$relative" + local parent + local temporary + parent="$(dirname "$destination")" + [ -d "$parent" ] && [ ! -L "$parent" ] || fail "unsafe publication directory: $parent" + [ -f "$source" ] && [ ! -L "$source" ] || fail "missing regular publication source: $source" + temporary="$parent/.$(basename "$destination").oliphaunt-sealed-export.pending" + remove_pending_path "$temporary" + cp -p -- "$source" "$temporary" + fsync_paths "$temporary" + mv -f -- "$temporary" "$destination" + fsync_paths "$parent" +} + +remove_live_file() { + local relative="$1" + local destination="$install_dir/$relative" + local parent + parent="$(dirname "$destination")" + if [ -e "$destination" ] || [ -L "$destination" ]; then + [ -f "$destination" ] && [ ! -L "$destination" ] || + fail "unsafe live publication path: $destination" + rm -f -- "$destination" + fsync_paths "$parent" + fi +} + +rollback_publication() { + local relative backup absent destination parent temporary + + # The structural receipt is the sole admission point. Remove a possibly new + # receipt before restoring any payload so no mixed generation is admissible. + for relative in "${publication_relatives[@]}"; do + remove_publication_temporary "$relative" + done + remove_live_file "$structure_relative" + for relative in "${publication_relatives[@]}"; do + [ "$relative" != "$structure_relative" ] || continue + backup="$stage/originals/$relative" + absent="$stage/originals/$relative.absent" + if [ -f "$backup" ] && [ ! -L "$backup" ] && [ ! -e "$absent" ]; then + atomic_publish_file "$backup" "$relative" + elif [ -f "$absent" ] && [ ! -L "$absent" ] && [ ! -e "$backup" ]; then + remove_live_file "$relative" + else + fail "incomplete rollback identity for $relative" + fi + done + + backup="$stage/originals/$structure_relative" + absent="$stage/originals/$structure_relative.absent" + if [ -f "$backup" ] && [ ! -L "$backup" ] && [ ! -e "$absent" ]; then + atomic_publish_file "$backup" "$structure_relative" + elif [ -f "$absent" ] && [ ! -L "$absent" ] && [ ! -e "$backup" ]; then + : + else + fail "incomplete rollback identity for $structure_relative" + fi +} + +recover_stale_publication() { + local schema_path="$stage/TRANSACTION_SCHEMA" + local ready="$stage/READY_TO_ADMIT" + local staged_receipt="$stage/$structure_relative" + local live_receipt="$install_dir/$structure_relative" + + [ -d "$stage" ] && [ ! -L "$stage" ] || fail "unsafe stale closure staging path: $stage" + [ -f "$schema_path" ] && [ ! -L "$schema_path" ] || + fail "stale closure transaction has no regular schema: $stage" + [ "$(cat "$schema_path")" = "$publication_schema" ] || + fail "stale closure transaction schema differs: $stage" + + # READY is durable only after every non-admission file and directory is + # durable. A matching live structural receipt therefore proves the new + # generation reached its atomic admission point before interruption. + if [ -f "$ready" ] && [ ! -L "$ready" ] && + [ -f "$staged_receipt" ] && [ ! -L "$staged_receipt" ] && + [ -f "$live_receipt" ] && [ ! -L "$live_receipt" ] && + cmp -s "$staged_receipt" "$live_receipt"; then + recovery_committed=1 + return 0 + fi + if [ -f "$stage/BACKUPS_COMPLETE" ] && [ ! -L "$stage/BACKUPS_COMPLETE" ]; then + rollback_publication + fi +} + +recovery_committed=0 +remove_disposable_tree "$stage_initializing" +remove_disposable_tree "$stage_discarded" +remove_completion_path "$publication_completion_pending" +if [ -e "$stage" ] || [ -L "$stage" ]; then + recover_stale_publication + if [ "$recovery_committed" -eq 1 ]; then + validate_existing_export_generation + publish_completion + discard_canonical_stage + printf 'recovered committed sealed export closure: module=%s receipt=%s\n' \ + "$postgres" "$install_dir/$structure_relative" + exit 0 + fi + discard_canonical_stage +fi +linear_memory_descendant="$install_dir/share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json" +if [ -e "$linear_memory_descendant" ] || [ -L "$linear_memory_descendant" ]; then + [ -f "$linear_memory_descendant" ] && [ ! -L "$linear_memory_descendant" ] || + fail "unsafe linear-memory descendant receipt: $linear_memory_descendant" + python3 "$FRESH_ROOT/lib/sealed_export_chain.py" \ + --install-root "$install_dir" \ + --project-root "$FRESH_ROOT" \ + --allow-linear-memory-descendant || + fail 'installed linear-memory descendant does not validate against the sealed export generation' + printf 'sealed export closure already has a validated linear-memory descendant: module=%s receipt=%s\n' \ + "$postgres" "$install_dir/$structure_relative" + exit 0 +fi +if [ -e "$install_dir/$structure_relative" ] || [ -L "$install_dir/$structure_relative" ]; then + validate_existing_export_generation + publish_completion + printf 'validated existing sealed export closure: module=%s receipt=%s\n' \ + "$postgres" "$install_dir/$structure_relative" + exit 0 +fi +remove_completion_path "$publication_completion" +mkdir -p "$stage_initializing/bin" "$stage_initializing/share/postgresql" +printf '%s\n' "$publication_schema" >"$stage_initializing/TRANSACTION_SCHEMA" +fsync_paths "$stage_initializing/TRANSACTION_SCHEMA" +fsync_tree_directories "$stage_initializing" +mv -- "$stage_initializing" "$stage" +fsync_paths "$install_dir" + +publish_complete=0 +cleanup() { + status=$? + trap - EXIT HUP INT TERM + if completion_matches_live; then + status=0 + fi + if [ -d "$stage" ] && [ ! -L "$stage" ]; then + if [ "$publish_complete" -eq 0 ]; then + recovery_committed=0 + recover_stale_publication + if [ "$recovery_committed" -eq 1 ]; then + publish_completion + status=0 + fi + fi + discard_canonical_stage + fi + remove_disposable_tree "$stage_initializing" + remove_disposable_tree "$stage_discarded" + exit "$status" +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +readonly tool_manifest="$FRESH_ROOT/tools/sealed-export-closure/Cargo.toml" +readonly mandatory_policy="$FRESH_ROOT/runtime/policies/sealed-main-runtime-exports.v1.txt" +readonly dlsym_policy="$FRESH_ROOT/runtime/policies/sealed-main-dlsym-exports.v1.txt" +readonly side_manifest="$FRESH_ROOT/runtime/policies/sealed-side-modules.v1.tsv" +for required in "$tool_manifest" "$mandatory_policy" "$dlsym_policy" "$side_manifest"; do + [ -f "$required" ] && [ ! -L "$required" ] || fail "missing regular closure input: $required" +done +grep -Fxq '# schema=oliphaunt.wasix-postmaster.sealed-side-modules.v1' "$side_manifest" || + fail 'side-module manifest schema differs' + +declare -a side_modules=() +declare -A admitted_side_paths=() +manifest_records=0 +while IFS=$'\t' read -r canonical aliases abi_policy extra; do + case "$canonical" in + ''|'#'*) continue ;; + esac + [ -z "${extra:-}" ] || fail "side-module manifest has extra columns: $canonical" + [ -n "$aliases" ] && [ -n "$abi_policy" ] || fail "incomplete side-module record: $canonical" + case "$canonical" in + /*|*/../*|../*|*/./*|./*|*//*|*[$'\n\r']*) fail "unsafe canonical side path: $canonical" ;; + esac + [ -z "${admitted_side_paths[$canonical]+x}" ] || fail "duplicate side path: $canonical" + canonical_file="$install_dir/$canonical" + [ -f "$canonical_file" ] && [ ! -L "$canonical_file" ] || + fail "missing regular canonical side module: $canonical" + admitted_side_paths[$canonical]=1 + side_modules+=("$canonical") + manifest_records=$((manifest_records + 1)) + if [ "$aliases" != - ]; then + IFS=',' read -r -a alias_paths <<<"$aliases" + [ "${#alias_paths[@]}" -gt 0 ] || fail "empty alias set: $canonical" + for alias_path in "${alias_paths[@]}"; do + case "$alias_path" in + ''|/*|*/../*|../*|*/./*|./*|*//*|*[$'\n\r']*) fail "unsafe side alias: $alias_path" ;; + esac + [ -z "${admitted_side_paths[$alias_path]+x}" ] || fail "duplicate side alias: $alias_path" + alias_file="$install_dir/$alias_path" + [ -f "$alias_file" ] || fail "missing side alias: $alias_path" + cmp -s "$canonical_file" "$alias_file" || + fail "side alias bytes differ from $canonical: $alias_path" + admitted_side_paths[$alias_path]=1 + done + fi +done <"$side_manifest" +[ "$manifest_records" -gt 0 ] || fail 'side-module manifest has no records' + +find "$install_dir/lib" \( -type f -o -type l \) \ + \( -name '*.so' -o -name '*.so.*' \) -printf '%P\0' >"$stage/discovered-side-modules.unsorted" +LC_ALL=C sort -z "$stage/discovered-side-modules.unsorted" >"$stage/discovered-side-modules.sorted" +while IFS= read -r -d '' discovered; do + relative="lib/$discovered" + [ -n "${admitted_side_paths[$relative]+x}" ] || + fail "installed side module is absent from the sealed graph: $relative" +done <"$stage/discovered-side-modules.sorted" + +tool_target="$FRESH_WORK_ROOT/runtime/sealed-export-closure-target" +fresh_require_managed_generated_path "$tool_target" sealed-export-closure-tool-target +CARGO_TARGET_DIR="$tool_target" cargo build --locked --release --manifest-path "$tool_manifest" +closure_tool="$tool_target/release/oliphaunt-wasix-sealed-export-closure" +[ -x "$closure_tool" ] && [ ! -L "$closure_tool" ] || fail "missing built closure analyzer: $closure_tool" + +docker_bin="$(fresh_docker_bin)" +fresh_ensure_docker_image +readonly container_wasm_opt=/opt/wasixcc-home/.wasixcc/binaryen/bin/wasm-opt +dce_identity="$($docker_bin run --rm "$FRESH_WASIX_DOCKER_IMAGE" sha256sum "$container_wasm_opt")" || + fail 'could not hash pinned wasm-opt' +dce_sha256="${dce_identity%% *}" +fresh_is_sha256 "$dce_sha256" || fail "invalid wasm-opt SHA-256: $dce_sha256" +dce_version="$($docker_bin run --rm "$FRESH_WASIX_DOCKER_IMAGE" "$container_wasm_opt" --version)" || + fail 'could not read pinned wasm-opt version' +[ "$(printf '%s\n' "$dce_version" | wc -l | tr -d ' ')" -eq 1 ] || fail 'wasm-opt version is multiline' + +cp -p "$postgres" "$stage/bin/postgres.seed" +seed_proof="$stage/share/postgresql/wasix-postmaster.sealed-export.seed-proof.json" +final_proof="$stage/share/postgresql/wasix-postmaster.sealed-export.final-proof.json" +allowlist="$stage/share/postgresql/wasix-postmaster.sealed-export.allowlist" +structure_receipt="$stage/share/postgresql/wasix-postmaster.sealed-export.structure.receipt" +start_proof="$stage/share/postgresql/wasix-postmaster.sealed-export.start-proof.intermediate.json" +concurrency_receipt="$stage/share/postgresql/wasix-postmaster.sealed-export.concurrency.intermediate.receipt" + +side_manifest_sha256="$(sha256sum "$side_manifest" | awk '{print $1}')" + +( + cd "$install_dir" + "$closure_tool" seal \ + bin/postgres \ + "$mandatory_policy" \ + "$dlsym_policy" \ + "$seed_proof" \ + "$allowlist" \ + "${side_modules[@]}" + "$closure_tool" rewrite \ + bin/postgres \ + "$allowlist" \ + .oliphaunt-sealed-export-closure.pending/bin/postgres.stripped +) + +docker_stage="$(fresh_docker_path_for "$stage")" +"$docker_bin" run --rm \ + --user "$(id -u):$(id -g)" \ + -v "$REPO_ROOT:/work" \ + -w /work \ + "$FRESH_WASIX_DOCKER_IMAGE" \ + "$container_wasm_opt" \ + "$docker_stage/bin/postgres.stripped" \ + --remove-unused-module-elements \ + --enable-bulk-memory \ + --enable-threads \ + --enable-mutable-globals \ + --enable-exception-handling \ + --enable-extended-const \ + -o "$docker_stage/bin/postgres" +chmod --reference="$postgres" "$stage/bin/postgres" + +( + cd "$install_dir" + "$closure_tool" attest-final \ + bin/postgres \ + .oliphaunt-sealed-export-closure.pending/bin/postgres \ + "$mandatory_policy" \ + "$dlsym_policy" \ + "$allowlist" \ + "$seed_proof" \ + "$final_proof" \ + "$structure_receipt" \ + "$dce_sha256" \ + "$dce_version" \ + "$side_manifest_sha256" \ + "${side_modules[@]}" +) + +python3 "$FRESH_ROOT/runtime/bin/verify-postmaster-wasm-import.py" "$stage/bin/postgres" +fresh_require_start_proof_tool "$FRESH_START_PROOF_BIN" "$FRESH_POSTMASTER_EXECUTOR_BUILD_RECEIPT" +"$FRESH_START_PROOF_BIN" "$stage/bin/postgres" >"$start_proof" + +"$docker_bin" run --rm \ + --user "$(id -u):$(id -g)" \ + -v "$REPO_ROOT:/work" \ + -w /work \ + "$FRESH_WASIX_DOCKER_IMAGE" \ + python3 \ + /work/src/runtimes/liboliphaunt/wasix-postmaster/runtime/bin/verify-postmaster-concurrency-contract.py \ + --expected-total "$expected_total" \ + --latch-state-contract packed-atomic-v1 \ + --wasm-dis /opt/wasixcc-home/.wasixcc/binaryen/bin/wasm-dis \ + --receipt "$docker_stage/share/postgresql/wasix-postmaster.sealed-export.concurrency.intermediate.receipt" \ + "$docker_stage/bin/postgres" + +for artifact in \ + "$seed_proof" \ + "$final_proof" \ + "$allowlist" \ + "$structure_receipt" \ + "$start_proof" \ + "$concurrency_receipt" +do + [ -f "$artifact" ] && [ ! -L "$artifact" ] || fail "missing staged receipt: $artifact" +done + +share_dir="$install_dir/share/postgresql" +mkdir -p "$share_dir" +[ ! -L "$share_dir" ] || fail "unsafe publication directory: $share_dir" + +# Copy every predecessor before changing the live prefix. BACKUPS_COMPLETE is a +# durable write-ahead boundary: before it, cleanup may discard the stage; +# after it, cleanup can restore the exact predecessor generation. +for relative in "${publication_relatives[@]}"; do + destination="$install_dir/$relative" + backup="$stage/originals/$relative" + absent="$stage/originals/$relative.absent" + mkdir -p "$(dirname "$backup")" + if [ -e "$destination" ] || [ -L "$destination" ]; then + [ -f "$destination" ] && [ ! -L "$destination" ] || + fail "unsafe predecessor publication path: $destination" + cp -p -- "$destination" "$backup" + fsync_paths "$backup" "$(dirname "$backup")" + else + : >"$absent" + fsync_paths "$absent" "$(dirname "$absent")" + fi +done +fsync_tree_directories "$stage/originals" +: >"$stage/BACKUPS_COMPLETE" +fsync_paths "$stage/BACKUPS_COMPLETE" "$stage/originals" "$stage" + +# De-admit the predecessor first. Publish the rewritten module and auxiliary +# proofs, make them durable, mark READY, and only then atomically publish the +# structural receipt that admits the new generation. +remove_live_file "$structure_relative" +for relative in "${publication_relatives[@]}"; do + [ "$relative" != "$structure_relative" ] || continue + atomic_publish_file "$stage/$relative" "$relative" +done +fsync_paths "$stage/$structure_relative" "$(dirname "$stage/$structure_relative")" +: >"$stage/READY_TO_ADMIT" +fsync_paths "$stage/READY_TO_ADMIT" "$stage" +atomic_publish_file "$stage/$structure_relative" "$structure_relative" +publish_completion +publish_complete=1 + +printf 'sealed exact main-module export closure: module=%s receipt=%s\n' \ + "$postgres" "$share_dir/wasix-postmaster.sealed-export.structure.receipt" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-core-exports.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-core-exports.test.sh new file mode 100755 index 00000000..cc8d90d2 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-core-exports.test.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +wrapper="$project_root/bin/seal-wasix-core-exports.sh" +build_script="$project_root/bin/build-wasix-core.sh" +tool_manifest="$project_root/tools/sealed-export-closure/Cargo.toml" +runtime_roots="$project_root/runtime/policies/sealed-main-runtime-exports.v1.txt" +dlsym_roots="$project_root/runtime/policies/sealed-main-dlsym-exports.v1.txt" +side_manifest="$project_root/runtime/policies/sealed-side-modules.v1.tsv" + +bash -n "$wrapper" +bash -n "$build_script" +"$wrapper" --help >/dev/null + +test_target="$(mktemp -d)" +cleanup() { + status=$? + trap - EXIT + rm -rf -- "$test_target" + exit "$status" +} +trap cleanup EXIT +CARGO_TARGET_DIR="$test_target" cargo test --locked --manifest-path "$tool_manifest" + +python3 - "$runtime_roots" "$dlsym_roots" "$side_manifest" "$build_script" "$wrapper" <<'PY' +import pathlib +import sys + +runtime_path, dlsym_path, side_path, build_path, wrapper_path = map(pathlib.Path, sys.argv[1:]) + + +def names(path: pathlib.Path) -> list[str]: + raw = path.read_bytes() + assert raw.endswith(b"\n") and b"\r" not in raw + values = [] + for line in raw.decode().splitlines(): + value = line.split("#", 1)[0].strip() + if value: + assert not any(char.isspace() for char in value) + values.append(value) + assert len(values) == len(set(values)) + return values + + +runtime = set(names(runtime_path)) +assert runtime == { + "__data_end", + "__tls_align", + "__tls_base", + "__tls_size", + "__wasm_apply_data_relocs", + "__wasm_call_ctors", + "__wasm_init_memory", + "__wasm_init_tls", + "__wasm_signal", + "_start", + "wasi_thread_start", + "ResetLatch", + "SetLatch", + "WaitEventSetWait", +} +assert names(dlsym_path) == [] + +raw = side_path.read_bytes() +assert raw.endswith(b"\n") and b"\r" not in raw +lines = [ + line + for line in raw.decode().splitlines() + if line and not line.startswith("#") +] +assert lines == [ + "lib/libpq.so.5.18\tlib/libpq.so,lib/libpq.so.5\tpublic-libpq-abi", + "lib/postgresql/cyrillic_and_mic.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/dict_snowball.so\t-\tpostgresql-server-extension", + "lib/postgresql/euc2004_sjis2004.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/euc_cn_and_mic.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/euc_jp_and_sjis.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/euc_kr_and_mic.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/euc_tw_and_big5.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/latin2_and_win1250.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/latin_and_mic.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/plpgsql.so\t-\tpostgresql-server-extension", + "lib/postgresql/utf8_and_big5.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_cyrillic.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_euc2004.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_euc_cn.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_euc_jp.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_euc_kr.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_euc_tw.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_gb18030.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_gbk.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_iso8859.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_iso8859_1.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_johab.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_sjis.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_sjis2004.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_uhc.so\t-\tpostgresql-encoding-conversion", + "lib/postgresql/utf8_and_win.so\t-\tpostgresql-encoding-conversion", +] + +build = build_path.read_text(encoding="utf-8") +pipeline_markers = [ + '"$FRESH_ROOT/bin/seal-wasix-core-exports.sh"', + '"$FRESH_ROOT/bin/seal-wasix-linear-memory.sh"', + 'final_start_proof="$proof_dir/wasix-postmaster.start-proof.json"', + 'final_concurrency_receipt="$proof_dir/wasix-postmaster.final-wasm-concurrency.receipt"', +] +positions = [build.rindex(marker) for marker in pipeline_markers] +assert positions == sorted(positions), positions +assert "schema=oliphaunt.wasix-postmaster.guest-build.v4" in build +guest_fields = [ + "final_wasm_concurrency_receipt_sha256", + "linear_memory_profile_id", + "linear_memory_install_receipt_sha256", + "postgres_tag", +] +guest_positions = [build.rindex(f"printf '{field}=") for field in guest_fields] +assert guest_positions == sorted(guest_positions), guest_positions + +wrapper = wrapper_path.read_text(encoding="utf-8") +assert 'sealed-export-publication.v1' in wrapper +assert 'READY_TO_ADMIT' in wrapper +assert 'BACKUPS_COMPLETE' in wrapper +assert 'cmp -s "$staged_receipt" "$live_receipt"' in wrapper +assert 'done < <(' not in wrapper +assert '>"$stage/discovered-side-modules.unsorted"' in wrapper +assert 'sort -z "$stage/discovered-side-modules.unsorted"' in wrapper +publication_markers = [ + ': >"$stage/BACKUPS_COMPLETE"', + 'remove_live_file "$structure_relative"', + 'atomic_publish_file "$stage/$relative" "$relative"', + ': >"$stage/READY_TO_ADMIT"', + 'atomic_publish_file "$stage/$structure_relative" "$structure_relative"', +] +positions = [wrapper.rindex(marker) for marker in publication_markers] +assert positions == sorted(positions), positions +PY + +printf 'sealed export closure policy and analyzer tests passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-core-exports.transaction.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-core-exports.transaction.test.sh new file mode 100755 index 00000000..5206f35a --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-core-exports.transaction.test.sh @@ -0,0 +1,1041 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Exercise the real publication transaction with tiny deterministic producer +# fixtures. The command shims do not replace transaction operations: they +# only make the expensive Cargo/Docker analyzers local and inject SIGKILL at +# externally observable filesystem boundaries. + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +repo_root="$(cd "$project_root/../../../.." && pwd -P)" +wrapper="$project_root/bin/seal-wasix-core-exports.sh" +side_manifest="$project_root/runtime/policies/sealed-side-modules.v1.tsv" +managed_root="$repo_root/target/oliphaunt-wasix-postmaster" + +fail() { + printf 'sealed export transaction test: %s\n' "$*" >&2 + exit 1 +} + +for command in bash cmp cp find flock grep mktemp python3 sha256sum sort; do + command -v "$command" >/dev/null 2>&1 || fail "missing test command: $command" +done +[ -x "$wrapper" ] || fail "missing executable wrapper: $wrapper" +[ -f "$side_manifest" ] && [ ! -L "$side_manifest" ] || + fail "missing regular side-module manifest: $side_manifest" + +real_cp="$(command -v cp)" +real_mv="$(command -v mv)" +real_rm="$(command -v rm)" +real_python3="$(command -v python3)" +real_sha256sum="$(command -v sha256sum)" + +mkdir -p "$managed_root" +[ -d "$managed_root" ] && [ ! -L "$managed_root" ] || + fail "unsafe managed test root: $managed_root" +test_root="$(mktemp -d "$managed_root/sealed-export-transaction-test.XXXXXX")" +fake_bin="$test_root/fake-bin" +mkdir -p "$fake_bin" +active_pids="" + +cleanup() { + status=$? + trap - EXIT HUP INT TERM + find "$test_root" -type f -name '*.gate-release' -exec touch {} + 2>/dev/null || : + for pid in $active_pids; do + kill -TERM "$pid" 2>/dev/null || : + done + for pid in $active_pids; do + wait "$pid" 2>/dev/null || : + done + "$real_rm" -rf -- "$test_root" + exit "$status" +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +fake_closure_tool="$test_root/fake-sealed-export-closure" +fake_closure_attest="$test_root/fake-sealed-export-attest.py" +fake_start_proof="$test_root/fake-start-proof" +executor_receipt="$test_root/postmaster-executor-build.receipt" + +cat >"$fake_closure_attest" <<'PY' +#!/usr/bin/env python3 +import json +import pathlib +import runpy +import sys + +( + fixture_helper, + install_raw, + staged_raw, + mandatory_raw, + dlsym_raw, + seed_raw, + final_raw, + allowlist_raw, + structure_raw, + dce_sha256, + dce_version, + side_manifest_sha256, + *side_paths, +) = sys.argv[1:] +fixture = runpy.run_path(fixture_helper) +digest = fixture["digest"] +json_bytes = fixture["json_bytes"] +module_summary = fixture["module_summary"] +proof = fixture["proof"] +snapshot = fixture["snapshot"] + +install = pathlib.Path(install_raw) +staged = pathlib.Path(staged_raw) +mandatory = digest(pathlib.Path(mandatory_raw).read_bytes()) +dlsym = digest(pathlib.Path(dlsym_raw).read_bytes()) +sides = [ + module_summary( + relative, + digest((install / relative).read_bytes()), + (install / relative).stat().st_size, + ) + for relative in side_paths +] +final_data = staged.read_bytes() +final_sha256 = digest(final_data) +final_main = module_summary("bin/postgres", final_sha256, len(final_data)) +seed_sha256 = digest(b"pre-dce-fixture\0" + final_data) +seed_main = module_summary("bin/postgres", seed_sha256, len(final_data) + 16) +seed_data = json_bytes(proof(seed_main, sides, mandatory, dlsym)) +final_proof_data = json_bytes(proof(final_main, sides, mandatory, dlsym)) +pathlib.Path(seed_raw).write_bytes(seed_data) +pathlib.Path(final_raw).write_bytes(final_proof_data) +allowlist = pathlib.Path(allowlist_raw) +receipt = { + "schema": "oliphaunt.wasix-postmaster.sealed-export-structure.v1", + "policy-id": "oliphaunt.wasix-postmaster.sealed-export-closure.v1", + "analyzer-version": "fixture", + "analyzer-binary-sha256": "0" * 64, + "dce-tool-sha256": dce_sha256, + "dce-tool-version": dce_version, + "dce-passes": ["--remove-unused-module-elements"], + "mandatory-policy-sha256": mandatory, + "declared-main-dlsym-policy-sha256": dlsym, + "side-manifest-sha256": side_manifest_sha256, + "allowlist-sha256": digest(allowlist.read_bytes()), + "seed-proof-sha256": digest(seed_data), + "final-proof-sha256": digest(final_proof_data), + "seed": snapshot(seed_sha256, len(final_data) + 16), + "final-module": snapshot(final_sha256, len(final_data)), + "estimate": { + "classification": "structural-estimate-not-measured-rss", + "formula": "removed-local-functions * 32 + removed-local-globals * 48", + "removed-local-functions": 1, + "bytes-per-eager-local-funcref": 32, + "removed-local-globals": 0, + "bytes-per-eager-local-global": 48, + "estimated-bytes-per-instance": 32, + "caveat": "synthetic transaction fixture", + }, + "sides": [ + {"path": side["path"], "sha256": side["sha256"]} for side in sides + ], +} +pathlib.Path(structure_raw).write_text( + json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8" +) +PY +chmod 755 "$fake_closure_attest" + +cat >"$fake_closure_tool" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +command="$1" +shift +case "$command" in + seal) + seed_proof="$4" + allowlist="$5" + printf '{"schema":"fixture-seed-proof-v1"}\n' >"$seed_proof" + printf 'fixture-export\n' >"$allowlist" + ;; + rewrite) + main_module="$1" + output="$3" + "$TX_REAL_CP" -p -- "$main_module" "$output" + # A valid empty-name custom section makes the successor byte-distinct. + printf '\000\001\000' >>"$output" + ;; + attest-final) + install="$PWD" + staged_module="$2" + mandatory_policy="$3" + dlsym_policy="$4" + allowlist="$5" + seed_proof="$6" + final_proof="$7" + structure_receipt="$8" + dce_sha256="$9" + dce_version="${10}" + side_manifest_sha256="${11}" + shift 11 + "$TX_REAL_PYTHON3" "$TX_FAKE_CLOSURE_ATTEST" \ + "$TX_EXPORT_FIXTURE_HELPER" \ + "$install" "$staged_module" "$mandatory_policy" "$dlsym_policy" \ + "$seed_proof" "$final_proof" "$allowlist" "$structure_receipt" \ + "$dce_sha256" "$dce_version" "$side_manifest_sha256" "$@" + ;; + *) + printf 'unexpected fake closure command: %s\n' "$command" >&2 + exit 2 + ;; +esac +EOF +chmod 755 "$fake_closure_tool" + +cat >"$fake_start_proof" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -eq 1 ] && [ "$1" = --policy-id ]; then + printf '%s\n' llvm-shared-memory-init-restricted-effects.v1 + exit 0 +fi +[ "$#" -eq 1 ] || exit 2 +module_sha256="$("$TX_REAL_SHA256SUM" "$1")" +module_sha256="${module_sha256%% *}" +printf '{"schema":"fixture-start-proof-v1","module-sha256":"%s"}\n' \ + "$module_sha256" +EOF +chmod 755 "$fake_start_proof" + +zero_sha=0000000000000000000000000000000000000000000000000000000000000000 +start_proof_sha256="$("$real_sha256sum" "$fake_start_proof")" +start_proof_sha256="${start_proof_sha256%% *}" +cat >"$executor_receipt" <"$fake_bin/cargo" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +printf '%s\n' "$$" >>"$TX_CARGO_LOG" +if [ "${TX_FAIL_BUILD:-0}" = 1 ]; then + exit 23 +fi +if [ -n "${TX_GATE_READY:-}" ]; then + : >"$TX_GATE_READY" + while [ ! -e "$TX_GATE_RELEASE" ]; do + sleep 0.02 + done +fi +mkdir -p "$CARGO_TARGET_DIR/release" +"$TX_REAL_CP" -p -- \ + "$TX_FAKE_CLOSURE_TOOL" \ + "$CARGO_TARGET_DIR/release/oliphaunt-wasix-sealed-export-closure" +chmod 755 "$CARGO_TARGET_DIR/release/oliphaunt-wasix-sealed-export-closure" +EOF +chmod 755 "$fake_bin/cargo" + +cat >"$fake_bin/docker" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +map_path() { + case "$1" in + /work) printf '%s\n' "$TX_REPO_ROOT" ;; + /work/*) printf '%s/%s\n' "$TX_REPO_ROOT" "${1#/work/}" ;; + *) printf '%s\n' "$1" ;; + esac +} + +case "${1:-}" in + image) + [ "${2:-}" = inspect ] || exit 2 + exit 0 + ;; + run) + shift + while [ "$#" -gt 0 ]; do + case "$1" in + --rm) shift ;; + --user|-v|-w) shift 2 ;; + *) image="$1"; shift; break ;; + esac + done + [ -n "${image:-}" ] && [ "$#" -gt 0 ] || exit 2 + command="$1" + shift + case "$command" in + sha256sum) + printf '%s %s\n' \ + 1111111111111111111111111111111111111111111111111111111111111111 \ + "${1:-/opt/fake-wasm-opt}" + ;; + */wasm-opt) + if [ "${1:-}" = --version ]; then + printf 'fixture-wasm-opt 1\n' + exit 0 + fi + input="$(map_path "$1")" + output="" + while [ "$#" -gt 0 ]; do + if [ "$1" = -o ]; then + shift + output="$(map_path "$1")" + break + fi + shift + done + [ -n "$output" ] || exit 2 + "$TX_REAL_CP" -p -- "$input" "$output" + ;; + python3) + receipt="" + postgres="" + while [ "$#" -gt 0 ]; do + if [ "$1" = --receipt ]; then + shift + receipt="$(map_path "$1")" + fi + postgres="$1" + shift + done + postgres="$(map_path "$postgres")" + [ -n "$receipt" ] && [ -f "$postgres" ] || exit 2 + postgres_sha256="$("$TX_REAL_SHA256SUM" "$postgres")" + postgres_sha256="${postgres_sha256%% *}" + cat >"$receipt" <&2 + exit 2 + ;; + esac + ;; + *) + exit 2 + ;; +esac +EOF +chmod 755 "$fake_bin/docker" + +cat >"$fake_bin/cp" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +previous="" +last="" +for argument in "$@"; do + previous="$last" + last="$argument" +done +source_path="$previous" +destination="$last" +stage="$WASIX_INSTALL_DIR/.oliphaunt-sealed-export-closure.pending" +share="$WASIX_INSTALL_DIR/share/postgresql" +trip=0 +case "${TX_KILL_AT:-}" in + de-admit) + [ "$source_path" = "$stage/bin/postgres" ] && + [ "$destination" = "$WASIX_INSTALL_DIR/bin/.postgres.oliphaunt-sealed-export.pending" ] && + trip=1 + ;; + READY) + [ "$source_path" = "$stage/share/postgresql/wasix-postmaster.sealed-export.structure.receipt" ] && + [ "$destination" = "$share/.wasix-postmaster.sealed-export.structure.receipt.oliphaunt-sealed-export.pending" ] && + trip=1 + ;; +esac +if [ "$trip" -eq 1 ]; then + printf '%s\n' "$TX_KILL_AT" >>"$TX_HOOK_LOG" + kill -KILL "$PPID" + exit 137 +fi +exec "$TX_REAL_CP" "$@" +EOF +chmod 755 "$fake_bin/cp" + +cat >"$fake_bin/mv" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +previous="" +last="" +for argument in "$@"; do + previous="$last" + last="$argument" +done +source_path="$previous" +destination="$last" +stage="$WASIX_INSTALL_DIR/.oliphaunt-sealed-export-closure.pending" +initializing="$WASIX_INSTALL_DIR/.oliphaunt-sealed-export-closure.initializing" +discarded="$WASIX_INSTALL_DIR/.oliphaunt-sealed-export-closure.discarded" + +trip_before=0 +trip_after=0 +case "${TX_KILL_AT:-}" in + init) + [ "$source_path" = "$initializing" ] && [ "$destination" = "$stage" ] && trip_before=1 + ;; + payload:*) + relative="${TX_KILL_AT#payload:}" + live="$WASIX_INSTALL_DIR/$relative" + temporary="$(dirname "$live")/.$(basename "$live").oliphaunt-sealed-export.pending" + [ "$source_path" = "$temporary" ] && [ "$destination" = "$live" ] && trip_after=1 + ;; + rollback:*) + relative="${TX_KILL_AT#rollback:}" + live="$WASIX_INSTALL_DIR/$relative" + temporary="$(dirname "$live")/.$(basename "$live").oliphaunt-sealed-export.pending" + [ "$source_path" = "$temporary" ] && [ "$destination" = "$live" ] && trip_after=1 + ;; + completion) + case "$source_path:$destination" in + *.completed.pending:*.completed) trip_after=1 ;; + esac + ;; + tombstone) + [ "$source_path" = "$stage" ] && [ "$destination" = "$discarded" ] && trip_after=1 + ;; +esac + +if [ "$trip_before" -eq 1 ]; then + printf '%s\n' "$TX_KILL_AT" >>"$TX_HOOK_LOG" + kill -KILL "$PPID" + exit 137 +fi +if [ "$trip_after" -eq 1 ]; then + "$TX_REAL_MV" "$@" + printf '%s\n' "$TX_KILL_AT" >>"$TX_HOOK_LOG" + kill -KILL "$PPID" + exit 137 +fi +exec "$TX_REAL_MV" "$@" +EOF +chmod 755 "$fake_bin/mv" + +cat >"$fake_bin/rm" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +last="" +for argument in "$@"; do + last="$argument" +done +structure="$WASIX_INSTALL_DIR/share/postgresql/wasix-postmaster.sealed-export.structure.receipt" +if [ "${TX_KILL_AT:-}" = backup ] && [ "$last" = "$structure" ]; then + printf '%s\n' "$TX_KILL_AT" >>"$TX_HOOK_LOG" + kill -KILL "$PPID" + exit 137 +fi +exec "$TX_REAL_RM" "$@" +EOF +chmod 755 "$fake_bin/rm" + +cat >"$fake_bin/python3" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +stage="$WASIX_INSTALL_DIR/.oliphaunt-sealed-export-closure.pending" +backups_complete="$stage/BACKUPS_COMPLETE" +partial_backup="$stage/originals/share/postgresql/wasix-postmaster.sealed-export.final-proof.json" +if [ "${TX_KILL_AT:-}" = backup ] && + [ "${1:-}" = - ] && [ "${2:-}" = "$backups_complete" ]; then + "$TX_REAL_PYTHON3" "$@" + printf '%s\n' "$TX_KILL_AT" >>"$TX_HOOK_LOG" + kill -KILL "$PPID" + exit 137 +fi +if [ "${TX_KILL_AT:-}" = backup-partial ] && + [ "${1:-}" = - ] && [ "${2:-}" = "$partial_backup" ]; then + "$TX_REAL_PYTHON3" "$@" + printf '%s\n' "$TX_KILL_AT" >>"$TX_HOOK_LOG" + kill -KILL "$PPID" + exit 137 +fi +if [ "${TX_KILL_AT:-}" = admission ] && + [ "${1:-}" = - ] && + [ "${2:-}" = oliphaunt.wasix-postmaster.sealed-export-completion.v2 ]; then + printf '%s\n' "$TX_KILL_AT" >>"$TX_HOOK_LOG" + kill -KILL "$PPID" + exit 137 +fi +exec "$TX_REAL_PYTHON3" "$@" +EOF +chmod 755 "$fake_bin/python3" + +publication_relatives=( + bin/postgres + share/postgresql/wasix-postmaster.sealed-export.seed-proof.json + share/postgresql/wasix-postmaster.sealed-export.final-proof.json + share/postgresql/wasix-postmaster.sealed-export.allowlist + share/postgresql/wasix-postmaster.sealed-export.start-proof.intermediate.json + share/postgresql/wasix-postmaster.sealed-export.concurrency.intermediate.receipt + share/postgresql/wasix-postmaster.sealed-export.structure.receipt +) +structure_relative=share/postgresql/wasix-postmaster.sealed-export.structure.receipt + +write_minimal_postmaster() { + "$real_python3" - "$1" <<'PY' +import pathlib +import sys + +def uleb(value: int) -> bytes: + out = bytearray() + while True: + byte = value & 0x7f + value >>= 7 + out.append(byte | (0x80 if value else 0)) + if not value: + return bytes(out) + +def vector(items: list[bytes]) -> bytes: + return uleb(len(items)) + b"".join(items) + +def name(value: str) -> bytes: + raw = value.encode("utf-8") + return uleb(len(raw)) + raw + +def function_type(parameters: tuple[int, ...]) -> bytes: + return b"\x60" + vector([bytes([value]) for value in parameters]) + vector([b"\x7f"]) + +types = [ + function_type((0x7f, 0x7e, 0x7e, 0x7f)), + function_type((0x7f, 0x7e, 0x7e, 0x7f, 0x7f)), + function_type((0x7f, 0x7f, 0x7f)), +] +imports = [] +for index, symbol in enumerate(("fd_sync_range", "fd_cache_offer", "fd_cache_revoke")): + imports.append(name("oliphaunt_postmaster_v1") + name(symbol) + b"\x00" + uleb(index)) + +def section(identifier: int, payload: bytes) -> bytes: + return bytes([identifier]) + uleb(len(payload)) + payload + +module = b"\x00asm\x01\x00\x00\x00" + section(1, vector(types)) + section(2, vector(imports)) +path = pathlib.Path(sys.argv[1]) +path.write_bytes(module) +PY +} + +create_fixture() { + case_root="$1" + install="$case_root/install" + mkdir -p "$install/bin" "$install/lib/postgresql" "$install/share/postgresql" "$case_root/work" + write_minimal_postmaster "$install/bin/postgres" + chmod 755 "$install/bin/postgres" + + while IFS=$'\t' read -r canonical aliases _abi extra; do + case "$canonical" in + ''|'#'*) continue ;; + esac + [ -z "${extra:-}" ] || fail "unexpected side-manifest column: $canonical" + mkdir -p "$(dirname "$install/$canonical")" + printf 'fixture-side-module-v1\n' >"$install/$canonical" + if [ "$aliases" != - ]; then + IFS=',' read -r -a alias_paths <<<"$aliases" + for alias_path in "${alias_paths[@]}"; do + mkdir -p "$(dirname "$install/$alias_path")" + "$real_cp" -p -- "$install/$canonical" "$install/$alias_path" + done + fi + done <"$side_manifest" + + printf 'old-seed-proof\n' >"$install/share/postgresql/wasix-postmaster.sealed-export.seed-proof.json" + printf 'old-final-proof\n' >"$install/share/postgresql/wasix-postmaster.sealed-export.final-proof.json" + printf 'old-allowlist\n' >"$install/share/postgresql/wasix-postmaster.sealed-export.allowlist" + printf 'old-start-proof\n' >"$install/share/postgresql/wasix-postmaster.sealed-export.start-proof.intermediate.json" + printf 'old-concurrency-receipt\n' >"$install/share/postgresql/wasix-postmaster.sealed-export.concurrency.intermediate.receipt" +} + +make_linear_memory_descendant() { + "$real_python3" - "$1/install" "$side_manifest" <<'PY' +import hashlib +import json +import pathlib +import sys + +install = pathlib.Path(sys.argv[1]) +side_manifest = pathlib.Path(sys.argv[2]) +side_paths = [ + line.split("\t", 1)[0] + for line in side_manifest.read_text(encoding="utf-8").splitlines() + if line and not line.startswith("#") +] +paths = sorted(["bin/postgres", *side_paths]) + +def digest(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + +records = [] +for relative in paths: + path = install / relative + source = path.read_bytes() + sealed = source + b"\x00\x01\x00" + path.write_bytes(sealed) + records.append( + { + "path": relative, + "source-module-sha256": digest(source), + "module-sha256": digest(sealed), + "initial-pages": 1, + "maximum-pages": 4096, + "maximum-bytes": 268435456, + "shared": True, + "import-module": "env", + "import-name": "memory", + "transformation": "pinned-wasixcc-65536-to-embedded-4096-reversible-v1", + } + ) + +def closure_hash(field: str) -> str: + value = hashlib.sha256() + for item in ( + "oliphaunt.wasix-postmaster.linear-memory-install-closure.v1", + field, + ): + encoded = item.encode() + value.update(len(encoded).to_bytes(8, "big")) + value.update(encoded) + for record in records: + for item in (record["path"], record[field]): + encoded = item.encode() + value.update(len(encoded).to_bytes(8, "big")) + value.update(encoded) + return value.hexdigest() + +predecessor_relative = "share/postgresql/wasix-postmaster.sealed-export.structure.receipt" +predecessor = (install / predecessor_relative).read_bytes() +receipt = { + "schema": "oliphaunt.wasix-postmaster.linear-memory-install.v1", + "profile-id": "oliphaunt.wasix-postmaster.linear-memory.wasm32-max256m-u64-static4g-guard2g.v1", + "address-width": "wasm32", + "supported-host-pointer-width": "u64", + "maximum-pages": 4096, + "maximum-bytes": 268435456, + "static-bound-pages": 65536, + "static-offset-guard-bytes": 2147483648, + "static-access-lowering": "wasmer-llvm-unchecked-reservation-and-guard-v1", + "requires-shared": True, + "requires-import": "env.memory", + "excludes-wasm32-end-wrap": True, + "predecessor-export-closure-receipt": predecessor_relative, + "predecessor-export-closure-receipt-sha256": digest(predecessor), + "source-module-closure-sha256": closure_hash("source-module-sha256"), + "module-closure-sha256": closure_hash("module-sha256"), + "module-count": len(records), + "modules": records, +} +output = install / "share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json" +output.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8") +PY +} + +write_snapshot() { + install="$1" + output="$2" + : >"$output" + for relative in "${publication_relatives[@]}"; do + if [ -f "$install/$relative" ] && [ ! -L "$install/$relative" ]; then + value="$("$real_sha256sum" "$install/$relative")" + printf '%s\t%s\n' "${value%% *}" "$relative" >>"$output" + elif [ ! -e "$install/$relative" ] && [ ! -L "$install/$relative" ]; then + printf 'absent\t%s\n' "$relative" >>"$output" + else + fail "snapshot source is neither regular nor absent: $install/$relative" + fi + done +} + +snapshot_hash() { + snapshot="$1" + relative="$2" + awk -F '\t' -v expected="$relative" '$2 == expected { count += 1; value = $1 } END { if (count != 1) exit 2; print value }' "$snapshot" +} + +assert_matches() { + install="$1" + snapshot="$2" + relative="$3" + expected="$(snapshot_hash "$snapshot" "$relative")" + if [ "$expected" = absent ]; then + assert_absent "$install/$relative" + return + fi + [ -f "$install/$relative" ] && [ ! -L "$install/$relative" ] || + fail "missing regular publication file: $install/$relative" + actual="$("$real_sha256sum" "$install/$relative")" + actual="${actual%% *}" + [ "$actual" = "$expected" ] || + fail "$relative does not match $(basename "$snapshot")" +} + +assert_snapshot() { + install="$1" + snapshot="$2" + for relative in "${publication_relatives[@]}"; do + assert_matches "$install" "$snapshot" "$relative" + done +} + +assert_absent() { + path="$1" + [ ! -e "$path" ] && [ ! -L "$path" ] || fail "expected absent path: $path" +} + +assert_transaction_clean() { + case_root="$1" + install="$case_root/install" + for suffix in pending initializing discarded; do + assert_absent "$install/.oliphaunt-sealed-export-closure.$suffix" + done + if find "$install" -type f -name '*.oliphaunt-sealed-export.pending' -print -quit | grep -q .; then + fail "publication temporary survived under $install" + fi + if find "$case_root/work/runtime/publication-locks" -type f -name '*.completed.pending' -print -quit 2>/dev/null | grep -q .; then + fail "completion temporary survived under $case_root/work" + fi +} + +invoke_wrapper() { + case_root="$1" + kill_at="$2" + fail_build="$3" + gate_ready="$4" + gate_release="$5" + install="$case_root/install" + mkdir -p "$case_root/work" + env \ + PATH="$fake_bin:$PATH" \ + FRESH_WORK_ROOT="$case_root/work" \ + FRESH_START_PROOF_BIN="$fake_start_proof" \ + FRESH_POSTMASTER_EXECUTOR_BUILD_RECEIPT="$executor_receipt" \ + FRESH_WASIX_DOCKER_IMAGE=fixture-wasix-image \ + WASIX_INSTALL_DIR="$install" \ + TX_CARGO_LOG="$case_root/cargo.log" \ + TX_FAIL_BUILD="$fail_build" \ + TX_GATE_READY="$gate_ready" \ + TX_GATE_RELEASE="$gate_release" \ + TX_HOOK_LOG="$case_root/hook.log" \ + TX_KILL_AT="$kill_at" \ + TX_EXPORT_FIXTURE_HELPER="$project_root/testdata/make-sealed-export-fixture.py" \ + TX_FAKE_CLOSURE_ATTEST="$fake_closure_attest" \ + TX_FAKE_CLOSURE_TOOL="$fake_closure_tool" \ + TX_REAL_CP="$real_cp" \ + TX_REAL_MV="$real_mv" \ + TX_REAL_RM="$real_rm" \ + TX_REAL_PYTHON3="$real_python3" \ + TX_REAL_SHA256SUM="$real_sha256sum" \ + TX_REPO_ROOT="$repo_root" \ + "$wrapper" --install-dir "$install" --expected-total 7 +} + +run_crash() { + case_root="$1" + boundary="$2" + set +e + invoke_wrapper "$case_root" "$boundary" 0 '' '' >"$case_root/crash.out" 2>&1 + status=$? + set -e + [ "$status" -ne 0 ] || fail "$boundary fault unexpectedly committed" + [ -f "$case_root/hook.log" ] || fail "$boundary fault hook was not reached" + [ "$(cat "$case_root/hook.log")" = "$boundary" ] || + fail "$boundary fault hook was reached more than once or at the wrong boundary" +} + +recover_rollback() { + case_root="$1" + set +e + invoke_wrapper "$case_root" '' 1 '' '' >"$case_root/recover.out" 2>&1 + status=$? + set -e + [ "$status" -ne 0 ] || fail "rollback recovery unexpectedly reached a new commit" + assert_snapshot "$case_root/install" "$old_snapshot" + assert_transaction_clean "$case_root" +} + +recover_committed() { + case_root="$1" + cargo_count_before=0 + if [ -f "$case_root/cargo.log" ]; then + cargo_count_before="$(wc -l <"$case_root/cargo.log" | tr -d ' ')" + fi + invoke_wrapper "$case_root" '' 1 '' '' >"$case_root/recover.out" 2>&1 || + fail "admitted generation recovery failed" + assert_snapshot "$case_root/install" "$new_snapshot" + assert_transaction_clean "$case_root" + completed_count="$(find "$case_root/work/runtime/publication-locks" -type f -name '*.completed' | wc -l | tr -d ' ')" + [ "$completed_count" -eq 1 ] || fail "admission recovery did not publish one completion record" + cargo_count_after="$(wc -l <"$case_root/cargo.log" | tr -d ' ')" + [ "$cargo_count_after" -eq "$cargo_count_before" ] || + fail "admission recovery unexpectedly rebuilt the generation" +} + +wait_for_path() { + path="$1" + attempts=0 + while [ ! -e "$path" ]; do + attempts=$((attempts + 1)) + [ "$attempts" -lt 500 ] || fail "timed out waiting for $path" + sleep 0.02 + done +} + +# Establish exact old and new byte generations once. Every crash fixture is +# separately created and compared against these deterministic snapshots. +golden="$test_root/golden" +create_fixture "$golden" +old_snapshot="$test_root/old.snapshot" +new_snapshot="$test_root/new.snapshot" +write_snapshot "$golden/install" "$old_snapshot" +invoke_wrapper "$golden" '' 0 '' '' >"$golden/commit.out" 2>&1 || + fail "golden transaction failed" +write_snapshot "$golden/install" "$new_snapshot" +assert_transaction_clean "$golden" + +init_case="$test_root/crash-init" +create_fixture "$init_case" +run_crash "$init_case" init +[ -d "$init_case/install/.oliphaunt-sealed-export-closure.initializing" ] || + fail "initialization crash lacks the durable initializing tree" +assert_absent "$init_case/install/.oliphaunt-sealed-export-closure.pending" +assert_snapshot "$init_case/install" "$old_snapshot" +recover_rollback "$init_case" + +partial_backup_case="$test_root/crash-partial-backup" +create_fixture "$partial_backup_case" +run_crash "$partial_backup_case" backup-partial +[ -f "$partial_backup_case/install/.oliphaunt-sealed-export-closure.pending/originals/share/postgresql/wasix-postmaster.sealed-export.final-proof.json" ] || + fail "partial-backup crash did not reach the selected durable backup" +assert_absent "$partial_backup_case/install/.oliphaunt-sealed-export-closure.pending/BACKUPS_COMPLETE" +assert_snapshot "$partial_backup_case/install" "$old_snapshot" +recover_rollback "$partial_backup_case" + +backup_case="$test_root/crash-backup" +create_fixture "$backup_case" +run_crash "$backup_case" backup +[ -f "$backup_case/install/.oliphaunt-sealed-export-closure.pending/BACKUPS_COMPLETE" ] || + fail "backup crash lacks durable BACKUPS_COMPLETE" +assert_snapshot "$backup_case/install" "$old_snapshot" +recover_rollback "$backup_case" + +deadmit_case="$test_root/crash-de-admit" +create_fixture "$deadmit_case" +run_crash "$deadmit_case" de-admit +assert_absent "$deadmit_case/install/$structure_relative" +for relative in "${publication_relatives[@]}"; do + [ "$relative" = "$structure_relative" ] || + assert_matches "$deadmit_case/install" "$old_snapshot" "$relative" +done +recover_rollback "$deadmit_case" + +payload_relatives=( + bin/postgres + share/postgresql/wasix-postmaster.sealed-export.seed-proof.json + share/postgresql/wasix-postmaster.sealed-export.final-proof.json + share/postgresql/wasix-postmaster.sealed-export.allowlist + share/postgresql/wasix-postmaster.sealed-export.start-proof.intermediate.json + share/postgresql/wasix-postmaster.sealed-export.concurrency.intermediate.receipt +) +payload_index=0 +for crashed_payload in "${payload_relatives[@]}"; do + payload_case="$test_root/crash-payload-$payload_index" + create_fixture "$payload_case" + run_crash "$payload_case" "payload:$crashed_payload" + assert_absent "$payload_case/install/$structure_relative" + check_index=0 + for relative in "${payload_relatives[@]}"; do + if [ "$check_index" -le "$payload_index" ]; then + assert_matches "$payload_case/install" "$new_snapshot" "$relative" + else + assert_matches "$payload_case/install" "$old_snapshot" "$relative" + fi + check_index=$((check_index + 1)) + done + recover_rollback "$payload_case" + payload_index=$((payload_index + 1)) +done + +ready_case="$test_root/crash-READY" +create_fixture "$ready_case" +run_crash "$ready_case" READY +[ -f "$ready_case/install/.oliphaunt-sealed-export-closure.pending/READY_TO_ADMIT" ] || + fail "READY crash lacks durable READY_TO_ADMIT" +assert_absent "$ready_case/install/$structure_relative" +for relative in "${publication_relatives[@]}"; do + [ "$relative" = "$structure_relative" ] || + assert_matches "$ready_case/install" "$new_snapshot" "$relative" +done +recover_rollback "$ready_case" + +# Interrupt rollback itself after restoring a middle payload. A third +# invocation must safely repeat the rollback from its beginning and recover +# the exact predecessor generation. +rollback_case="$test_root/crash-rollback" +create_fixture "$rollback_case" +run_crash "$rollback_case" READY +: >"$rollback_case/hook.log" +rollback_relative=share/postgresql/wasix-postmaster.sealed-export.final-proof.json +set +e +invoke_wrapper "$rollback_case" "rollback:$rollback_relative" 1 '' '' \ + >"$rollback_case/rollback-crash.out" 2>&1 +status=$? +set -e +[ "$status" -ne 0 ] || fail "rollback interruption unexpectedly completed" +[ "$(cat "$rollback_case/hook.log")" = "rollback:$rollback_relative" ] || + fail "rollback interruption did not reach the selected payload" +assert_absent "$rollback_case/install/$structure_relative" +for relative in \ + bin/postgres \ + share/postgresql/wasix-postmaster.sealed-export.seed-proof.json \ + share/postgresql/wasix-postmaster.sealed-export.final-proof.json +do + assert_matches "$rollback_case/install" "$old_snapshot" "$relative" +done +for relative in \ + share/postgresql/wasix-postmaster.sealed-export.allowlist \ + share/postgresql/wasix-postmaster.sealed-export.start-proof.intermediate.json \ + share/postgresql/wasix-postmaster.sealed-export.concurrency.intermediate.receipt +do + assert_matches "$rollback_case/install" "$new_snapshot" "$relative" +done +recover_rollback "$rollback_case" + +admission_case="$test_root/crash-admission" +create_fixture "$admission_case" +run_crash "$admission_case" admission +[ -f "$admission_case/install/.oliphaunt-sealed-export-closure.pending/READY_TO_ADMIT" ] || + fail "admission crash lacks durable READY_TO_ADMIT" +assert_snapshot "$admission_case/install" "$new_snapshot" +if find "$admission_case/work/runtime/publication-locks" -type f -name '*.completed' -print -quit 2>/dev/null | grep -q .; then + fail "admission fault occurred after completion publication" +fi +recover_committed "$admission_case" + +completion_case="$test_root/crash-completion" +create_fixture "$completion_case" +run_crash "$completion_case" completion +assert_snapshot "$completion_case/install" "$new_snapshot" +[ -d "$completion_case/install/.oliphaunt-sealed-export-closure.pending" ] || + fail "completion crash unexpectedly discarded the recovery journal" +completed_count="$(find "$completion_case/work/runtime/publication-locks" -type f -name '*.completed' | wc -l | tr -d ' ')" +[ "$completed_count" -eq 1 ] || fail "completion crash did not reach the completion rename" +recover_committed "$completion_case" + +tombstone_case="$test_root/crash-tombstone" +create_fixture "$tombstone_case" +run_crash "$tombstone_case" tombstone +assert_snapshot "$tombstone_case/install" "$new_snapshot" +assert_absent "$tombstone_case/install/.oliphaunt-sealed-export-closure.pending" +[ -d "$tombstone_case/install/.oliphaunt-sealed-export-closure.discarded" ] || + fail "tombstone crash did not preserve the renamed transaction journal" +recover_committed "$tombstone_case" + +# A linear-memory successor intentionally makes the export completion record +# stale because the module bytes changed. The wrapper must validate that +# descendant chain and return without trying to reseal its predecessor. +descendant_case="$test_root/linear-memory-descendant" +create_fixture "$descendant_case" +invoke_wrapper "$descendant_case" '' 0 '' '' >"$descendant_case/seal.out" 2>&1 || + fail "descendant predecessor seal failed" +make_linear_memory_descendant "$descendant_case" +descendant_snapshot="$test_root/descendant.snapshot" +write_snapshot "$descendant_case/install" "$descendant_snapshot" +cargo_count_before="$(wc -l <"$descendant_case/cargo.log" | tr -d ' ')" +invoke_wrapper "$descendant_case" '' 1 '' '' >"$descendant_case/recheck.out" 2>&1 || + fail "valid linear-memory descendant was not accepted" +cargo_count_after="$(wc -l <"$descendant_case/cargo.log" | tr -d ' ')" +[ "$cargo_count_after" -eq "$cargo_count_before" ] || + fail "linear-memory descendant caused its export predecessor to be resealed" +grep -Fq 'validated linear-memory descendant' "$descendant_case/recheck.out" || + fail "linear-memory descendant did not use the strict successor path" +assert_snapshot "$descendant_case/install" "$descendant_snapshot" +assert_transaction_clean "$descendant_case" + +# Hold the first invocation inside the producer after it owns the publication +# lock. A second invocation must neither run Cargo nor mutate the prefix; once +# released, it must observe the first invocation's durable completion. +concurrent_case="$test_root/concurrent" +create_fixture "$concurrent_case" +gate_ready="$concurrent_case/producer.gate-ready" +gate_release="$concurrent_case/producer.gate-release" +invoke_wrapper "$concurrent_case" '' 0 "$gate_ready" "$gate_release" \ + >"$concurrent_case/first.out" 2>&1 & +first_pid=$! +active_pids="$active_pids $first_pid" +wait_for_path "$gate_ready" +invoke_wrapper "$concurrent_case" '' 0 '' '' \ + >"$concurrent_case/second.out" 2>&1 & +second_pid=$! +active_pids="$active_pids $second_pid" +sleep 0.2 +kill -0 "$second_pid" 2>/dev/null || fail "second invocation did not wait for the publication lock" +cargo_count="$(wc -l <"$concurrent_case/cargo.log" | tr -d ' ')" +[ "$cargo_count" -eq 1 ] || fail "concurrent invocation entered the producer while the lock was held" +assert_snapshot "$concurrent_case/install" "$old_snapshot" +: >"$gate_release" +wait "$first_pid" || fail "first concurrent invocation failed" +wait "$second_pid" || fail "second concurrent invocation failed" +active_pids="" +cargo_count="$(wc -l <"$concurrent_case/cargo.log" | tr -d ' ')" +[ "$cargo_count" -eq 1 ] || fail "second concurrent invocation rebuilt an already committed generation" +grep -Fq 'validated existing sealed export closure' "$concurrent_case/second.out" || + fail "second concurrent invocation did not validate the committed generation" +assert_snapshot "$concurrent_case/install" "$new_snapshot" +assert_transaction_clean "$concurrent_case" + +printf 'sealed export transaction crash-recovery and exclusion tests passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-linear-memory.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-linear-memory.sh new file mode 100755 index 00000000..4f8fd007 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-linear-memory.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +usage() { + cat <<'EOF' +Usage: seal-wasix-linear-memory.sh [options] + +Seal every installed WASIX WebAssembly module to the versioned product memory +ABI after all code-rewriting passes have completed. + +Options: + --install-dir DIR WASIX PostgreSQL prefix (default: WASIX_INSTALL_DIR) + --predecessor-receipt FILE Exact sealed-export structural receipt + -h, --help Show this help +EOF +} + +fail() { + printf 'WASIX linear-memory sealer: %s\n' "$*" >&2 + exit 2 +} + +install_dir="$WASIX_INSTALL_DIR" +predecessor_receipt="" +while [ "$#" -gt 0 ]; do + case "$1" in + --install-dir|--predecessor-receipt) + option="$1" + shift + [ "$#" -gt 0 ] || fail "$option requires a value" + case "$option" in + --install-dir) install_dir="$1" ;; + --predecessor-receipt) predecessor_receipt="$1" ;; + esac + ;; + -h|--help) + usage + exit 0 + ;; + *) fail "unknown argument: $1" ;; + esac + shift +done + +[ -n "$predecessor_receipt" ] || fail '--predecessor-receipt is required' +for command in find flock od python3 sha256sum sort; do + fresh_require_command "$command" +done +[ -d "$install_dir" ] && [ ! -L "$install_dir" ] || + fail "missing non-symlink install prefix: $install_dir" +install_dir="$(cd "$install_dir" && pwd -P)" +fresh_require_managed_generated_path "$install_dir" WASIX_INSTALL_DIR + +stage="$install_dir/.oliphaunt-linear-memory.pending" +fresh_require_managed_generated_path "$stage" linear-memory-stage +transaction_tool="$FRESH_ROOT/lib/linear_memory_transaction.py" +[ -f "$transaction_tool" ] && [ ! -L "$transaction_tool" ] || + fail "missing regular linear-memory transaction helper: $transaction_tool" +lock_path="$install_dir/.oliphaunt-linear-memory.lock" +exec {linear_memory_lock_fd}>"$lock_path" +chmod 0600 "$lock_path" +flock -n "$linear_memory_lock_fd" || + fail "another linear-memory transaction holds the install-prefix lock: $lock_path" +python3 "$transaction_tool" recover \ + --install-root "$install_dir" \ + --stage "$stage" >/dev/null || + fail 'could not recover an interrupted linear-memory transaction' + +[ -f "$predecessor_receipt" ] && [ ! -L "$predecessor_receipt" ] || + fail "missing regular predecessor receipt: $predecessor_receipt" +predecessor_receipt="$(cd "$(dirname "$predecessor_receipt")" && pwd -P)/$(basename "$predecessor_receipt")" +expected_predecessor="$install_dir/share/postgresql/wasix-postmaster.sealed-export.structure.receipt" +[ "$predecessor_receipt" = "$expected_predecessor" ] || + fail "predecessor receipt must be the canonical sealed-export receipt: $expected_predecessor" + +memory_tool="$FRESH_MEMORY_PROFILE_BIN" +fresh_require_memory_profile_tool "$memory_tool" "$FRESH_POSTMASTER_EXECUTOR_BUILD_RECEIPT" +aggregate_destination="$install_dir/share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json" +if [ -e "$aggregate_destination" ] || [ -L "$aggregate_destination" ]; then + [ -f "$aggregate_destination" ] && [ ! -L "$aggregate_destination" ] || + fail "existing linear-memory receipt is not a regular file: $aggregate_destination" + python3 "$FRESH_ROOT/lib/sealed_export_chain.py" \ + --install-root "$install_dir" \ + --project-root "$FRESH_ROOT" \ + --allow-linear-memory-descendant || + fail 'existing linear-memory descendant proof chain is invalid' + python3 - "$aggregate_destination" "$install_dir" "$memory_tool" <<'PY' +import json +from pathlib import Path, PurePosixPath +import subprocess +import sys + +receipt_path, install_root, tool = sys.argv[1:] +with open(receipt_path, encoding="utf-8") as stream: + receipt = json.load(stream) +for module in receipt["modules"]: + relative = module["path"] + pure = PurePosixPath(relative) + if pure.is_absolute() or any(part in ("", ".", "..") for part in pure.parts): + raise SystemExit(f"unsafe existing linear-memory module path: {relative!r}") + subprocess.run( + [tool, "verify", str(Path(install_root).joinpath(*pure.parts))], + check=True, + stdout=subprocess.DEVNULL, + ) +PY + printf 'WASIX linear-memory profile already sealed: receipt=%s\n' \ + "$aggregate_destination" + exit 0 +fi + +python3 "$FRESH_ROOT/lib/sealed_export_chain.py" \ + --install-root "$install_dir" \ + --project-root "$FRESH_ROOT" || + fail 'sealed-export predecessor proof chain is invalid' +profile_json="$($memory_tool --profile-json)" || fail 'could not read memory-tool profile' +predecessor_sha256="$(sha256sum "$predecessor_receipt" | awk '{print $1}')" +fresh_is_sha256 "$predecessor_sha256" || fail 'predecessor receipt hash is invalid' +predecessor_relative="${predecessor_receipt#"$install_dir"/}" + +python3 "$transaction_tool" init \ + --install-root "$install_dir" \ + --stage "$stage" || + fail 'could not initialize the linear-memory transaction' +index="$stage/modules.tsv" +: >"$index" +transaction_active=1 +cleanup() { + local status=$? + trap - EXIT + if [ "${transaction_active:-0}" -eq 1 ] && \ + { [ -e "$stage" ] || [ -L "$stage" ]; }; then + if ! python3 "$transaction_tool" recover \ + --install-root "$install_dir" \ + --stage "$stage" >/dev/null; then + printf 'WASIX linear-memory sealer: automatic transaction recovery failed: %s\n' \ + "$stage" >&2 + status=2 + fi + fi + exit "$status" +} +trap cleanup EXIT + +module_count=0 +module_paths="$stage/module-paths.nul" +find "$install_dir/bin" "$install_dir/lib" -type f -print0 | \ + LC_ALL=C sort -z >"$module_paths" || + fail 'could not enumerate the installed WebAssembly module closure' +while IFS= read -r -d '' module; do + magic="$(od -An -tx1 -N4 "$module" | tr -d ' \n')" + [ "$magic" = 0061736d ] || continue + relative="${module#"$install_dir"/}" + case "$relative" in + ''|/*|*/../*|../*|*/./*|./*|*//*|*$'\t'*|*$'\n'*|*$'\r'*) + fail "unsafe installed module path: $relative" + ;; + esac + output="$stage/modules/$relative" + receipt="$stage/receipts/$relative.json" + mkdir -p "$(dirname "$output")" "$(dirname "$receipt")" + "$memory_tool" seal --output "$output" --receipt "$receipt" "$module" + chmod --reference="$module" "$output" + printf '%s\t%s\n' "$relative" "${receipt#"$stage"/}" >>"$index" + module_count=$((module_count + 1)) +done <"$module_paths" +[ "$module_count" -gt 0 ] || fail 'no installed WebAssembly modules were found' + +for required in \ + bin/initdb \ + bin/postgres \ + lib/libpq.so.5.18 \ + lib/postgresql/dict_snowball.so \ + lib/postgresql/plpgsql.so +do + awk -F '\t' -v expected="$required" '$1 == expected { count += 1 } END { exit count == 1 ? 0 : 1 }' "$index" || + fail "required carrier module was not sealed exactly once: $required" +done + +aggregate="$stage/wasix-postmaster.linear-memory-profile.receipt.json" +PROFILE_JSON="$profile_json" python3 - \ + "$stage" "$index" "$predecessor_relative" "$predecessor_sha256" "$aggregate" <<'PY' +import hashlib +import json +import os +import sys +from pathlib import Path + +stage = Path(sys.argv[1]) +index = Path(sys.argv[2]) +predecessor_path = sys.argv[3] +predecessor_sha256 = sys.argv[4] +output = Path(sys.argv[5]) +profile = json.loads(os.environ["PROFILE_JSON"]) +expected_profile = { + "address-width": "wasm32", + "supported-host-pointer-width": "u64", + "maximum-pages": 4096, + "maximum-bytes": 268435456, + "static-bound-pages": 65536, + "static-offset-guard-bytes": 2147483648, + "requires-shared": True, + "requires-import": "env.memory", + "excludes-wasm32-end-wrap": True, + "static-access-lowering": "wasmer-llvm-unchecked-reservation-and-guard-v1", +} +for key, expected in expected_profile.items(): + if profile.get(key) != expected: + raise SystemExit(f"memory-tool profile mismatch for {key}: {profile.get(key)!r}") +profile_id = profile.get("id") +if profile_id != "oliphaunt.wasix-postmaster.linear-memory.wasm32-max256m-u64-static4g-guard2g.v1": + raise SystemExit(f"memory-tool profile id mismatch: {profile_id!r}") + +records = [] +for line in index.read_text(encoding="utf-8").splitlines(): + relative, receipt_relative = line.split("\t") + receipt_bytes = (stage / receipt_relative).read_bytes() + receipt = json.loads(receipt_bytes) + if receipt.get("schema") != "oliphaunt.wasix-postmaster.linear-memory-module.v1": + raise SystemExit(f"module receipt schema mismatch: {relative}") + if receipt.get("profile-id") != profile_id: + raise SystemExit(f"module profile mismatch: {relative}") + if receipt.get("source-module-sha256") is None: + raise SystemExit(f"module receipt has no predecessor hash: {relative}") + records.append({ + "path": relative, + "source-module-sha256": receipt["source-module-sha256"], + "module-sha256": receipt["module-sha256"], + "initial-pages": receipt["initial-pages"], + "maximum-pages": receipt["maximum-pages"], + "maximum-bytes": receipt["maximum-bytes"], + "shared": receipt["shared"], + "import-module": receipt["import-module"], + "import-name": receipt["import-name"], + "transformation": receipt["transformation"], + }) +records.sort(key=lambda record: record["path"]) +if len(records) != len({record["path"] for record in records}): + raise SystemExit("duplicate installed module paths") + +def closure_hash(hash_field): + digest = hashlib.sha256() + for value in ("oliphaunt.wasix-postmaster.linear-memory-install-closure.v1", hash_field): + encoded = value.encode() + digest.update(len(encoded).to_bytes(8, "big")) + digest.update(encoded) + for record in records: + for value in (record["path"], record[hash_field]): + encoded = value.encode() + digest.update(len(encoded).to_bytes(8, "big")) + digest.update(encoded) + return digest.hexdigest() + +aggregate = { + "schema": "oliphaunt.wasix-postmaster.linear-memory-install.v1", + "profile-id": profile_id, + **expected_profile, + "predecessor-export-closure-receipt": predecessor_path, + "predecessor-export-closure-receipt-sha256": predecessor_sha256, + "source-module-closure-sha256": closure_hash("source-module-sha256"), + "module-closure-sha256": closure_hash("module-sha256"), + "module-count": len(records), + "modules": records, +} +with output.open("x", encoding="utf-8", newline="\n") as stream: + json.dump(aggregate, stream, indent=2, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) +PY + +[ "$(sha256sum "$predecessor_receipt" | awk '{print $1}')" = "$predecessor_sha256" ] || + fail 'predecessor export receipt changed while modules were sealed' +while IFS=$'\t' read -r relative receipt_relative; do + "$memory_tool" verify "$stage/modules/$relative" >/dev/null +done <"$index" + +[ "$(sha256sum "$predecessor_receipt" | awk '{print $1}')" = "$predecessor_sha256" ] || + fail 'predecessor export receipt changed before transaction preparation' + +python3 "$transaction_tool" prepare \ + --install-root "$install_dir" \ + --stage "$stage" \ + --aggregate "$aggregate" || + fail 'could not prepare durable linear-memory rollback state' +python3 "$transaction_tool" publish \ + --install-root "$install_dir" \ + --stage "$stage" || + fail 'could not publish the linear-memory transaction' +transaction_active=0 +trap - EXIT +printf 'sealed WASIX linear-memory profile: modules=%s receipt=%s\n' \ + "$module_count" "$aggregate_destination" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-linear-memory.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-linear-memory.test.sh new file mode 100755 index 00000000..660f0595 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/seal-wasix-linear-memory.test.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +memory_tool="${FRESH_MEMORY_PROFILE_BIN:?set FRESH_MEMORY_PROFILE_BIN to a built oliphaunt-wasix-memory-profile}" +repo_root="$(cd "$project_root/../../../.." && pwd -P)" +mkdir -p "$repo_root/target/oliphaunt-wasix-postmaster" +test_root="$(mktemp -d "$repo_root/target/oliphaunt-wasix-postmaster/linear-memory-test.XXXXXX")" +cleanup() { + chmod -R u+w "$test_root" 2>/dev/null || true + rm -rf -- "$test_root" +} +trap cleanup EXIT + +make_fixture() { + local name="$1" + local root="$test_root/$name" + local receipt="$root/executor.receipt" + mkdir -p \ + "$root/install/bin" \ + "$root/install/lib/postgresql" \ + "$root/install/share/postgresql" + python3 - "$root" <<'PY' +import os +import sys +from pathlib import Path + +root = Path(sys.argv[1]) +module = bytes.fromhex( + "0061736d01000000" + "0212" + "01" + "03656e76" + "066d656d6f7279" + "02" + "03" + "01" + "808004" +) +for relative in ( + "bin/initdb", + "bin/postgres", + "lib/libpq.so.5.18", + "lib/postgresql/dict_snowball.so", + "lib/postgresql/plpgsql.so", +): + path = root / "install" / relative + path.write_bytes(module) + os.chmod(path, 0o755) +PY + python3 "$project_root/testdata/make-sealed-export-fixture.py" \ + --install-root "$root/install" \ + --project-root "$project_root" + memory_hash="$(sha256sum "$memory_tool" | awk '{print $1}')" + python3 - "$receipt" "$memory_hash" <<'PY' +import sys + +path, memory_hash = sys.argv[1:] +fields = [ + ("schema", "oliphaunt.wasix-postmaster.postmaster-executor-build.v3"), + ("build_recipe_sha256", "1" * 64), + ("wasmer_build_receipt_sha256", "2" * 64), + ("wasmer_source_commit", "3" * 40), + ("wasmer_patch_sha256", "4" * 64), + ("wasmer_prepared_signature_sha256", "5" * 64), + ("wasmer_cargo_lock_sha256", "6" * 64), + ("runtime_abi_id", "7" * 64), + ("artifact_abi_version", "21"), + ("executor_package", "oliphaunt-wasix-postmaster-executor"), + ("executor_binary", "oliphaunt-wasix-postmaster-executor"), + ("executor_features", "product-executor"), + ("executor_role", "postmaster-product"), + ("runtime_policy_id", "fixture"), + ("cli_contract", "fixture"), + ("executor_binary_sha256", "8" * 64), + ("start_proof_binary", "oliphaunt-wasix-start-proof"), + ("start_proof_features", "start-proof-tool"), + ("start_proof_policy", "fixture"), + ("start_proof_binary_sha256", "9" * 64), + ("memory_profile_binary", "oliphaunt-wasix-memory-profile"), + ("memory_profile_features", "memory-profile-tool"), + ("linear_memory_profile_id", "oliphaunt.wasix-postmaster.linear-memory.wasm32-max256m-u64-static4g-guard2g.v1"), + ("memory_profile_binary_sha256", memory_hash), + ("postmaster_compiler_binary", "oliphaunt-wasix-postmaster-compiler"), + ("postmaster_compiler_features", "product-compiler"), + ("compiler_cpu_policy", "generic-baseline"), + ("compiler_cpu_features", "none"), + ("postmaster_compiler_binary_sha256", "a" * 64), + ("host_platform", "fixture"), + ("host_abi", "fixture"), + ("rustc_host", "fixture"), + ("rustc_version", "fixture"), +] +with open(path, "x", encoding="utf-8", newline="\n") as stream: + for key, value in fields: + stream.write(f"{key}={value}\n") +PY + printf '%s\n' "$root" +} + +invoke() { + local root="$1" + FRESH_WORK_ROOT="$test_root/work" \ + WASIX_INSTALL_DIR="$root/install" \ + FRESH_MEMORY_PROFILE_BIN="$memory_tool" \ + FRESH_POSTMASTER_EXECUTOR_BUILD_RECEIPT="$root/executor.receipt" \ + "$project_root/bin/seal-wasix-linear-memory.sh" \ + --install-dir "$root/install" \ + --predecessor-receipt \ + "$root/install/share/postgresql/wasix-postmaster.sealed-export.structure.receipt" +} + +success_root="$(make_fixture success)" +invoke "$success_root" +python3 - "$success_root/install" "$memory_tool" <<'PY' +import json +import subprocess +import sys +from pathlib import Path + +root = Path(sys.argv[1]) +tool = sys.argv[2] +receipt = json.loads( + (root / "share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json").read_text() +) +assert receipt["module-count"] == 29 +assert [record["path"] for record in receipt["modules"]] == sorted( + record["path"] for record in receipt["modules"] +) +for record in receipt["modules"]: + assert record["source-module-sha256"] != record["module-sha256"] + subprocess.run([tool, "verify", root / record["path"]], check=True, stdout=subprocess.DEVNULL) +PY +receipt_before="$(sha256sum "$success_root/install/share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json" | awk '{print $1}')" +invoke "$success_root" >/dev/null +receipt_after="$(sha256sum "$success_root/install/share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json" | awk '{print $1}')" +[ "$receipt_before" = "$receipt_after" ] || { + echo 'idempotent linear-memory sealing changed the aggregate receipt' >&2 + exit 1 +} + +exec {held_lock_fd}>"$success_root/install/.oliphaunt-linear-memory.lock" +flock -n "$held_lock_fd" +if invoke "$success_root" >/dev/null 2>&1; then + echo 'linear-memory sealer ignored its install-prefix lock' >&2 + exit 1 +fi +flock -u "$held_lock_fd" +exec {held_lock_fd}>&- + +stale_root="$(make_fixture stale-staging)" +python3 "$project_root/lib/linear_memory_transaction.py" init \ + --install-root "$stale_root/install" \ + --stage "$stale_root/install/.oliphaunt-linear-memory.pending" +invoke "$stale_root" >/dev/null +[ ! -e "$stale_root/install/.oliphaunt-linear-memory.pending" ] || { + echo 'linear-memory sealer did not recover an abandoned construction stage' >&2 + exit 1 +} + +rollback_root="$(make_fixture rollback)" +before="$(sha256sum "$rollback_root/install/bin/initdb" | awk '{print $1}')" +chmod 0555 "$rollback_root/install/lib" +if invoke "$rollback_root" >/dev/null 2>&1; then + echo 'expected publication failure with a read-only later module directory' >&2 + exit 1 +fi +chmod 0755 "$rollback_root/install/lib" +after="$(sha256sum "$rollback_root/install/bin/initdb" | awk '{print $1}')" +[ "$after" = "$before" ] || { + echo 'publication rollback did not restore an earlier module' >&2 + exit 1 +} +[ ! -e "$rollback_root/install/share/postgresql/wasix-postmaster.linear-memory-profile.receipt.json" ] || { + echo 'failed publication exposed an aggregate receipt' >&2 + exit 1 +} +[ ! -e "$rollback_root/install/.oliphaunt-linear-memory.pending" ] || { + echo 'failed publication left recoverable transaction state after rollback' >&2 + exit 1 +} + +printf 'WASIX linear-memory sealer tests passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/smoke-native-oracle.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/smoke-native-oracle.sh new file mode 100755 index 00000000..07bb4b92 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/smoke-native-oracle.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +fresh_ensure_dirs + +"$FRESH_ROOT/bin/build-native-oracle.sh" >/dev/null + +pgdata="$RUN_DIR/native-oracle/pgdata" +socket_dir="$RUN_DIR/native-oracle/socket" +log_file="$REPORT_DIR/native-smoke-postgres.log" +report="$REPORT_DIR/native-smoke.md" +port="${PGPORT:-55432}" + +rm -rf "$RUN_DIR/native-oracle" +mkdir -p "$pgdata" "$socket_dir" + +fresh_write_report_header "$report" "Native Oracle Smoke" + +"$NATIVE_INSTALL_DIR/bin/initdb" -D "$pgdata" --no-locale --encoding=UTF8 >"$REPORT_DIR/native-initdb.log" 2>&1 + +"$NATIVE_INSTALL_DIR/bin/pg_ctl" \ + -D "$pgdata" \ + -l "$log_file" \ + -o "-h 127.0.0.1 -p $port -c unix_socket_directories=" \ + -w start + +cleanup() { + "$NATIVE_INSTALL_DIR/bin/pg_ctl" -D "$pgdata" -m fast -w stop >/dev/null 2>&1 || true +} +trap cleanup EXIT + +"$NATIVE_INSTALL_DIR/bin/psql" \ + "postgresql://127.0.0.1:$port/postgres" \ + -f "$FRESH_ROOT/bench/sql/core-smoke.sql" \ + >"$REPORT_DIR/native-core-smoke.out" 2>&1 + +"$NATIVE_INSTALL_DIR/bin/pg_ctl" -D "$pgdata" -m fast -w stop >/dev/null +trap - EXIT + +{ + printf '## Result\n\n' + printf -- '- Status: `pass`\n' + printf -- '- Initdb log: `%s`\n' "$REPORT_DIR/native-initdb.log" + printf -- '- Server log: `%s`\n' "$log_file" + printf -- '- SQL output: `%s`\n' "$REPORT_DIR/native-core-smoke.out" +} >>"$report" + +printf 'native oracle smoke passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/smoke-wasix-concurrent-connections.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/smoke-wasix-concurrent-connections.sh new file mode 100755 index 00000000..1e15f59b --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/smoke-wasix-concurrent-connections.sh @@ -0,0 +1,536 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +usage() { + cat <<'USAGE' +Usage: smoke-wasix-concurrent-connections.sh [options] + +Start a WASIX PostgreSQL server and fan out native psql clients against it. +The smoke checks that concurrent client connections create distinct backends, +overlap in time, and can insert/update indexed rows without client failure. + +Options: + --connections N Number of concurrent psql clients. Default: 2. + --iterations N Rows inserted and updated by each client. Default: 4. + --hold-seconds N Seconds each backend sleeps while connected. Default: 1. + --timeout SECONDS Wall timeout for the client fanout. Default: 60. + --port PORT TCP port. Default: PGPORT or 55445. + --label NAME Report/run label. Default: wasix-concurrent-connections. + --skip-build Require an existing WASIX install. + --skip-precompile Reuse the current Wasmer cache. + --postgres-guc GUC Extra postmaster -c name=value setting. May repeat. + --wasmer-arg ARG Extra wasmer run argument. May repeat. + -h, --help Show this help. +USAGE +} + +connections="${WASIX_CONCURRENT_CONNECTIONS:-2}" +iterations="${WASIX_CONCURRENT_ITERATIONS:-4}" +hold_seconds="${WASIX_CONCURRENT_HOLD_SECONDS:-1}" +client_timeout="${WASIX_CONCURRENT_TIMEOUT:-60}" +verify_timeout="${WASIX_CONCURRENT_VERIFY_TIMEOUT:-20}" +port="${PGPORT:-55445}" +label="${WASIX_CONCURRENT_LABEL:-wasix-concurrent-connections}" +skip_build=0 +skip_precompile="${WASIX_SKIP_PRECOMPILE:-0}" +postgres_gucs=() +wasmer_extra_args=() + +while [ "$#" -gt 0 ]; do + case "$1" in + --connections) + shift + if [ "$#" -eq 0 ]; then + echo "--connections requires a positive integer" >&2 + exit 2 + fi + connections="$1" + ;; + --iterations) + shift + if [ "$#" -eq 0 ]; then + echo "--iterations requires a positive integer" >&2 + exit 2 + fi + iterations="$1" + ;; + --hold-seconds) + shift + if [ "$#" -eq 0 ]; then + echo "--hold-seconds requires a numeric value" >&2 + exit 2 + fi + hold_seconds="$1" + ;; + --timeout) + shift + if [ "$#" -eq 0 ]; then + echo "--timeout requires a positive integer" >&2 + exit 2 + fi + client_timeout="$1" + ;; + --port) + shift + if [ "$#" -eq 0 ]; then + echo "--port requires a port number" >&2 + exit 2 + fi + port="$1" + ;; + --label) + shift + if [ "$#" -eq 0 ]; then + echo "--label requires a value" >&2 + exit 2 + fi + label="$1" + ;; + --skip-build) + skip_build=1 + ;; + --skip-precompile) + skip_precompile=1 + ;; + --postgres-guc) + shift + if [ "$#" -eq 0 ]; then + echo "--postgres-guc requires name=value" >&2 + exit 2 + fi + postgres_gucs+=("$1") + ;; + --wasmer-arg) + shift + if [ "$#" -eq 0 ]; then + echo "--wasmer-arg requires one wasmer run argument" >&2 + exit 2 + fi + wasmer_extra_args+=("$1") + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +case "$connections" in ''|*[!0-9]*|0) echo "--connections requires a positive integer" >&2; exit 2 ;; esac +case "$iterations" in ''|*[!0-9]*|0) echo "--iterations requires a positive integer" >&2; exit 2 ;; esac +case "$client_timeout" in ''|*[!0-9]*|0) echo "--timeout requires a positive integer" >&2; exit 2 ;; esac +case "$port" in ''|*[!0-9]*) echo "--port requires a port number" >&2; exit 2 ;; esac +case "$label" in ""|*[!A-Za-z0-9._-]*) echo "--label may only contain letters, numbers, '.', '_', and '-'" >&2; exit 2 ;; esac +case "$hold_seconds" in ''|*[!0-9.]*|*.*.*) echo "--hold-seconds requires a non-negative numeric value" >&2; exit 2 ;; esac + +fresh_ensure_dirs + +if [ ! -x "$NATIVE_INSTALL_DIR/bin/psql" ]; then + "$FRESH_ROOT/bin/build-native-oracle.sh" >/dev/null +fi +if [ ! -x "$WASIX_INSTALL_DIR/bin/postgres" ] || [ ! -x "$WASIX_INSTALL_DIR/bin/initdb" ]; then + if [ "$skip_build" -eq 1 ]; then + printf 'missing WASIX install with --skip-build: %s\n' "$WASIX_INSTALL_DIR" >&2 + exit 2 + fi + "$FRESH_ROOT/bin/build-wasix-core.sh" >/dev/null +fi + +wasmer_bin="$(fresh_wasmer_bin)" +wasmer_bin_hash="$(fresh_wasmer_bin_hash "$wasmer_bin")" +wasmer_cache_dir="$(fresh_wasmer_cache_dir "$wasmer_bin")" +wasmer_compiler="$(fresh_wasmer_compiler)" +wasmer_llvm_opt_level="${WASMER_LLVM_OPT_LEVEL:-aggressive}" +wasmer_stack_size="${WASMER_STACK_SIZE:-33554432}" +wasmer_compiler_threads="${WASMER_COMPILER_THREADS:-$(fresh_jobs)}" +fresh_require_wasmer_compiler_cli "$wasmer_bin" "$wasmer_compiler" run + +if [ "$skip_precompile" != "1" ]; then + "$FRESH_ROOT/bin/precompile-wasix-core.sh" >/dev/null +fi + +suite_root="$RUN_DIR/$label" +report_dir="$REPORT_DIR/$label" +pgdata="$suite_root/pgdata" +dev_shm="$suite_root/dev-shm" +client_sql="$suite_root/concurrent-client.sql" +initdb_log="$report_dir/initdb.log" +server_log="$report_dir/server.log" +wait_log="$report_dir/wait.log" +setup_log="$report_dir/setup.log" +verify_log="$report_dir/verify.log" +verify_sql="$suite_root/verify.sql" +summary="$report_dir/summary.md" +summary_tsv="$report_dir/summary.tsv" + +rm -rf "$suite_root" "$report_dir" +mkdir -p "$pgdata" "$dev_shm" "$report_dir" + +fresh_write_report_header "$summary" "WASIX Concurrent Connections Smoke" +{ + printf -- '- Connections: `%s`\n' "$connections" + printf -- '- Iterations per connection: `%s`\n' "$iterations" + printf -- '- Hold seconds: `%s`\n' "$hold_seconds" + printf -- '- Client fanout timeout: `%s seconds`\n' "$client_timeout" + printf -- '- Verification timeout: `%s seconds`\n' "$verify_timeout" + printf -- '- Port: `%s`\n' "$port" + printf -- '- PGDATA: `%s`\n' "$pgdata" + printf -- '- Report dir: `%s`\n' "$report_dir" + printf -- '- Wasmer binary: `%s`\n' "$wasmer_bin" + printf -- '- Wasmer binary hash: `%s`\n' "$wasmer_bin_hash" + printf -- '- Wasmer version: `%s`\n' "$(fresh_wasmer_version "$wasmer_bin" 2>/dev/null || true)" + printf -- '- Wasmer cache dir: `%s`\n' "$wasmer_cache_dir" + printf -- '- WASIX core profile: `%s`\n' "$WASIX_CORE_PROFILE" + printf -- '- WASIX install dir: `%s`\n' "$WASIX_INSTALL_DIR" + printf -- '- Pinned runtime: `%s`\n' "${FRESH_PINNED_RUNTIME_NAME:-}" + printf -- '- Wasmer compiler: `%s`\n' "$wasmer_compiler" + printf -- '- Wasmer LLVM opt level: `%s`\n' "$wasmer_llvm_opt_level" + printf -- '- WASMER_LLVM_NATIVE_CPU: `%s`\n' "${WASMER_LLVM_NATIVE_CPU:-0}" + printf -- '- WASMER_LLVM_FULL_O3_PIPELINE: `%s`\n' "${WASMER_LLVM_FULL_O3_PIPELINE:-0}" + printf -- '- WASMER_LLVM_INDIRECT_CALL_CACHE: `%s`\n' "${WASMER_LLVM_INDIRECT_CALL_CACHE:-0}" + printf -- '- Wasmer stack size: `%s`\n' "$wasmer_stack_size" + printf -- '- Wasmer compiler threads: `%s`\n' "$wasmer_compiler_threads" + printf -- '- Runtime network capability: `--net` (host networking requested; host policy still applies)\n' + printf -- '- Skip precompile: `%s`\n\n' "$skip_precompile" +} >>"$summary" +printf 'client\tstatus\tlog\n' >"$summary_tsv" + +run_logged_timeout() { + local timeout_seconds="$1" + shift + local log="$1" + shift + local pid started elapsed status + + "$@" >"$log" 2>&1 & + pid=$! + started="$(date +%s)" + while kill -0 "$pid" 2>/dev/null; do + elapsed=$(( $(date +%s) - started )) + if [ "$elapsed" -ge "$timeout_seconds" ]; then + { + printf '\ncommand timed out after %s seconds\n' "$timeout_seconds" + printf 'command:' + printf ' %q' "$@" + printf '\n' + } >>"$log" + kill "$pid" 2>/dev/null || true + sleep 0.5 + kill -9 "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + return 124 + fi + sleep 0.1 + done + if wait "$pid"; then + return 0 + fi + status=$? + return "$status" +} + +readiness_blocker_reason() { + local log="$1" + + if [ -f "$log" ] && grep -q 'could not reattach to WASIX shared memory object' "$log"; then + printf 'runtime-shared-memory-reattach' + return 0 + fi + if [ -f "$log" ] && grep -q 'failed to epoll during deep sleep - intr' "$log"; then + printf 'runtime-epoll-interrupt' + return 0 + fi + if [ -f "$log" ] && + grep -Eq 'could not listen on .*(Operation not permitted|EPERM)' "$log"; then + printf 'host-network-policy-denied' + return 0 + fi + if [ -n "$server_pid" ] && ! kill -0 "$server_pid" 2>/dev/null; then + printf 'server-exited' + return 0 + fi + printf '' +} + +cat >"$client_sql" <<'SQL' +\set ON_ERROR_STOP 1 +\pset tuples_only on +\pset format unaligned +begin; +select pg_backend_pid() as backend_pid \gset +insert into wasix_concurrent_probe(client_id, iteration, backend_pid, started_at, payload) +select :client_id, g, :backend_pid, clock_timestamp(), md5((:client_id::text || ':' || g::text)) +from generate_series(1, :iterations) as g; +select pg_sleep(:hold_seconds); +update wasix_concurrent_probe +set finished_at = clock_timestamp(), + payload = md5(payload || ':done') +where client_id = :client_id + and backend_pid = :backend_pid; +commit; +select :client_id || '|' || :backend_pid || '|' || count(*) +from wasix_concurrent_probe +where client_id = :client_id + and backend_pid = :backend_pid; +SQL + +cat >"$verify_sql" <<'SQL' +select + count(*)::int as rows_written, + count(distinct client_id)::int as clients_seen, + count(distinct backend_pid)::int as backends_seen, + count(finished_at)::int as rows_finished, + coalesce((max(started_at) <= min(finished_at))::text, 'false') as all_clients_overlapped +from wasix_concurrent_probe; +SQL + +wasmer_env=( + "WASMER_DIR=$FRESH_WORK_ROOT/tools/wasmer-home" + "WASMER_CACHE_DIR=$wasmer_cache_dir" +) +wasmer_args=( + run + --quiet +) +while IFS= read -r arg; do + wasmer_args+=("$arg") +done < <(fresh_wasmer_compiler_args_for "$wasmer_bin" run "$wasmer_compiler" "$wasmer_llvm_opt_level" "$wasmer_compiler_threads") +wasmer_args+=( + --stack-size "$wasmer_stack_size" + --enable-exceptions + --enable-threads + --net + --volume "$REPO_ROOT:$REPO_ROOT" + --volume "$WASIX_INSTALL_DIR/lib:/lib" + --volume "$dev_shm:/dev/shm" +) +if [ "${#wasmer_extra_args[@]}" -gt 0 ]; then + wasmer_args+=("${wasmer_extra_args[@]}") +fi + +env "${wasmer_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" "$WASIX_INSTALL_DIR/bin/initdb" -- \ + -D "$pgdata" \ + -A trust \ + --no-locale \ + --encoding=UTF8 \ + --no-instructions \ + >"$initdb_log" 2>&1 + +server_pid="" +cleanup() { + if [ -n "$server_pid" ] && kill -0 "$server_pid" 2>/dev/null; then + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi +} +trap cleanup EXIT + +postgres_args=( + -D "$pgdata" + -h 127.0.0.1 + -p "$port" + -c unix_socket_directories= + -c "max_connections=$((connections + 16))" + -c log_connections=on + -c log_disconnections=on +) +if [ "${#postgres_gucs[@]}" -gt 0 ]; then + for guc in "${postgres_gucs[@]}"; do + postgres_args+=(-c "$guc") + done +fi + +set +e +env "${wasmer_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" "$WASIX_INSTALL_DIR/bin/postgres" -- \ + "${postgres_args[@]}" >"$server_log" 2>&1 & +server_pid=$! +set -e + +conn="postgresql://wasix@127.0.0.1:$port/postgres" +: >"$wait_log" +ready=0 +readiness_reason="" +for _ in $(seq 1 300); do + if "$NATIVE_INSTALL_DIR/bin/psql" "$conn" -X -q -c 'select 1' >>"$wait_log" 2>&1; then + ready=1 + break + fi + readiness_reason="$(readiness_blocker_reason "$server_log")" + if [ -n "$readiness_reason" ]; then + printf 'WASIX server readiness blocker: %s\n' "$readiness_reason" >>"$wait_log" + break + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + echo "WASIX server exited before readiness" >>"$wait_log" + readiness_reason="server-exited" + break + fi + sleep 0.1 +done +if [ "$ready" -ne 1 ]; then + { + printf '\n## Result\n\n' + printf -- '- Status: `blocked`\n' + printf -- '- Gate: `readiness`\n' + printf -- '- Reason: `%s`\n' "${readiness_reason:-timeout}" + printf -- '- Server log: `%s`\n' "$server_log" + printf -- '- Wait log: `%s`\n' "$wait_log" + } >>"$summary" + echo "WASIX concurrent smoke blocked at readiness; see $server_log and $wait_log" >&2 + exit 2 +fi + +"$NATIVE_INSTALL_DIR/bin/psql" "$conn" -X -q -v ON_ERROR_STOP=1 >"$setup_log" 2>&1 <<'SQL' +drop table if exists wasix_concurrent_probe; +create table wasix_concurrent_probe ( + client_id integer not null, + iteration integer not null, + backend_pid integer not null, + started_at timestamptz not null, + finished_at timestamptz, + payload text not null, + primary key (client_id, iteration) +); +create index wasix_concurrent_probe_backend_idx on wasix_concurrent_probe (backend_pid); +SQL + +client_pids=() +client_logs=() +for client in $(seq 1 "$connections"); do + client_log="$report_dir/client-$client.log" + client_logs+=("$client_log") + PGCONNECT_TIMEOUT=10 "$NATIVE_INSTALL_DIR/bin/psql" "$conn" \ + -X -q \ + -v ON_ERROR_STOP=1 \ + -v "client_id=$client" \ + -v "iterations=$iterations" \ + -v "hold_seconds=$hold_seconds" \ + -f "$client_sql" \ + >"$client_log" 2>&1 & + client_pids+=("$!") +done + +deadline=$(( $(date +%s) + client_timeout )) +timed_out=0 +while :; do + running=0 + for pid in "${client_pids[@]}"; do + if kill -0 "$pid" 2>/dev/null; then + running=1 + break + fi + done + if [ "$running" -eq 0 ]; then + break + fi + if [ "$(date +%s)" -ge "$deadline" ]; then + timed_out=1 + for pid in "${client_pids[@]}"; do + kill "$pid" 2>/dev/null || true + done + sleep 0.5 + for pid in "${client_pids[@]}"; do + kill -9 "$pid" 2>/dev/null || true + done + break + fi + sleep 0.1 +done + +client_status=0 +for index in "${!client_pids[@]}"; do + pid="${client_pids[$index]}" + client=$((index + 1)) + log="${client_logs[$index]}" + set +e + wait "$pid" + status=$? + set -e + if [ "$timed_out" -eq 1 ] && [ "$status" -eq 143 ]; then + status=124 + fi + printf '%s\t%s\t%s\n' "$client" "$status" "$log" >>"$summary_tsv" + if [ "$status" -ne 0 ]; then + client_status=1 + fi +done + +set +e +run_logged_timeout "$verify_timeout" "$verify_log" \ + "$NATIVE_INSTALL_DIR/bin/psql" "$conn" \ + -X -q -A -t -F $'\t' \ + -v ON_ERROR_STOP=1 \ + -f "$verify_sql" +verify_status=$? +set -e +verify_line="" +if [ "$verify_status" -eq 0 ]; then + verify_line="$(tail -n 1 "$verify_log")" +fi + +expected_rows=$((connections * iterations)) +rows_written="" +clients_seen="" +backends_seen="" +rows_finished="" +all_clients_overlapped="" +if [ "$verify_status" -eq 0 ]; then + IFS=$'\t' read -r rows_written clients_seen backends_seen rows_finished all_clients_overlapped <>"$summary" + +if [ "$overall_status" -ne 0 ]; then + printf 'failed: WASIX concurrent connections smoke; see %s\n' "$summary" >&2 +else + printf 'passed: WASIX concurrent connections smoke; see %s\n' "$summary" +fi +exit "$overall_status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/smoke-wasix-core.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/smoke-wasix-core.sh new file mode 100755 index 00000000..b497f902 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/smoke-wasix-core.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +fresh_ensure_dirs + +report="$REPORT_DIR/wasix-core-smoke.md" +initdb_log="$REPORT_DIR/wasix-initdb.log" +prewarm_log="$REPORT_DIR/wasix-prewarm.log" +fresh_write_report_header "$report" "WASIX Core Smoke" + +set +e +wasmer_bin="$(fresh_wasmer_bin 2>>"$report")" +wasmer_status=$? +set -e +if [ "$wasmer_status" -ne 0 ]; then + { + printf '## Result\n\n' + printf -- '- Status: `blocked`\n' + printf -- '- Blocker: Wasmer CLI not installed.\n\n' + printf 'Run `%s/runtime/bin/build-runtime.sh`, or set `WASMER_BIN` and `WASMER_BUILD_RECEIPT` to a matching pinned build.\n' "$FRESH_ROOT" + } >>"$report" + echo "blocked: Wasmer CLI not found; see $report" >&2 + exit 2 +fi + +if [ ! -x "$WASIX_INSTALL_DIR/bin/initdb" ]; then + "$FRESH_ROOT/bin/build-wasix-core.sh" +fi + +wasmer_bin_hash="$(fresh_wasmer_bin_hash "$wasmer_bin")" +wasmer_cache_dir="$(fresh_wasmer_cache_dir "$wasmer_bin")" +wasmer_compiler="$(fresh_wasmer_compiler)" +wasmer_llvm_opt_level="${WASMER_LLVM_OPT_LEVEL:-aggressive}" +wasmer_stack_size="${WASMER_STACK_SIZE:-33554432}" +wasmer_compiler_threads="${WASMER_COMPILER_THREADS:-$(fresh_jobs)}" +fresh_require_wasmer_compiler_cli "$wasmer_bin" "$wasmer_compiler" run + +pgdata="$RUN_DIR/wasix-core/pgdata" +dev_shm="$RUN_DIR/wasix-core/dev-shm" +rm -rf "$pgdata" +rm -rf "$dev_shm" +mkdir -p "$RUN_DIR/wasix-core" "$dev_shm" + +wasmer_env=( + "WASMER_DIR=$FRESH_WORK_ROOT/tools/wasmer-home" + "WASMER_CACHE_DIR=$wasmer_cache_dir" +) +wasmer_args=( + run + --quiet +) +while IFS= read -r arg; do + wasmer_args+=("$arg") +done < <(fresh_wasmer_compiler_args_for "$wasmer_bin" run "$wasmer_compiler" "$wasmer_llvm_opt_level" "$wasmer_compiler_threads") +wasmer_args+=( + --stack-size "$wasmer_stack_size" + --enable-exceptions + --enable-threads + --net + --volume "$FRESH_WORK_ROOT:$FRESH_WORK_ROOT" + --volume "$WASIX_INSTALL_DIR/lib:/lib" + --volume "$dev_shm:/dev/shm" +) + +{ + printf '## Runtime\n\n' + printf -- '- Wasmer binary: `%s`\n' "$wasmer_bin" + printf -- '- Wasmer binary hash: `%s`\n' "$wasmer_bin_hash" + printf -- '- Wasmer version: `%s`\n' "$(fresh_wasmer_version "$wasmer_bin" 2>/dev/null || true)" + printf -- '- Wasmer cache dir: `%s`\n' "$wasmer_cache_dir" + printf -- '- WASIX core profile: `%s`\n' "$WASIX_CORE_PROFILE" + printf -- '- WASIX install dir: `%s`\n' "$WASIX_INSTALL_DIR" + printf -- '- Wasmer compiler: `%s`\n' "$wasmer_compiler" + printf -- '- Wasmer LLVM opt level: `%s`\n' "$wasmer_llvm_opt_level" + printf -- '- WASMER_LLVM_FULL_O3_PIPELINE: `%s`\n' "${WASMER_LLVM_FULL_O3_PIPELINE:-0}" + printf -- '- WASMER_LLVM_INDIRECT_CALL_CACHE: `%s`\n' "${WASMER_LLVM_INDIRECT_CALL_CACHE:-0}" + printf -- '- Wasmer stack size: `%s`\n' "$wasmer_stack_size" + printf -- '- Wasmer compiler threads: `%s`\n' "$wasmer_compiler_threads" + printf -- '- Guest mapping: `%s:%s`\n' "$FRESH_WORK_ROOT" "$FRESH_WORK_ROOT" + printf -- '- Library mapping: `%s:/lib`\n' "$WASIX_INSTALL_DIR/lib" + printf -- '- Shared-memory mapping: `%s:/dev/shm`\n' "$dev_shm" + printf -- '- Prewarm log: `%s`\n' "$prewarm_log" + printf -- '- Initdb log: `%s`\n\n' "$initdb_log" +} >>"$report" + +warm_wasix_binary() { + local binary="$1" + shift + + { + printf '## %s %s\n\n' "$(basename "$binary")" "$*" + } >>"$prewarm_log" + + env "${wasmer_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" "$binary" -- "$@" \ + >>"$prewarm_log" 2>&1 +} + +if [ "${WASIX_SKIP_PREWARM:-0}" != "1" ]; then + : >"$prewarm_log" + if [ "${WASIX_SKIP_PRECOMPILE:-0}" != "1" ]; then + "$FRESH_ROOT/bin/precompile-wasix-core.sh" >>"$prewarm_log" 2>&1 + fi + set +e + warm_wasix_binary "$WASIX_INSTALL_DIR/bin/postgres" --version + prewarm_postgres_status=$? + warm_wasix_binary "$WASIX_INSTALL_DIR/bin/initdb" --version + prewarm_initdb_status=$? + set -e + + if [ "$prewarm_postgres_status" -ne 0 ] || [ "$prewarm_initdb_status" -ne 0 ]; then + { + printf '## Result\n\n' + printf -- '- Status: `blocked`\n' + printf -- '- Gate: `prewarm`\n' + printf -- '- postgres --version exit code: `%s`\n' "$prewarm_postgres_status" + printf -- '- initdb --version exit code: `%s`\n\n' "$prewarm_initdb_status" + printf 'The runtime could not load the core PostgreSQL tools. Inspect `%s`.\n' "$prewarm_log" + } >>"$report" + printf 'blocked: WASIX prewarm failed; see %s and %s\n' "$report" "$prewarm_log" >&2 + exit 2 + fi +else + : >"$prewarm_log" + printf 'prewarm skipped because WASIX_SKIP_PREWARM=1\n' >"$prewarm_log" +fi + +: >"$initdb_log" +set +e +env "${wasmer_env[@]}" \ + "$wasmer_bin" "${wasmer_args[@]}" "$WASIX_INSTALL_DIR/bin/initdb" -- \ + -D "$pgdata" \ + -A trust \ + --no-locale \ + --encoding=UTF8 \ + --no-instructions \ + >"$initdb_log" 2>&1 +initdb_status=$? +set -e + +if [ "$initdb_status" -ne 0 ]; then + blocker="initdb failed under WASIX; inspect the initdb log" + if grep -q '/bin/sh' "$initdb_log" || grep -q 'could not execute command ".*postgres.*-V"' "$initdb_log"; then + blocker="WASIX popen/system currently routes through /bin/sh, but no WASIX shell is mounted; initdb still needs a real process-spawn path for PostgreSQL subcommands" + elif grep -q 'program "postgres" is needed by initdb' "$initdb_log"; then + blocker="initdb could not validate the sibling postgres executable through PostgreSQL's normal lookup path" + fi + + { + printf '## Result\n\n' + printf -- '- Status: `blocked`\n' + printf -- '- Gate: `initdb`\n' + printf -- '- Exit code: `%s`\n' "$initdb_status" + printf -- '- Blocker: %s.\n\n' "$blocker" + printf 'This is a runtime/process execution blocker for the WASIX core loop, not a PostgreSQL success shim candidate.\n' + } >>"$report" + + printf 'blocked: WASIX initdb failed; see %s and %s\n' "$report" "$initdb_log" >&2 + exit 2 +fi + +{ + printf '## Result\n\n' + printf -- '- Status: `pass`\n' + printf -- '- Gate: `initdb`\n' + printf -- '- Data directory: `%s`\n\n' "$pgdata" + printf 'Next gate: start `%s/bin/postgres` under Wasmer and connect with `%s/bin/psql` over a real WASIX socket.\n' \ + "$WASIX_INSTALL_DIR" "$WASIX_INSTALL_DIR" +} >>"$report" + +echo "passed: WASIX initdb gate; see $report" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/stress-wasix-backend-waves.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/stress-wasix-backend-waves.sh new file mode 100755 index 00000000..68f70d3c --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/stress-wasix-backend-waves.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +attempts="${WASIX_BACKEND_WAVE_ATTEMPTS:-10}" +client_iterations="${WASIX_BACKEND_WAVE_ITERATIONS:-100000}" +timeout_seconds="${WASIX_BACKEND_WAVE_TIMEOUT:-30}" +start_port="${PGPORT:-55820}" +wait_sample_interval="${WASIX_BACKEND_WAVE_WAIT_SAMPLE_INTERVAL:-0}" +memory_map_snapshots="${WASIX_BACKEND_WAVE_MEMORY_MAP_SNAPSHOTS:-0}" +label="${WASIX_BACKEND_WAVE_LABEL:-backend-wave-$(date -u +%Y%m%dT%H%M%SZ)}" +wasmer_args=() + +usage() { + cat <<'USAGE' +Usage: stress-wasix-backend-waves.sh [options] + +Repeat the sustained read -> mixed-write -> indexed-update sequence in one +WASIX postmaster per attempt. This targets nondeterministic logical-backend, +WAL-wakeup, and wait-registration liveness failures. The first failed attempt +stops the gate while preserving its PGDATA, logs, resource samples, and client +statuses below target/oliphaunt-wasix-postmaster/. + +Options: + --attempts N Number of fresh-postmaster attempts. Default: 10. + --iterations N Operations per client/workload. Default: 100000. + --timeout SECONDS Per phase/client timeout. Default: 30. + --start-port PORT PostgreSQL port. Default: PGPORT or 55820. + --wait-sample-interval S pg_stat_activity sample interval; 0 disables it. + --memory-map-snapshots Capture pmap/vmmap at workload boundaries. + --wasmer-arg ARG Extra Wasmer run argument. May repeat. + --label NAME Unique report label. Default: timestamped. + -h, --help Show this help. +USAGE +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --attempts) + [ "$#" -ge 2 ] || { echo '--attempts requires a value' >&2; exit 2; } + attempts="$2" + shift 2 + ;; + --iterations) + [ "$#" -ge 2 ] || { echo '--iterations requires a value' >&2; exit 2; } + client_iterations="$2" + shift 2 + ;; + --timeout) + [ "$#" -ge 2 ] || { echo '--timeout requires a value' >&2; exit 2; } + timeout_seconds="$2" + shift 2 + ;; + --start-port) + [ "$#" -ge 2 ] || { echo '--start-port requires a value' >&2; exit 2; } + start_port="$2" + shift 2 + ;; + --wait-sample-interval) + [ "$#" -ge 2 ] || { echo '--wait-sample-interval requires a value' >&2; exit 2; } + wait_sample_interval="$2" + shift 2 + ;; + --memory-map-snapshots) + memory_map_snapshots=1 + shift + ;; + --wasmer-arg) + [ "$#" -ge 2 ] || { echo '--wasmer-arg requires a value' >&2; exit 2; } + wasmer_args+=("$2") + shift 2 + ;; + --label) + [ "$#" -ge 2 ] || { echo '--label requires a value' >&2; exit 2; } + label="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +case "$attempts" in ''|*[!0-9]*|0) echo '--attempts requires a positive integer' >&2; exit 2 ;; esac +case "$client_iterations" in ''|*[!0-9]*|0) echo '--iterations requires a positive integer' >&2; exit 2 ;; esac +case "$timeout_seconds" in ''|*[!0-9]*|0) echo '--timeout requires a positive integer' >&2; exit 2 ;; esac +case "$start_port" in ''|*[!0-9]*|0) echo '--start-port requires a positive integer' >&2; exit 2 ;; esac +case "$wait_sample_interval" in ''|*[!0-9.]*) echo '--wait-sample-interval requires a non-negative number' >&2; exit 2 ;; esac +case "$label" in ''|*[!A-Za-z0-9._-]*) echo '--label has unsafe characters' >&2; exit 2 ;; esac +case "$memory_map_snapshots" in 0|1) ;; *) echo 'memory-map snapshots must be 0 or 1' >&2; exit 2 ;; esac + +fresh_ensure_dirs +stress_report_dir="$REPORT_DIR/backend-wave-stress/$label" +summary="$stress_report_dir/summary.md" +attempts_tsv="$stress_report_dir/attempts.tsv" +[ ! -e "$stress_report_dir" ] || { + printf 'refusing to overwrite backend-wave evidence: %s\n' "$stress_report_dir" >&2 + exit 2 +} +mkdir -p "$stress_report_dir" +printf 'attempt\tstatus\treport\n' >"$attempts_tsv" + +write_summary() { + local result="$1" + local passed="$2" + local failed_attempt="${3:-}" + + { + printf '# WASIX backend-wave liveness stress\n\n' + printf -- '- Generated: `%s`\n' "$(fresh_timestamp)" + printf -- '- Result: `%s`\n' "$result" + printf -- '- Attempts requested: `%s`\n' "$attempts" + printf -- '- Attempts passed: `%s`\n' "$passed" + printf -- '- First failed attempt: `%s`\n' "${failed_attempt:-none}" + printf -- '- Connections: `4`\n' + printf -- '- Client iterations: `%s`\n' "$client_iterations" + printf -- '- Workload sequence: `indexed-read mixed-write indexed-update`\n' + printf -- '- Per-phase timeout: `%s seconds`\n' "$timeout_seconds" + printf -- '- pg_stat_activity sample interval: `%s seconds`\n' "$wait_sample_interval" + printf -- '- pg_stat_activity sampler: `persistent-connection`\n' + printf -- '- Memory-map snapshots: `%s`\n' "$memory_map_snapshots" + if [ "${#wasmer_args[@]}" -eq 0 ]; then + printf -- '- Extra Wasmer run arguments: `none`\n' + else + printf -- '- Extra Wasmer run arguments: `%s`\n' "${wasmer_args[*]}" + fi + printf -- '- Wasmer SHA-256: `%s`\n' "$(fresh_wasmer_bin_hash "$(fresh_wasmer_bin)")" + printf -- '- Attempt index: `%s`\n' "$attempts_tsv" + } >"$summary" +} + +for ((attempt = 1; attempt <= attempts; attempt++)); do + attempt_label="$label-attempt-$attempt" + command=( + "$FRESH_ROOT/bin/bench-wasix-concurrent-query-suite.sh" + --connections 4 + --iterations "$client_iterations" + --rows 100000 + --workloads 'indexed-read mixed-write indexed-update' + --skip-native + --skip-build + --skip-precompile + --timeout "$timeout_seconds" + --resource-interval 0.2 + --pg-wait-sample-interval "$wait_sample_interval" + --postgres-guc log_lock_waits=on + --postgres-guc deadlock_timeout=100ms + --start-port "$start_port" + --label "$attempt_label" + ) + [ "$memory_map_snapshots" -eq 0 ] || command+=(--memory-map-snapshots) + for wasmer_arg in "${wasmer_args[@]}"; do + command+=(--wasmer-arg "$wasmer_arg") + done + + set +e + "${command[@]}" + status=$? + set -e + report="$REPORT_DIR/concurrent-query-suite/$attempt_label/summary.md" + printf '%s\t%s\t%s\n' "$attempt" "$status" "$report" >>"$attempts_tsv" + if [ "$status" -ne 0 ]; then + write_summary fail $((attempt - 1)) "$attempt" + printf 'failed backend-wave attempt %s/%s; preserved evidence at %s\n' \ + "$attempt" "$attempts" "$report" >&2 + exit "$status" + fi +done + +write_summary pass "$attempts" +printf 'passed: %s/%s backend-wave attempts; see %s\n' "$attempts" "$attempts" "$summary" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/stress-wasix-initdb.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/stress-wasix-initdb.sh new file mode 100755 index 00000000..04ba4d50 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/stress-wasix-initdb.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +iterations="${WASIX_INITDB_STRESS_ITERATIONS:-20}" +label="${WASIX_INITDB_STRESS_LABEL:-wasix-initdb-stress}" + +usage() { + cat <<'USAGE' +Usage: stress-wasix-initdb.sh [--iterations N] [--label NAME] + +Repeatedly creates a clean PostgreSQL cluster under the patched WASIX runtime. +Every iteration must pass and produce a log without lifecycle/runtime warnings. +USAGE +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --iterations) + [ "$#" -ge 2 ] || { echo '--iterations requires a value' >&2; exit 2; } + iterations="$2" + shift 2 + ;; + --label) + [ "$#" -ge 2 ] || { echo '--label requires a value' >&2; exit 2; } + label="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +case "$iterations" in ''|*[!0-9]*|0) echo '--iterations requires a positive integer' >&2; exit 2 ;; esac +case "$label" in ''|*[!A-Za-z0-9._-]*) echo '--label has unsafe characters' >&2; exit 2 ;; esac + +fresh_ensure_dirs +wasmer_bin="$(fresh_wasmer_bin)" +stress_report_dir="$REPORT_DIR/$label" +stress_run_dir="$RUN_DIR/$label" +summary="$stress_report_dir/summary.md" +forbidden='WARNING|ERROR|FATAL|PANIC|proc_join failed|uninitialized element|signal handler runtime error' + +mkdir -p "$stress_report_dir/iterations" "$stress_run_dir/iterations" + +for ((iteration = 1; iteration <= iterations; iteration++)); do + iteration_report="$stress_report_dir/iterations/$iteration" + iteration_run="$stress_run_dir/iterations/$iteration" + + REPORT_DIR="$iteration_report" \ + RUN_DIR="$iteration_run" \ + WASIX_SKIP_PREWARM=1 \ + "$FRESH_ROOT/bin/smoke-wasix-core.sh" + + if grep -En "$forbidden" "$iteration_report/wasix-initdb.log"; then + printf 'initdb stress iteration %s produced a forbidden runtime warning\n' "$iteration" >&2 + exit 1 + fi +done + +{ + printf '# WASIX initdb lifecycle stress\n\n' + printf -- '- Generated: `%s`\n' "$(fresh_timestamp)" + printf -- '- Iterations: `%s`\n' "$iterations" + printf -- '- Passed: `%s`\n' "$iterations" + printf -- '- Clean logs: `%s`\n' "$iterations" + printf -- '- Wasmer binary: `%s`\n' "$wasmer_bin" + printf -- '- Wasmer SHA-256: `%s`\n' "$(fresh_wasmer_bin_hash "$wasmer_bin")" + printf -- '- Exact libc sysroot: `%s`\n' "$WASIXCC_SYSROOT" + printf -- '- Result: `pass`\n' +} >"$summary" + +printf 'passed: %s/%s clean WASIX initdb iterations; see %s\n' "$iterations" "$iterations" "$summary" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-checkpoint-qualification.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-checkpoint-qualification.py new file mode 100755 index 00000000..c06d64f9 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-checkpoint-qualification.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Summarize balanced native/WASIX checkpoint-overlap sample receipts.""" + +from __future__ import annotations + +import argparse +import csv +import re +import statistics +import sys +from pathlib import Path + + +HEADER = [ + "block", + "pair", + "position", + "target", + "label", + "harness_status", + "sample_status", + "p95_ns", + "p99_ns", + "report_dir", + "settings_sha256", +] +SHA256 = re.compile(r"^[0-9a-f]{64}$") + + +class EvidenceError(ValueError): + pass + + +def parser() -> argparse.ArgumentParser: + result = argparse.ArgumentParser() + result.add_argument("--samples", type=Path, required=True) + result.add_argument("--output", type=Path, required=True) + result.add_argument("--result", type=Path, required=True) + result.add_argument("--mode", choices=("smoke", "diagnostic", "promotional"), required=True) + result.add_argument("--blocks", type=int, required=True) + result.add_argument("--max-median-p95-ratio", type=float, default=2.0) + result.add_argument("--max-median-p99-ratio", type=float, default=2.5) + result.add_argument("--policy-sha256", required=True) + result.add_argument("--carrier-identity", required=True) + result.add_argument("--native-identity", required=True) + result.add_argument("--memory-status", choices=("passed", "failed", "not-run"), required=True) + return result + + +def write(path: Path, rows: list[list[object]]) -> None: + if path.exists() or path.is_symlink(): + raise EvidenceError(f"refusing to replace output: {path}") + with path.open("x", newline="", encoding="utf-8") as stream: + csv.writer(stream, delimiter="\t", lineterminator="\n").writerows(rows) + + +def main() -> int: + args = parser().parse_args() + try: + if not args.samples.is_file() or args.samples.is_symlink(): + raise EvidenceError("samples must be regular and non-symlink") + pairs: dict[tuple[int, int], dict[str, tuple[int, int]]] = {} + settings: dict[tuple[int, int], dict[str, str]] = {} + failed = False + row_count = 0 + seen_positions: set[tuple[int, int]] = set() + seen_labels: set[str] = set() + with args.samples.open(newline="", encoding="utf-8") as stream: + reader = csv.DictReader(stream, delimiter="\t") + if reader.fieldnames != HEADER: + raise EvidenceError(f"unexpected sample header: {reader.fieldnames!r}") + for row in reader: + row_count += 1 + if row["target"] not in ("native", "wasix"): + raise EvidenceError("unknown target in sample receipt") + for field in ("block", "pair", "position", "harness_status", "p95_ns", "p99_ns"): + if not row[field].isdigit(): + raise EvidenceError(f"non-numeric sample field: {field}") + block = int(row["block"]) + pair_number = int(row["pair"]) + position = int(row["position"]) + position_key = (block, position) + expected_order = ( + ("native", "wasix", "wasix", "native") + if block % 2 == 1 + else ("wasix", "native", "native", "wasix") + ) + if ( + block not in range(1, args.blocks + 1) + or position not in range(1, 5) + or pair_number != (position - 1) // 2 + 1 + or row["target"] != expected_order[position - 1] + or position_key in seen_positions + or not row["label"] + or row["label"] in seen_labels + or not SHA256.fullmatch(row["settings_sha256"]) + or int(row["p95_ns"]) <= 0 + or int(row["p99_ns"]) < int(row["p95_ns"]) + ): + raise EvidenceError("sample receipt violates the balanced block plan") + seen_positions.add(position_key) + seen_labels.add(row["label"]) + key = (block, pair_number) + if row["target"] in pairs.setdefault(key, {}): + raise EvidenceError(f"duplicate target within balanced pair: {key}") + pairs[key][row["target"]] = (int(row["p95_ns"]), int(row["p99_ns"])) + settings.setdefault(key, {})[row["target"]] = row["settings_sha256"] + failed |= row["harness_status"] != "0" or row["sample_status"] != "passed" + expected_rows = args.blocks * 4 + expected_pairs = args.blocks * 2 + if row_count != expected_rows or len(pairs) != expected_pairs: + failed = True + p95_ratios: list[float] = [] + p99_ratios: list[float] = [] + settings_equal = True + for key in sorted(pairs): + pair = pairs[key] + if set(pair) != {"native", "wasix"}: + failed = True + continue + native_p95, native_p99 = pair["native"] + wasix_p95, wasix_p99 = pair["wasix"] + if native_p95 <= 0 or native_p99 <= 0: + failed = True + continue + p95_ratios.append(wasix_p95 / native_p95) + p99_ratios.append(wasix_p99 / native_p99) + if settings.get(key, {}).get("native") != settings.get(key, {}).get("wasix"): + settings_equal = False + if not p95_ratios or not p99_ratios: + raise EvidenceError("no complete native/WASIX pairs") + median_p95 = statistics.median(p95_ratios) + median_p99 = statistics.median(p99_ratios) + performance_enforced = args.mode in ("diagnostic", "promotional") + ratios_pass = ( + median_p95 <= args.max_median_p95_ratio + and median_p99 <= args.max_median_p99_ratio + ) + if performance_enforced and not ratios_pass: + failed = True + if not settings_equal: + failed = True + if args.memory_status != "passed": + failed = True + if args.mode == "promotional" and args.blocks < 10: + failed = True + status = "failed" if failed else "passed" + classification = f"candidate-unqualified-{args.mode}-{status}-non-release" + write( + args.output, + [[ + "schema_version", "mode", "status", "paired_samples", + "median_p95_ratio", "median_p99_ratio", "performance_enforced", + "settings_equal", "memory_status", + ], [ + "oliphaunt.wasix-postmaster.checkpoint-paired.v1", args.mode, + status, len(p95_ratios), f"{median_p95:.9f}", + f"{median_p99:.9f}", int(performance_enforced), + int(settings_equal), args.memory_status, + ]], + ) + write( + args.result, + [[ + "schema_version", "status", "classification", "claim_scope", + "mode", "blocks", "policy_sha256", "carrier_identity", + "native_identity", "samples", "paired_summary", + "memory_status", + ], [ + "oliphaunt.wasix-postmaster.checkpoint-qualification.v1", + status, classification, + "candidate-unqualified-checkpoint-recycle-only-no-product-promotion", + args.mode, args.blocks, args.policy_sha256, + args.carrier_identity, args.native_identity, args.samples, + args.output, args.memory_status, + ]], + ) + return 0 if status == "passed" else 1 + except (EvidenceError, OSError, csv.Error) as error: + print(f"checkpoint qualification summary failed: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-checkpoint-qualification.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-checkpoint-qualification.test.py new file mode 100755 index 00000000..ccfc9269 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-checkpoint-qualification.test.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent +SUMMARIZER = ROOT / "summarize-checkpoint-qualification.py" +HEADER = [ + "block", "pair", "position", "target", "label", "harness_status", + "sample_status", "p95_ns", "p99_ns", "report_dir", "settings_sha256", +] + + +class CheckpointQualificationSummaryTest(unittest.TestCase): + def run_fixture( + self, *, wasix_p99: int = 12, wrong_order: bool = False + ) -> subprocess.CompletedProcess[str]: + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + root = Path(temporary.name) + samples = root / "samples.tsv" + with samples.open("x", newline="", encoding="utf-8") as stream: + writer = csv.writer(stream, delimiter="\t", lineterminator="\n") + writer.writerow(HEADER) + for position, target in enumerate(("native", "wasix", "wasix", "native"), 1): + if wrong_order and position == 1: + target = "wasix" + pair = 1 if position <= 2 else 2 + writer.writerow( + [1, pair, position, target, f"s{position}", 0, "passed", + 5 if target == "native" else 8, + 6 if target == "native" else wasix_p99, + f"/r/{position}", "a" * 64] + ) + return subprocess.run( + [ + str(SUMMARIZER), "--samples", str(samples), + "--output", str(root / "summary.tsv"), + "--result", str(root / "result.tsv"), + "--mode", "diagnostic", "--blocks", "1", + "--policy-sha256", "b" * 64, + "--carrier-identity", "c" * 64, + "--native-identity", "d" * 64, + "--memory-status", "passed", + ], + text=True, + capture_output=True, + check=False, + ) + + def test_balanced_ratios_pass(self) -> None: + result = self.run_fixture() + self.assertEqual(result.returncode, 0, result.stderr) + + def test_p99_ratio_gate_fails(self) -> None: + result = self.run_fixture(wasix_p99=30) + self.assertEqual(result.returncode, 1, result.stderr) + + def test_unbalanced_execution_order_is_rejected(self) -> None: + result = self.run_fixture(wrong_order=True) + self.assertEqual(result.returncode, 2, result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-libpq-latency.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-libpq-latency.sh new file mode 100755 index 00000000..154eb786 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-libpq-latency.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: summarize-libpq-latency.sh --raw PATH --output PATH --target NAME + --mode persistent|reconnect --warmup COUNT --samples COUNT + --libpq-path PATH --libpq-sha256 SHA256 --probe-sha256 SHA256 + +Validates one raw CLOCK_MONOTONIC libpq latency stream and atomically writes +nearest-rank p50/p95/p99 values. Any failed, missing, duplicate, malformed, or +unexpected sample prevents summary creation. +USAGE +} + +raw="" +output="" +target="" +mode="" +warmup_count="" +sample_count="" +libpq_path="" +libpq_sha256="" +probe_sha256="" + +while [ "$#" -gt 0 ]; do + case "$1" in + --raw|--output|--target|--mode|--warmup|--samples|--libpq-path|--libpq-sha256|--probe-sha256) + option="$1" + shift + [ "$#" -gt 0 ] || { printf '%s requires a value\n' "$option" >&2; exit 2; } + case "$option" in + --raw) [ -z "$raw" ] || { echo "--raw may only be specified once" >&2; exit 2; }; raw="$1" ;; + --output) [ -z "$output" ] || { echo "--output may only be specified once" >&2; exit 2; }; output="$1" ;; + --target) [ -z "$target" ] || { echo "--target may only be specified once" >&2; exit 2; }; target="$1" ;; + --mode) [ -z "$mode" ] || { echo "--mode may only be specified once" >&2; exit 2; }; mode="$1" ;; + --warmup) [ -z "$warmup_count" ] || { echo "--warmup may only be specified once" >&2; exit 2; }; warmup_count="$1" ;; + --samples) [ -z "$sample_count" ] || { echo "--samples may only be specified once" >&2; exit 2; }; sample_count="$1" ;; + --libpq-path) [ -z "$libpq_path" ] || { echo "--libpq-path may only be specified once" >&2; exit 2; }; libpq_path="$1" ;; + --libpq-sha256) [ -z "$libpq_sha256" ] || { echo "--libpq-sha256 may only be specified once" >&2; exit 2; }; libpq_sha256="$1" ;; + --probe-sha256) [ -z "$probe_sha256" ] || { echo "--probe-sha256 may only be specified once" >&2; exit 2; }; probe_sha256="$1" ;; + esac + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +[ -n "$raw" ] || { echo "missing required value: raw" >&2; exit 2; } +[ -n "$output" ] || { echo "missing required value: output" >&2; exit 2; } +[ -n "$target" ] || { echo "missing required value: target" >&2; exit 2; } +[ -n "$mode" ] || { echo "missing required value: mode" >&2; exit 2; } +[ -n "$warmup_count" ] || { echo "missing required value: warmup_count" >&2; exit 2; } +[ -n "$sample_count" ] || { echo "missing required value: sample_count" >&2; exit 2; } +[ -n "$libpq_path" ] || { echo "missing required value: libpq_path" >&2; exit 2; } +[ -n "$libpq_sha256" ] || { echo "missing required value: libpq_sha256" >&2; exit 2; } +[ -n "$probe_sha256" ] || { echo "missing required value: probe_sha256" >&2; exit 2; } +case "$mode" in persistent|reconnect) ;; *) echo "--mode requires persistent or reconnect" >&2; exit 2 ;; esac +[[ "$warmup_count" =~ ^(0|[1-9][0-9]*)$ ]] || { echo "--warmup requires a canonical nonnegative integer" >&2; exit 2; } +[[ "$sample_count" =~ ^[1-9][0-9]*$ ]] || { echo "--samples requires a canonical positive integer" >&2; exit 2; } +if ! awk -v samples="$sample_count" -v warmup="$warmup_count" \ + 'BEGIN { exit !((samples + 0) <= 10000000 && (warmup + 0) <= 10000000) }' +then + echo "sample and warmup counts may not exceed 10000000" >&2 + exit 2 +fi +case "$libpq_path" in /*) ;; *) echo "--libpq-path requires an absolute path" >&2; exit 2 ;; esac +case "$libpq_sha256" in *[!0-9a-f]*|"") echo "--libpq-sha256 requires lowercase hexadecimal SHA-256" >&2; exit 2 ;; esac +case "$probe_sha256" in *[!0-9a-f]*|"") echo "--probe-sha256 requires lowercase hexadecimal SHA-256" >&2; exit 2 ;; esac +[ "${#libpq_sha256}" -eq 64 ] || { echo "--libpq-sha256 requires 64 hexadecimal characters" >&2; exit 2; } +[ "${#probe_sha256}" -eq 64 ] || { echo "--probe-sha256 requires 64 hexadecimal characters" >&2; exit 2; } +for field in "$raw" "$output" "$target" "$libpq_path"; do + case "$field" in *$'\t'*|*$'\n'*|*$'\r'*) echo "paths and labels may not contain tabs or newlines" >&2; exit 2 ;; esac +done +if [ ! -f "$raw" ] || [ -L "$raw" ]; then + printf 'raw evidence is not a regular non-symlink file: %s\n' "$raw" >&2 + exit 1 +fi +if [ ! -f "$libpq_path" ] || [ -L "$libpq_path" ]; then + printf 'libpq provenance is not a regular non-symlink file: %s\n' "$libpq_path" >&2 + exit 1 +fi +if [ -e "$output" ] || [ -L "$output" ]; then + printf 'refusing to replace latency summary: %s\n' "$output" >&2 + exit 2 +fi +[ -d "$(dirname "$output")" ] || { printf 'summary parent directory does not exist: %s\n' "$output" >&2; exit 2; } + +hash_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{ print $1 }' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{ print $1 }' + else + echo "no SHA-256 tool available" >&2 + return 127 + fi +} +actual_libpq_sha256="$(hash_file "$libpq_path")" +[ "$actual_libpq_sha256" = "$libpq_sha256" ] || { + printf 'libpq SHA-256 does not match exact provenance path: %s\n' "$libpq_path" >&2 + exit 1 +} + +work_dir="$(mktemp -d "$(dirname "$output")/.libpq-latency-summary.XXXXXX")" +cleanup() { + rm -rf -- "$work_dir" +} +trap cleanup EXIT HUP INT TERM +durations="$work_dir/durations" +sorted="$work_dir/sorted" +pending="$work_dir/summary" + +if ! awk -F '\t' -v expected_mode="$mode" -v expected_warmup="$warmup_count" \ + -v expected_samples="$sample_count" ' + NR == 1 { + expected = "schema_version\tmode\tphase\tsample_index\tduration_ns\tstatus" + if ($0 != expected) bad = 1 + next + } + { + if (NF != 6 || $1 != "1" || $2 != expected_mode || + ($3 != "warmup" && $3 != "measure") || + $4 !~ /^[1-9][0-9]*$/ || $5 !~ /^[1-9][0-9]*$/ || $6 != "ok") { + bad = 1 + next + } + count[$3]++ + if (($4 + 0) != count[$3]) bad = 1 + if ($3 == "measure") { + seen_measure = 1 + print $5 + } else if (seen_measure) { + bad = 1 + } + } + END { + if (NR == 0 || count["warmup"] != expected_warmup || + count["measure"] != expected_samples || bad) exit 1 + } + ' "$raw" >"$durations" +then + printf 'raw latency evidence failed validation: %s\n' "$raw" >&2 + exit 1 +fi + +LC_ALL=C sort -n "$durations" >"$sorted" +p50_index=$(( (sample_count * 50 + 99) / 100 )) +p95_index=$(( (sample_count * 95 + 99) / 100 )) +p99_index=$(( (sample_count * 99 + 99) / 100 )) +p50_ns="$(sed -n "${p50_index}p" "$sorted")" +p95_ns="$(sed -n "${p95_index}p" "$sorted")" +p99_ns="$(sed -n "${p99_index}p" "$sorted")" +for percentile in "$p50_ns" "$p95_ns" "$p99_ns"; do + case "$percentile" in ""|*[!0-9]*) echo "percentile selection failed" >&2; exit 1 ;; esac +done + +to_ms() { + awk -v nanoseconds="$1" 'BEGIN { printf "%.6f", nanoseconds / 1000000.0 }' +} + +{ + printf 'schema_version\ttarget\tmode\tstatus\tclock\twarmup_count\tsample_count\tp50_ns\tp95_ns\tp99_ns\tp50_ms\tp95_ms\tp99_ms\traw_tsv\tlibpq_path\tlibpq_sha256\tprobe_sha256\n' + printf '1\t%s\t%s\tok\tCLOCK_MONOTONIC\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$target" "$mode" "$warmup_count" "$sample_count" \ + "$p50_ns" "$p95_ns" "$p99_ns" \ + "$(to_ms "$p50_ns")" "$(to_ms "$p95_ns")" "$(to_ms "$p99_ns")" "$raw" \ + "$libpq_path" "$libpq_sha256" "$probe_sha256" +} >"$pending" +mv "$pending" "$output" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-libpq-latency.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-libpq-latency.test.sh new file mode 100755 index 00000000..a2f54011 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-libpq-latency.test.sh @@ -0,0 +1,364 @@ +#!/usr/bin/env bash + +set -euo pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SUMMARIZER="$PROJECT_ROOT/bin/summarize-libpq-latency.sh" +PROBE_SOURCE="$PROJECT_ROOT/probes/libpq_latency_probe.c" +CC_BIN="${CC:-cc}" + +command -v "$CC_BIN" >/dev/null 2>&1 || { + printf 'missing C compiler for libpq latency probe warnings test: %s\n' "$CC_BIN" >&2 + exit 127 +} + +test_root="$(mktemp -d)" +cleanup() { + rm -rf -- "$test_root" +} +trap cleanup EXIT HUP INT TERM + +cat >"$test_root/libpq-fe.h" <<'HEADER' +#ifndef FAKE_LIBPQ_FE_H +#define FAKE_LIBPQ_FE_H + +typedef struct pg_conn PGconn; +typedef struct pg_result PGresult; + +typedef enum +{ + CONNECTION_OK, + CONNECTION_BAD +} ConnStatusType; + +typedef enum +{ + PGRES_EMPTY_QUERY, + PGRES_COMMAND_OK, + PGRES_TUPLES_OK +} ExecStatusType; + +PGconn *PQconnectdb(const char *conninfo); +ConnStatusType PQstatus(const PGconn *connection); +void PQfinish(PGconn *connection); +PGresult *PQexec(PGconn *connection, const char *query); +ExecStatusType PQresultStatus(const PGresult *result); +int PQntuples(const PGresult *result); +int PQnfields(const PGresult *result); +int PQgetisnull(const PGresult *result, int row, int column); +char *PQgetvalue(const PGresult *result, int row, int column); +void PQclear(PGresult *result); + +#endif +HEADER + +cat >"$test_root/fake-libpq.c" <<'SOURCE' +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include + +struct pg_conn +{ + int connected; +}; + +struct pg_result +{ + int valid; +}; + +static void +fake_delay(void) +{ + struct timespec duration = {0, 100000L}; + + (void) nanosleep(&duration, NULL); +} + +PGconn * +PQconnectdb(const char *conninfo) +{ + PGconn *connection; + + (void) conninfo; + fake_delay(); + connection = malloc(sizeof(*connection)); + if (connection != NULL) + connection->connected = getenv("FAKE_LIBPQ_FAIL_CONNECT") == NULL; + return connection; +} + +ConnStatusType +PQstatus(const PGconn *connection) +{ + return connection != NULL && connection->connected ? CONNECTION_OK : CONNECTION_BAD; +} + +void +PQfinish(PGconn *connection) +{ + fake_delay(); + free(connection); +} + +PGresult * +PQexec(PGconn *connection, const char *query) +{ + PGresult *result; + + (void) connection; + fake_delay(); + if (getenv("FAKE_LIBPQ_FAIL_QUERY") != NULL || strcmp(query, "SELECT 1") != 0) + return NULL; + result = malloc(sizeof(*result)); + if (result != NULL) + result->valid = 1; + return result; +} + +ExecStatusType +PQresultStatus(const PGresult *result) +{ + return result != NULL && result->valid ? PGRES_TUPLES_OK : PGRES_EMPTY_QUERY; +} + +int +PQntuples(const PGresult *result) +{ + return result != NULL && result->valid ? 1 : 0; +} + +int +PQnfields(const PGresult *result) +{ + return result != NULL && result->valid ? 1 : 0; +} + +int +PQgetisnull(const PGresult *result, int row, int column) +{ + return result == NULL || !result->valid || row != 0 || column != 0; +} + +char * +PQgetvalue(const PGresult *result, int row, int column) +{ + static char value[] = "1"; + + if (result == NULL || !result->valid || row != 0 || column != 0) + return NULL; + return value; +} + +void +PQclear(PGresult *result) +{ + free(result); +} +SOURCE + +"$CC_BIN" -std=c11 -O2 -Wall -Wextra -Werror -Wpedantic -Wconversion -Wshadow \ + -I"$test_root" "$PROBE_SOURCE" "$test_root/fake-libpq.c" \ + -o "$test_root/libpq-latency-probe" + +hash_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{ print $1 }' + else + shasum -a 256 "$1" | awk '{ print $1 }' + fi +} +printf 'fake exact libpq shared object\n' >"$test_root/libpq.so.5.18" +fake_libpq_path="$test_root/libpq.so.5.18" +fake_libpq_sha256="$(hash_file "$fake_libpq_path")" +fake_probe_sha256="$(hash_file "$test_root/libpq-latency-probe")" + +for mode in persistent reconnect; do + raw="$test_root/$mode.raw.tsv" + summary="$test_root/$mode.summary.tsv" + "$test_root/libpq-latency-probe" \ + --conninfo postgresql://fake/postgres \ + --mode "$mode" \ + --warmup 2 \ + --samples 5 \ + --output "$raw" + "$SUMMARIZER" \ + --raw "$raw" \ + --output "$summary" \ + --target fake \ + --mode "$mode" \ + --warmup 2 \ + --samples 5 \ + --libpq-path "$fake_libpq_path" \ + --libpq-sha256 "$fake_libpq_sha256" \ + --probe-sha256 "$fake_probe_sha256" + awk -F '\t' -v mode="$mode" ' + NR == 1 { + if ($0 != "schema_version\tmode\tphase\tsample_index\tduration_ns\tstatus") exit 1 + next + } + $2 != mode || $6 != "ok" { exit 1 } + $3 == "warmup" { warmup++ } + $3 == "measure" { measure++ } + END { if (NR != 8 || warmup != 2 || measure != 5) exit 1 } + ' "$raw" + awk -F '\t' -v mode="$mode" ' + NR == 1 { next } + $1 != "1" || $2 != "fake" || $3 != mode || $4 != "ok" || + $5 != "CLOCK_MONOTONIC" || $6 != 2 || $7 != 5 || + $8 !~ /^[1-9][0-9]*$/ || $9 !~ /^[1-9][0-9]*$/ || + $10 !~ /^[1-9][0-9]*$/ { exit 1 } + END { if (NR != 2) exit 1 } + ' "$summary" +done + +cat >"$test_root/known.raw.tsv" <<'TSV' +schema_version mode phase sample_index duration_ns status +1 persistent warmup 1 7 ok +1 persistent warmup 2 8 ok +1 persistent measure 1 50 ok +1 persistent measure 2 10 ok +1 persistent measure 3 40 ok +1 persistent measure 4 20 ok +1 persistent measure 5 30 ok +TSV +"$SUMMARIZER" \ + --raw "$test_root/known.raw.tsv" \ + --output "$test_root/known.summary.tsv" \ + --target native \ + --mode persistent \ + --warmup 2 \ + --samples 5 \ + --libpq-path "$fake_libpq_path" \ + --libpq-sha256 "$fake_libpq_sha256" \ + --probe-sha256 "$fake_probe_sha256" +awk -F '\t' 'NR == 2 { if ($8 != 30 || $9 != 50 || $10 != 50) exit 1; found = 1 } END { exit !found }' \ + "$test_root/known.summary.tsv" + +cat >"$test_root/failed.raw.tsv" <<'TSV' +schema_version mode phase sample_index duration_ns status +1 persistent warmup 1 7 ok +1 persistent warmup 2 8 ok +1 persistent measure 1 50 ok +1 persistent measure 2 10 query_error +TSV +if "$SUMMARIZER" \ + --raw "$test_root/failed.raw.tsv" \ + --output "$test_root/failed.summary.tsv" \ + --target native \ + --mode persistent \ + --warmup 2 \ + --samples 2 \ + --libpq-path "$fake_libpq_path" \ + --libpq-sha256 "$fake_libpq_sha256" \ + --probe-sha256 "$fake_probe_sha256" >/dev/null 2>&1 +then + echo "summarizer accepted a failed raw sample" >&2 + exit 1 +fi +[ ! -e "$test_root/failed.summary.tsv" ] || { + echo "summarizer left derived metrics for failed evidence" >&2 + exit 1 +} + +cat >"$test_root/missing.raw.tsv" <<'TSV' +schema_version mode phase sample_index duration_ns status +1 reconnect warmup 1 7 ok +1 reconnect measure 1 50 ok +TSV +if "$SUMMARIZER" \ + --raw "$test_root/missing.raw.tsv" \ + --output "$test_root/missing.summary.tsv" \ + --target wasix \ + --mode reconnect \ + --warmup 1 \ + --samples 2 \ + --libpq-path "$fake_libpq_path" \ + --libpq-sha256 "$fake_libpq_sha256" \ + --probe-sha256 "$fake_probe_sha256" >/dev/null 2>&1 +then + echo "summarizer accepted a missing measured sample" >&2 + exit 1 +fi +[ ! -e "$test_root/missing.summary.tsv" ] || { + echo "summarizer left derived metrics for incomplete evidence" >&2 + exit 1 +} + +cat >"$test_root/interleaved.raw.tsv" <<'TSV' +schema_version mode phase sample_index duration_ns status +1 persistent measure 1 50 ok +1 persistent warmup 1 7 ok +1 persistent measure 2 60 ok +TSV +if "$SUMMARIZER" \ + --raw "$test_root/interleaved.raw.tsv" \ + --output "$test_root/interleaved.summary.tsv" \ + --target native \ + --mode persistent \ + --warmup 1 \ + --samples 2 \ + --libpq-path "$fake_libpq_path" \ + --libpq-sha256 "$fake_libpq_sha256" \ + --probe-sha256 "$fake_probe_sha256" >/dev/null 2>&1 +then + echo "summarizer accepted a warmup row after measurement began" >&2 + exit 1 +fi +[ ! -e "$test_root/interleaved.summary.tsv" ] || { + echo "summarizer left derived metrics for interleaved phases" >&2 + exit 1 +} + +if "$SUMMARIZER" \ + --raw "$test_root/known.raw.tsv" \ + --output "$test_root/wrong-libpq.summary.tsv" \ + --target native \ + --mode persistent \ + --warmup 2 \ + --samples 5 \ + --libpq-path "$fake_libpq_path" \ + --libpq-sha256 0000000000000000000000000000000000000000000000000000000000000000 \ + --probe-sha256 "$fake_probe_sha256" >/dev/null 2>&1 +then + echo "summarizer accepted a mismatched libpq provenance hash" >&2 + exit 1 +fi +[ ! -e "$test_root/wrong-libpq.summary.tsv" ] || { + echo "summarizer left derived metrics for mismatched libpq provenance" >&2 + exit 1 +} + +if FAKE_LIBPQ_FAIL_QUERY=1 "$test_root/libpq-latency-probe" \ + --conninfo postgresql://fake/postgres \ + --mode reconnect \ + --warmup 1 \ + --samples 1 \ + --output "$test_root/probe-failed.raw.tsv" >/dev/null 2>&1 +then + echo "probe accepted a failed query" >&2 + exit 1 +fi +grep -Fq $'\tquery_error' "$test_root/probe-failed.raw.tsv" || { + echo "probe did not preserve explicit failed-sample status" >&2 + exit 1 +} +if "$SUMMARIZER" \ + --raw "$test_root/probe-failed.raw.tsv" \ + --output "$test_root/probe-failed.summary.tsv" \ + --target fake \ + --mode reconnect \ + --warmup 1 \ + --samples 1 \ + --libpq-path "$fake_libpq_path" \ + --libpq-sha256 "$fake_libpq_sha256" \ + --probe-sha256 "$fake_probe_sha256" >/dev/null 2>&1 +then + echo "summarizer accepted failed probe output" >&2 + exit 1 +fi + +printf 'libpq latency probe and summary tests passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-linux-smaps.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-linux-smaps.sh new file mode 100755 index 00000000..83d3fb2d --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-linux-smaps.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + printf 'usage: %s SMAPS MAPPINGS_TSV CATEGORIES_TSV\n' "${0##*/}" >&2 + exit 64 +} + +[ "$#" -eq 3 ] || usage + +SMAPS_PATH="$1" +MAPPINGS_OUT="$2" +CATEGORIES_OUT="$3" + +[ -r "$SMAPS_PATH" ] || { + printf 'smaps summary: input is not readable: %s\n' "$SMAPS_PATH" >&2 + exit 66 +} +[ "$SMAPS_PATH" != "$MAPPINGS_OUT" ] || { + printf 'smaps summary: mappings output must not overwrite the input\n' >&2 + exit 64 +} +[ "$SMAPS_PATH" != "$CATEGORIES_OUT" ] || { + printf 'smaps summary: categories output must not overwrite the input\n' >&2 + exit 64 +} +[ "$MAPPINGS_OUT" != "$CATEGORIES_OUT" ] || { + printf 'smaps summary: output paths must be distinct\n' >&2 + exit 64 +} + +MAPPINGS_DIR="$(dirname "$MAPPINGS_OUT")" +CATEGORIES_DIR="$(dirname "$CATEGORIES_OUT")" +[ -d "$MAPPINGS_DIR" ] || { + printf 'smaps summary: mappings output directory does not exist: %s\n' "$MAPPINGS_DIR" >&2 + exit 73 +} +[ -d "$CATEGORIES_DIR" ] || { + printf 'smaps summary: categories output directory does not exist: %s\n' "$CATEGORIES_DIR" >&2 + exit 73 +} + +MAPPINGS_TMP="$(mktemp "$MAPPINGS_DIR/.smaps-mappings.XXXXXX")" +CATEGORIES_TMP="$(mktemp "$CATEGORIES_DIR/.smaps-categories.XXXXXX")" +cleanup() { + rm -f "$MAPPINGS_TMP" "$CATEGORIES_TMP" +} +trap cleanup EXIT HUP INT TERM + +awk -v mappings_out="$MAPPINGS_TMP" -v categories_out="$CATEGORIES_TMP" ' +BEGIN { + OFS = "\t" + category_order = "postgres-shared stack heap anonymous-exec anonymous-rw anonymous-reserved anonymous-other file-executable file-backed" + category_count = split(category_order, categories, " ") + + print "category", "address", "perms", "offset", "device", "inode", "pathname", \ + "size_kb", "rss_kb", "pss_kb", "private_kb", "shared_kb", \ + "anonymous_kb", "swap_kb" > mappings_out +} + +function reset_metrics() { + size_kb = 0 + rss_kb = 0 + pss_kb = 0 + private_clean_kb = 0 + private_dirty_kb = 0 + private_hugetlb_kb = 0 + shared_clean_kb = 0 + shared_dirty_kb = 0 + shared_hugetlb_kb = 0 + anonymous_kb = 0 + swap_kb = 0 +} + +function mapping_category(path, mapping_perms, lowered_path, anonymous_mapping) { + lowered_path = tolower(path) + if (lowered_path ~ /postgresql-wasix/ || lowered_path ~ /(^|\/)postgresql\./) { + return "postgres-shared" + } + if (path ~ /^\[stack(:[^]]+)?\]$/) { + return "stack" + } + if (path == "[heap]") { + return "heap" + } + + anonymous_mapping = (path == "" || path ~ /^\[/) + if (anonymous_mapping && mapping_perms ~ /x/) { + return "anonymous-exec" + } + if (anonymous_mapping && mapping_perms ~ /^---[ps]$/) { + return "anonymous-reserved" + } + if (anonymous_mapping && mapping_perms ~ /w/) { + return "anonymous-rw" + } + if (anonymous_mapping) { + return "anonymous-other" + } + if (mapping_perms ~ /x/) { + return "file-executable" + } + return "file-backed" +} + +function flush_mapping( category, private_kb, shared_kb) { + if (!have_mapping) { + return + } + + category = mapping_category(pathname, perms) + private_kb = private_clean_kb + private_dirty_kb + private_hugetlb_kb + shared_kb = shared_clean_kb + shared_dirty_kb + shared_hugetlb_kb + + print category, address, perms, offset, device, inode, pathname, \ + size_kb, rss_kb, pss_kb, private_kb, shared_kb, anonymous_kb, \ + swap_kb > mappings_out + + mappings[category]++ + sizes[category] += size_kb + rss[category] += rss_kb + pss[category] += pss_kb + private_pages[category] += private_kb + shared_pages[category] += shared_kb + anonymous_pages[category] += anonymous_kb + swaps[category] += swap_kb + + total_mappings++ + total_size += size_kb + total_rss += rss_kb + total_pss += pss_kb + total_private += private_kb + total_shared += shared_kb + total_anonymous += anonymous_kb + total_swap += swap_kb +} + +$1 ~ /^[[:xdigit:]]+-[[:xdigit:]]+$/ && $2 ~ /^[r-][w-][x-][ps]$/ && NF >= 5 { + flush_mapping() + + address = $1 + perms = $2 + offset = $3 + device = $4 + inode = $5 + pathname = "" + for (field = 6; field <= NF; field++) { + if (field > 6) { + pathname = pathname " " + } + pathname = pathname $field + } + gsub(/[\t\r\n]/, " ", pathname) + reset_metrics() + have_mapping = 1 + next +} + +have_mapping && $1 == "Size:" { size_kb = $2 + 0; next } +have_mapping && $1 == "Rss:" { rss_kb = $2 + 0; next } +have_mapping && $1 == "Pss:" { pss_kb = $2 + 0; next } +have_mapping && $1 == "Private_Clean:" { private_clean_kb = $2 + 0; next } +have_mapping && $1 == "Private_Dirty:" { private_dirty_kb = $2 + 0; next } +have_mapping && $1 == "Private_Hugetlb:" { private_hugetlb_kb = $2 + 0; next } +have_mapping && $1 == "Shared_Clean:" { shared_clean_kb = $2 + 0; next } +have_mapping && $1 == "Shared_Dirty:" { shared_dirty_kb = $2 + 0; next } +have_mapping && $1 == "Shared_Hugetlb:" { shared_hugetlb_kb = $2 + 0; next } +have_mapping && $1 == "Anonymous:" { anonymous_kb = $2 + 0; next } +have_mapping && $1 == "Swap:" { swap_kb = $2 + 0; next } + +END { + flush_mapping() + if (total_mappings == 0) { + print "smaps summary: no mapping records found" > "/dev/stderr" + exit 65 + } + + print "category", "mappings", "size_kb", "rss_kb", "pss_kb", \ + "private_kb", "shared_kb", "anonymous_kb", "swap_kb" > categories_out + for (category_index = 1; category_index <= category_count; category_index++) { + category = categories[category_index] + print category, mappings[category] + 0, sizes[category] + 0, \ + rss[category] + 0, pss[category] + 0, private_pages[category] + 0, \ + shared_pages[category] + 0, anonymous_pages[category] + 0, \ + swaps[category] + 0 > categories_out + } + print "total", total_mappings, total_size, total_rss, total_pss, \ + total_private, total_shared, total_anonymous, total_swap > categories_out +} +' "$SMAPS_PATH" + +mv "$MAPPINGS_TMP" "$MAPPINGS_OUT" +mv "$CATEGORIES_TMP" "$CATEGORIES_OUT" +trap - EXIT HUP INT TERM diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-linux-smaps.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-linux-smaps.test.sh new file mode 100755 index 00000000..bab3e943 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-linux-smaps.test.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CLASSIFIER="$SCRIPT_DIR/summarize-linux-smaps.sh" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/oliphaunt-smaps-test.XXXXXX")" +trap 'rm -rf "$TEST_ROOT"' EXIT HUP INT TERM + +SMAPS="$TEST_ROOT/smaps" +MAPPINGS="$TEST_ROOT/mappings.tsv" +CATEGORIES="$TEST_ROOT/categories.tsv" +EXPECTED_MAPPINGS="$TEST_ROOT/expected-mappings.tsv" +EXPECTED_CATEGORIES="$TEST_ROOT/expected-categories.tsv" + +cat >"$SMAPS" <<'SMAPS' +10000000-10001000 rw-s 00000000 00:01 10 /tmp/postgresql-wasix-1234 +Size: 100 kB +Rss: 80 kB +Pss: 50 kB +Private_Clean: 1 kB +Private_Dirty: 2 kB +Private_Hugetlb: 3 kB +Shared_Clean: 4 kB +Shared_Dirty: 5 kB +Shared_Hugetlb: 6 kB +Anonymous: 7 kB +Swap: 8 kB +11000000-11001000 rw-s 00000000 00:02 11 /dev/shm/PostgreSQL.123 +Size: 10 kB +Rss: 9 kB +Pss: 8 kB +Private_Dirty: 1 kB +Shared_Clean: 2 kB +Anonymous: 3 kB +Swap: 4 kB +12000000-12001000 rw-p 00000000 00:00 0 [stack:42] +Size: 20 kB +Rss: 19 kB +Pss: 18 kB +Private_Dirty: 17 kB +Shared_Clean: 16 kB +Anonymous: 15 kB +Swap: 14 kB +13000000-13001000 rw-p 00000000 00:00 0 [heap] +Size: 30 kB +Rss: 29 kB +Pss: 28 kB +Private_Dirty: 27 kB +Shared_Clean: 26 kB +Anonymous: 25 kB +Swap: 24 kB +14000000-14001000 r-xp 00000000 00:00 0 +Size: 40 kB +Rss: 39 kB +Pss: 38 kB +Private_Dirty: 37 kB +Shared_Clean: 36 kB +Anonymous: 35 kB +Swap: 34 kB +15000000-15001000 rw-p 00000000 00:00 0 +Size: 50 kB +Rss: 49 kB +Pss: 48 kB +Private_Dirty: 47 kB +Shared_Clean: 46 kB +Anonymous: 45 kB +Swap: 44 kB +16000000-16001000 ---p 00000000 00:00 0 +Size: 60 kB +Rss: 0 kB +Pss: 0 kB +17000000-17001000 r--p 00000000 00:00 0 [vvar] +Size: 70 kB +Rss: 69 kB +Pss: 68 kB +Private_Dirty: 67 kB +Shared_Clean: 66 kB +Anonymous: 65 kB +Swap: 64 kB +18000000-18001000 r-xp 00000000 08:01 12 /opt/with space/module.so +Size: 80 kB +Rss: 79 kB +Pss: 78 kB +Private_Dirty: 77 kB +Shared_Clean: 76 kB +Anonymous: 75 kB +Swap: 74 kB +19000000-19001000 r--p 00000000 08:01 13 /opt/data.bin +Size: 90 kB +Rss: 89 kB +Pss: 88 kB +Private_Dirty: 87 kB +Shared_Clean: 86 kB +Anonymous: 85 kB +Swap: 84 kB +SMAPS + +bash "$CLASSIFIER" "$SMAPS" "$MAPPINGS" "$CATEGORIES" + +{ + printf 'category\taddress\tperms\toffset\tdevice\tinode\tpathname\tsize_kb\trss_kb\tpss_kb\tprivate_kb\tshared_kb\tanonymous_kb\tswap_kb\n' + printf 'postgres-shared\t10000000-10001000\trw-s\t00000000\t00:01\t10\t/tmp/postgresql-wasix-1234\t100\t80\t50\t6\t15\t7\t8\n' + printf 'postgres-shared\t11000000-11001000\trw-s\t00000000\t00:02\t11\t/dev/shm/PostgreSQL.123\t10\t9\t8\t1\t2\t3\t4\n' + printf 'stack\t12000000-12001000\trw-p\t00000000\t00:00\t0\t[stack:42]\t20\t19\t18\t17\t16\t15\t14\n' + printf 'heap\t13000000-13001000\trw-p\t00000000\t00:00\t0\t[heap]\t30\t29\t28\t27\t26\t25\t24\n' + printf 'anonymous-exec\t14000000-14001000\tr-xp\t00000000\t00:00\t0\t\t40\t39\t38\t37\t36\t35\t34\n' + printf 'anonymous-rw\t15000000-15001000\trw-p\t00000000\t00:00\t0\t\t50\t49\t48\t47\t46\t45\t44\n' + printf 'anonymous-reserved\t16000000-16001000\t---p\t00000000\t00:00\t0\t\t60\t0\t0\t0\t0\t0\t0\n' + printf 'anonymous-other\t17000000-17001000\tr--p\t00000000\t00:00\t0\t[vvar]\t70\t69\t68\t67\t66\t65\t64\n' + printf 'file-executable\t18000000-18001000\tr-xp\t00000000\t08:01\t12\t/opt/with space/module.so\t80\t79\t78\t77\t76\t75\t74\n' + printf 'file-backed\t19000000-19001000\tr--p\t00000000\t08:01\t13\t/opt/data.bin\t90\t89\t88\t87\t86\t85\t84\n' +} >"$EXPECTED_MAPPINGS" + +{ + printf 'category\tmappings\tsize_kb\trss_kb\tpss_kb\tprivate_kb\tshared_kb\tanonymous_kb\tswap_kb\n' + printf 'postgres-shared\t2\t110\t89\t58\t7\t17\t10\t12\n' + printf 'stack\t1\t20\t19\t18\t17\t16\t15\t14\n' + printf 'heap\t1\t30\t29\t28\t27\t26\t25\t24\n' + printf 'anonymous-exec\t1\t40\t39\t38\t37\t36\t35\t34\n' + printf 'anonymous-rw\t1\t50\t49\t48\t47\t46\t45\t44\n' + printf 'anonymous-reserved\t1\t60\t0\t0\t0\t0\t0\t0\n' + printf 'anonymous-other\t1\t70\t69\t68\t67\t66\t65\t64\n' + printf 'file-executable\t1\t80\t79\t78\t77\t76\t75\t74\n' + printf 'file-backed\t1\t90\t89\t88\t87\t86\t85\t84\n' + printf 'total\t10\t550\t462\t424\t366\t369\t355\t350\n' +} >"$EXPECTED_CATEGORIES" + +diff -u "$EXPECTED_MAPPINGS" "$MAPPINGS" +diff -u "$EXPECTED_CATEGORIES" "$CATEGORIES" + +printf 'Linux smaps summary tests passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-wasix-cold-ownership.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-wasix-cold-ownership.py new file mode 100755 index 00000000..0f2a9421 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-wasix-cold-ownership.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +"""Aggregate independently validated cold-start samples with nearest-rank tails.""" + +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +import math +import os +import sys +from pathlib import Path +from typing import NoReturn + + +SAMPLE_SCHEMA = "oliphaunt.wasix-postmaster.cold-ownership-sample.v1" +SUMMARY_SCHEMA = "oliphaunt.wasix-postmaster.cold-ownership-summary.v1" + + +class SummaryError(RuntimeError): + pass + + +def fail(message: str) -> NoReturn: + raise SummaryError(message) + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def nearest_rank(values: list[float], percentile: float) -> float: + if not values: + fail("cannot calculate a percentile over no values") + ordered = sorted(values) + rank = max(1, math.ceil(percentile * len(ordered))) + return ordered[rank - 1] + + +def unsigned(row: dict[str, str], name: str) -> int: + value = row.get(name, "") + if not value.isdigit(): + fail(f"sample field {name} must be unsigned: {value!r}") + return int(value) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--input", action="append", required=True, type=Path) + parser.add_argument("--expected-blocks", required=True, type=int) + parser.add_argument("--max-p95-ms", type=float) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--receipt", required=True, type=Path) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + if args.expected_blocks < 5: + fail("cold qualification requires at least five independent blocks") + if len(args.input) != args.expected_blocks or len(set(args.input)) != len(args.input): + fail("input count/uniqueness does not match expected blocks") + if args.max_p95_ms is not None and args.max_p95_ms <= 0: + fail("--max-p95-ms must be positive") + + rows: list[dict[str, str]] = [] + inputs: list[dict[str, str]] = [] + header: list[str] | None = None + for index, path in enumerate(args.input, start=1): + if not path.is_file() or path.is_symlink(): + fail(f"sample must be a regular non-symlink file: {path}") + with path.open("r", encoding="utf-8", newline="") as stream: + reader = csv.DictReader(stream, delimiter="\t") + if reader.fieldnames is None: + fail(f"sample has no header: {path}") + if header is None: + header = reader.fieldnames + elif reader.fieldnames != header: + fail(f"sample header changed: {path}") + current = list(reader) + if len(current) != 1: + fail(f"sample must contain exactly one row: {path}") + row = current[0] + if row.get("schema_version") != SAMPLE_SCHEMA or row.get("target") != "wasix" or row.get("status") != "passed": + fail(f"sample is not an exact passed WASIX cold sample: {path}") + if unsigned(row, "resident_after_pages") != 0: + fail(f"sample retained resident pages: {path}") + if unsigned(row, "full_valid_sample_count") < 1: + fail(f"sample has no full resource observation: {path}") + io_status = row.get("io_observation_status") + io_controller = row.get("io_controller_status") + io_reason = row.get("io_missing_reason") + io_first_touch = row.get("io_first_touch_status") + if io_status == "available": + if io_controller not in {"available", "missing"} or io_reason != "none": + fail(f"sample has incoherent available io evidence: {path}") + if io_first_touch != "attributable": + fail(f"sample does not bind attributable io first touch: {path}") + for name in ("io_read_bytes", "io_write_bytes", "io_read_ios", "io_write_ios"): + unsigned(row, name) + elif io_status == "unavailable": + expected_reason = ( + "io-controller-missing" + if io_controller == "missing" + else "io-stat-missing" + ) + if io_controller not in {"available", "missing"} or io_reason != expected_reason: + fail(f"sample has incoherent unavailable io evidence: {path}") + if io_first_touch != "unavailable": + fail(f"sample claims io first touch while io is unavailable: {path}") + for name in ("io_read_bytes", "io_write_bytes", "io_read_ios", "io_write_ios"): + if row.get(name, "") != "": + fail(f"sample fills unavailable io metric {name}: {path}") + else: + fail(f"sample has invalid io observation status: {path}") + float(row["spawn_to_first_query_ms"]) + rows.append(row) + inputs.append({"block": str(index), "path": str(path), "sha256": sha256_file(path)}) + + execution_identities = {row["execution_identity_sha256"] for row in rows} + carrier_roots = {row["carrier_root"] for row in rows} + pgdata_roots = {row["pgdata_root"] for row in rows} + memory_limits = {row["memory_max_bytes"] for row in rows} + high_limits = {row["memory_high_bytes"] for row in rows} + swap_limits = {row["swap_max_bytes"] for row in rows} + if len(execution_identities) != 1 or len(carrier_roots) != 1: + fail("carrier/execution identity changed across cold blocks") + if len(pgdata_roots) != len(rows): + fail("cold blocks reused PGDATA instead of creating independent initialized roots") + if len(memory_limits) != 1 or len(high_limits) != 1 or len(swap_limits) != 1: + fail("cgroup limits changed across cold blocks") + + latencies = [float(row["spawn_to_first_query_ms"]) for row in rows] + p50 = nearest_rank(latencies, 0.50) + p95 = nearest_rank(latencies, 0.95) + status = "passed" + io_available_rows = [row for row in rows if row["io_observation_status"] == "available"] + io_unavailable_rows = [row for row in rows if row["io_observation_status"] == "unavailable"] + if len(io_available_rows) == len(rows): + io_first_touch_status = "attributable" + total_io_read_bytes: int | str = sum( + unsigned(row, "io_read_bytes") for row in io_available_rows + ) + total_io_write_bytes: int | str = sum( + unsigned(row, "io_write_bytes") for row in io_available_rows + ) + elif len(io_unavailable_rows) == len(rows): + io_first_touch_status = "unavailable" + total_io_read_bytes = "" + total_io_write_bytes = "" + else: + io_first_touch_status = "mixed-unavailable" + total_io_read_bytes = "" + total_io_write_bytes = "" + io_controllers = {row["io_controller_status"] for row in rows} + io_reasons = {row["io_missing_reason"] for row in rows} + io_controller_status = next(iter(io_controllers)) if len(io_controllers) == 1 else "mixed" + io_missing_reason = next(iter(io_reasons)) if len(io_reasons) == 1 else "mixed" + detail = f"cold-boundary-memory-resource-gates-passed;io-first-touch-{io_first_touch_status}" + if args.max_p95_ms is not None and p95 > args.max_p95_ms: + status = "failed" + detail = "cold-start-p95-exceeds-declared-ceiling" + values = [ + SUMMARY_SCHEMA, + status, + "candidate-unqualified-non-release", + detail, + len(rows), + f"{p50:.6f}", + f"{p95:.6f}", + "" if args.max_p95_ms is None else f"{args.max_p95_ms:.6f}", + max(unsigned(row, "whole_scope_memory_peak_bytes") for row in rows), + max(unsigned(row, "peak_file_dirty_bytes") for row in rows), + max(unsigned(row, "peak_file_writeback_bytes") for row in rows), + io_first_touch_status, + io_controller_status, + io_missing_reason, + len(io_available_rows), + len(io_unavailable_rows), + total_io_read_bytes, + total_io_write_bytes, + next(iter(execution_identities)), + next(iter(carrier_roots)), + next(iter(memory_limits)), + next(iter(high_limits)), + next(iter(swap_limits)), + ] + columns = [ + "schema_version", + "status", + "classification", + "detail", + "blocks", + "spawn_to_first_query_p50_ms", + "spawn_to_first_query_p95_ms", + "max_p95_ms", + "max_whole_scope_memory_peak_bytes", + "max_file_dirty_bytes", + "max_file_writeback_bytes", + "io_first_touch_status", + "io_controller_status", + "io_missing_reason", + "io_attributable_block_count", + "io_unavailable_block_count", + "total_io_read_bytes", + "total_io_write_bytes", + "execution_identity_sha256", + "carrier_root", + "memory_max_bytes", + "memory_high_bytes", + "swap_max_bytes", + ] + if len(values) != len(columns): + fail("internal cold summary schema mismatch") + args.output.parent.mkdir(parents=True, exist_ok=True) + pending = args.output.with_name(f".{args.output.name}.pending.{os.getpid()}") + with pending.open("x", encoding="utf-8", newline="") as stream: + writer = csv.writer(stream, delimiter="\t", lineterminator="\n") + writer.writerow(columns) + writer.writerow(values) + stream.flush() + os.fsync(stream.fileno()) + os.replace(pending, args.output) + receipt = { + "schema_version": "oliphaunt.wasix-postmaster.cold-ownership-qualification-receipt.v1", + "status": status, + "classification": "candidate-unqualified-non-release", + "summary_sha256": sha256_file(args.output), + "summarizer_sha256": sha256_file(Path(__file__)), + "inputs": inputs, + } + pending_receipt = args.receipt.with_name(f".{args.receipt.name}.pending.{os.getpid()}") + with pending_receipt.open("x", encoding="utf-8", newline="\n") as stream: + json.dump(receipt, stream, indent=2, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + os.replace(pending_receipt, args.receipt) + return 0 if status == "passed" else 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (SummaryError, OSError, ValueError) as error: + print(f"cold-ownership summary failed: {error}", file=sys.stderr) + raise SystemExit(1) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-wasix-perf-stats.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-wasix-perf-stats.sh new file mode 100755 index 00000000..8541a090 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/summarize-wasix-perf-stats.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: summarize-wasix-perf-stats.sh PERF_LOG [OUTPUT_PREFIX] + +Extracts the last cumulative WASIX perf-stats snapshot from PERF_LOG and writes: + OUTPUT_PREFIX.tsv raw counters + OUTPUT_PREFIX.top-time.tsv counters sorted by total_ns descending + OUTPUT_PREFIX.top-bytes.tsv counters sorted by total_bytes descending + OUTPUT_PREFIX.pwrite-paths.tsv per-path fd_pwrite counters + OUTPUT_PREFIX.pwrite-paths.top-time.tsv per-path fd_pwrite counters by time + OUTPUT_PREFIX.pwrite-paths.top-bytes.tsv per-path fd_pwrite counters by bytes + +The runtime must be built with the wasmer-wasix `perf-stats` feature and run +with WASIX_PERF_STATS=1. Set WASIX_PERF_STATS_FILE to choose PERF_LOG. +EOF +} + +if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then + usage >&2 + exit 2 +fi + +perf_log="$1" +output_prefix="${2:-${perf_log%.log}}" + +if [ ! -s "$perf_log" ]; then + printf 'missing or empty perf log: %s\n' "$perf_log" >&2 + exit 2 +fi + +raw_tsv="$output_prefix.tsv" +top_time_tsv="$output_prefix.top-time.tsv" +top_bytes_tsv="$output_prefix.top-bytes.tsv" +pwrite_paths_tsv="$output_prefix.pwrite-paths.tsv" +pwrite_paths_top_time_tsv="$output_prefix.pwrite-paths.top-time.tsv" +pwrite_paths_top_bytes_tsv="$output_prefix.pwrite-paths.top-bytes.tsv" + +perl -Mstrict -Mwarnings -e ' + my ($in, $raw, $pwrite_paths) = @ARGV; + open my $fh, "<", $in or die "open $in: $!"; + my @current_stats; + my @current_paths; + my @last_stats; + my @last_paths; + my $inside = 0; + while (my $line = <$fh>) { + chomp $line; + if ($line =~ /^wasix-perf-stats\tbegin\t/) { + @current_stats = (); + @current_paths = (); + $inside = 1; + next; + } + if ($line eq "wasix-perf-stats\tend") { + if (@current_stats || @current_paths) { + @last_stats = @current_stats; + @last_paths = @current_paths; + } + $inside = 0; + next; + } + next unless $inside; + if ($line =~ /^wasix-perf-stat\t/) { + next if $line =~ /^wasix-perf-stat\tname\t/; + push @current_stats, $line; + next; + } + if ($line =~ /^wasix-perf-pwrite-path\t/) { + next if $line =~ /^wasix-perf-pwrite-path\tpath\t/; + push @current_paths, $line; + next; + } + } + close $fh; + die "no perf stat snapshot found in $in\n" unless @last_stats || @last_paths; + + open my $out, ">", $raw or die "open $raw: $!"; + print {$out} "name\tcalls\ttotal_ns\tmax_ns\tavg_ns\ttotal_bytes\tmax_bytes\tlast_bytes\n"; + for my $line (@last_stats) { + $line =~ s/^wasix-perf-stat\t//; + print {$out} "$line\n"; + } + close $out; + + open my $paths_out, ">", $pwrite_paths or die "open $pwrite_paths: $!"; + print {$paths_out} "path\tcalls\ttotal_ns\tmax_ns\tavg_ns\ttotal_bytes\tmax_bytes\tlast_bytes\n"; + for my $line (@last_paths) { + $line =~ s/^wasix-perf-pwrite-path\t//; + print {$paths_out} "$line\n"; + } + close $paths_out; +' "$perf_log" "$raw_tsv" "$pwrite_paths_tsv" + +{ + head -n 1 "$raw_tsv" + tail -n +2 "$raw_tsv" | sort -t $'\t' -k3,3nr +} >"$top_time_tsv" + +{ + head -n 1 "$raw_tsv" + tail -n +2 "$raw_tsv" | sort -t $'\t' -k6,6nr +} >"$top_bytes_tsv" + +{ + head -n 1 "$pwrite_paths_tsv" + tail -n +2 "$pwrite_paths_tsv" | sort -t $'\t' -k3,3nr +} >"$pwrite_paths_top_time_tsv" + +{ + head -n 1 "$pwrite_paths_tsv" + tail -n +2 "$pwrite_paths_tsv" | sort -t $'\t' -k6,6nr +} >"$pwrite_paths_top_bytes_tsv" + +printf 'wrote %s, %s, %s, %s, %s, and %s\n' \ + "$raw_tsv" \ + "$top_time_tsv" \ + "$top_bytes_tsv" \ + "$pwrite_paths_tsv" \ + "$pwrite_paths_top_time_tsv" \ + "$pwrite_paths_top_bytes_tsv" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/symbolize-wasmer-sample.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/symbolize-wasmer-sample.sh new file mode 100755 index 00000000..52232c04 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/symbolize-wasmer-sample.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <&2 + exit 2 +fi + +sample_txt="$1" +perf_map="$2" +output_prefix="$3" + +if [ ! -s "$sample_txt" ]; then + echo "missing or empty sample report: $sample_txt" >&2 + exit 2 +fi +if [ ! -s "$perf_map" ]; then + echo "missing or empty perf map: $perf_map" >&2 + exit 2 +fi + +perl -Mstrict -Mwarnings -e ' + no warnings "portable"; + + my ($sample_txt, $perf_map, $annotated_out, $top_out) = @ARGV; + + open my $perf_fh, "<", $perf_map or die "open $perf_map: $!"; + my @entries; + while (my $line = <$perf_fh>) { + chomp $line; + next unless $line =~ /^0x([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.+)$/; + my $start = hex($1); + my $size = hex($2); + next if $size <= 0; + push @entries, [$start, $start + $size, $size, $3]; + } + close $perf_fh; + @entries = sort { $a->[0] <=> $b->[0] } @entries; + + sub lookup_addr { + my ($addr) = @_; + my ($lo, $hi) = (0, scalar(@entries) - 1); + while ($lo <= $hi) { + my $mid = int(($lo + $hi) / 2); + my $entry = $entries[$mid]; + if ($addr < $entry->[0]) { + $hi = $mid - 1; + } elsif ($addr >= $entry->[1]) { + $lo = $mid + 1; + } else { + return ($entry->[3], $addr - $entry->[0], $entry->[2]); + } + } + return; + } + + open my $sample_fh, "<", $sample_txt or die "open $sample_txt: $!"; + open my $annotated_fh, ">", $annotated_out or die "open $annotated_out: $!"; + + my %top_counts; + my $in_top = 0; + while (my $line = <$sample_fh>) { + if ($line =~ /^Sort by top of stack/) { + $in_top = 1; + } elsif ($line =~ /^Binary Images:/) { + $in_top = 0; + } + + my @annotations; + while ($line =~ /\[(0x[0-9a-fA-F]+)\]/g) { + my $addr = hex($1); + my @hit = lookup_addr($addr); + next unless @hit; + push @annotations, sprintf("%s=>%s+0x%x/0x%x", $1, $hit[0], $hit[1], $hit[2]); + } + if (@annotations) { + chomp $line; + print {$annotated_fh} $line, " # ", join("; ", @annotations), "\n"; + } else { + print {$annotated_fh} $line; + } + + next unless $in_top; + next unless $line =~ /\[(0x[0-9a-fA-F]+)\]\s+([0-9]+)\s*$/; + my $addr = hex($1); + my $count = $2 + 0; + my @hit = lookup_addr($addr); + next unless @hit; + my $symbol = sprintf("%s+0x%x/0x%x", $hit[0], $hit[1], $hit[2]); + $top_counts{$symbol} += $count; + } + close $sample_fh; + close $annotated_fh; + + open my $top_fh, ">", $top_out or die "open $top_out: $!"; + print {$top_fh} "samples\tsymbol\n"; + for my $symbol (sort { $top_counts{$b} <=> $top_counts{$a} || $a cmp $b } keys %top_counts) { + print {$top_fh} $top_counts{$symbol}, "\t", $symbol, "\n"; + } + close $top_fh; +' "$sample_txt" "$perf_map" "$output_prefix.txt" "$output_prefix.top.tsv" + +printf 'wrote %s and %s\n' "$output_prefix.txt" "$output_prefix.top.tsv" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-adaptive-file-cache-telemetry.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-adaptive-file-cache-telemetry.py new file mode 100755 index 00000000..1f32c960 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-adaptive-file-cache-telemetry.py @@ -0,0 +1,1586 @@ +#!/usr/bin/env python3 + +"""Validate exact adaptive-v5 or fail-closed cache-policy evidence.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import stat +import sys +from pathlib import Path +from typing import Any + + +ACTIVE_SCHEMA = "oliphaunt.wasix-postmaster.file-cache-adaptive-telemetry.v5" +FALLBACK_SCHEMA = "oliphaunt.wasix-postmaster.file-cache-admission-fallback.v1" +RESULT_SCHEMA = "oliphaunt.wasix-postmaster.file-cache-adaptive-validation.v5" +POLICY_ID = "oliphaunt.wasix-postmaster.file-cache.adaptive-linux.v5" +POLICY_MODE = "adaptive-linux-explicit" +CONFIG_ID = "oliphaunt.wasix-postmaster.file-cache.adaptive-linux.embedded-v4" +CONFIG_SHA256 = "01668b856435cb8c34b2d2324ab55b7f1f5961b8b403c1ee49d9ee4b5c865f53" +FALLBACK_POLICY_ID = "oliphaunt.wasix-postmaster.file-cache.observe-only.v1" +FALLBACK_POLICY_MODE = "observe-only-retain" +MANIFEST_FALLBACK_MODE = "observe-only" +PORTABLE_ACCEPTANCE_POLICY = "portable-correctness-v1" +CONSTRAINED_ACCEPTANCE_POLICY = "constrained-linux-wal-action-v1" +ACCEPTANCE_POLICIES = { + PORTABLE_ACCEPTANCE_POLICY, + CONSTRAINED_ACCEPTANCE_POLICY, +} +SHA256_RE = re.compile(r"[0-9a-f]{64}\Z") +CGROUP_IDENTITY_RE = re.compile(r"([1-9][0-9]*):([1-9][0-9]*)\Z") +MEASUREMENT_ID_RE = re.compile(r"[A-Za-z0-9._-]+\Z") +MEBIBYTE = 1024 * 1024 + +CONFIG = { + "page-alignment": "host-page", + "sample-interval-ns": 250_000_000, + "warmup-samples": 3, + "enter-level1-per-mille": 780, + "exit-level1-per-mille": 720, + "enter-level2-per-mille": 850, + "exit-level2-per-mille": 800, + "enter-level3-per-mille": 920, + "exit-level3-per-mille": 870, + "emergency-headroom-bytes": 24 * 1024 * 1024, + "cooldown-ns": 2_000_000_000, + "circuit-breaker-cooldown-ns": 5_000_000_000, + "healthy-samples-to-recover": 3, + "max-dirty-bytes": 16 * 1024 * 1024, + "max-dirty-per-mille": 80, + "immediate-wal-cache-drop-safe": False, + "relation-pressure-relief": False, + "allow-wal-cache-drop-safe-dirty-bypass": True, + "wal-emergency-max-bytes": 16 * 1024 * 1024, + "deferred-wal-max-entries": 4, + "deferred-wal-max-bytes": 64 * 1024 * 1024, + "deferred-wal-max-fds": 4, + "deferred-wal-ttl-ns": 4_000_000_000, + "deferred-wal-drain-per-trigger": 1, + "deferred-wal-busy-retries": 0, + "bytes-per-second": 32 * 1024 * 1024, + "burst-bytes": 32 * 1024 * 1024, + "max-bytes-per-offer": 16 * 1024 * 1024, + "min-bytes-per-offer": 4096, + "psi-some-breaker-per-mille": 250, + "psi-full-breaker-per-mille": 100, + "refault-min-pages": 256, + "refault-breaker-per-mille": 500, +} +MANIFEST_POLICY_FIELDS = { + "requested-policy-id", + "approved-config-id", + "config-sha256", + "portable-fallback-mode", +} +ACTIVE_FIELDS = { + "schema", + "policy-id", + "policy-mode", + "workload-id", + "runtime-abi-id", + "fallback-policy-id", + "fallback-policy-mode", + "config", + "resolved-page-bytes", + "state", + "sample-count", + "state-transitions", + "sample-errors", + "clock-errors", + "psi-breaker-trips", + "refault-breaker-trips", + "dirty-vetoes", + "wal-dirty-veto-bypasses", + "wal-dirty-veto-bypass-bytes", + "range-offered-bytes", + "range-aligned-bytes", + "range-advised-bytes", + "token-bytes", + "state-deadline-ns", + "advised-bytes-since-sample", + "advice-errors", + "last-advice-raw-os-error", + "max-current-bytes", + "max-used-per-mille", + "max-file-context-bytes", + "max-file-dirty-bytes", + "last-psi-some-delta-us", + "last-psi-full-delta-us", + "last-psi-some-per-mille", + "last-psi-full-per-mille", + "psi-no-advice-baseline-some-per-mille", + "psi-no-advice-baseline-full-per-mille", + "last-refault-delta", + "last-local-high-event-delta", + "last-local-max-event-delta", + "last-local-oom-event-delta", + "workload-finalized", + "deferred-wal-maintenance-constructed", + "deferred-wal-maintenance-active", + "deferred-wal-queued-entries", + "deferred-wal-queued-bytes", + "deferred-wal-inflight-entries", + "deferred-wal-inflight-bytes", + "deferred-wal-oldest-age-ns", + "deferred-wal-oldest-overdue-ns", + "deferred-wal-high-entries", + "deferred-wal-high-bytes", + "deferred-wal-enqueued", + "deferred-wal-enqueued-bytes", + "deferred-wal-capacity-evicted", + "deferred-wal-capacity-evicted-bytes", + "deferred-wal-capacity-rejected", + "deferred-wal-capacity-rejected-bytes", + "deferred-wal-sequence-rejected", + "deferred-wal-sequence-rejected-bytes", + "deferred-wal-expired", + "deferred-wal-expired-bytes", + "deferred-wal-attempts", + "deferred-wal-attempted-bytes", + "deferred-wal-successes", + "deferred-wal-success-bytes", + "deferred-wal-busy", + "deferred-wal-busy-requeued", + "deferred-wal-busy-dropped", + "deferred-wal-busy-dropped-bytes", + "deferred-wal-invalidated", + "deferred-wal-invalidated-bytes", + "deferred-wal-revoked", + "deferred-wal-revoked-bytes", + "deferred-wal-errors", + "deferred-wal-error-bytes", + "deferred-wal-pin-errors", + "deferred-wal-flushes", + "deferred-wal-flushed-entries", + "deferred-wal-flushed-bytes", + "deferred-wal-sampler-flushes", + "deferred-wal-clock-flushes", + "deferred-wal-breaker-flushes", + "deferred-wal-advice-error-flushes", + "deferred-wal-drop-flushes", + "deferred-wal-finalization-flushes", + "deferred-wal-revoke-calls", + "deferred-wal-revoke-errors", + "deferred-wal-revoke-sequence", + "deferred-wal-pressure-samples", + "deferred-wal-actionable-samples", + "deferred-wal-open-fds", + "deferred-wal-mutation-epoch-identities", + "deferred-wal-terminal-entries", + "deferred-wal-terminal-bytes", + "deferred-wal-conservation-entries-ok", + "deferred-wal-conservation-bytes-ok", + "wal-emergency-oversize-rejects", + "wal-emergency-attempts", + "wal-emergency-attempted-bytes", + "wal-emergency-successes", + "wal-emergency-success-bytes", + "wal-emergency-current-attempts", + "wal-emergency-current-attempted-bytes", + "wal-emergency-current-successes", + "wal-emergency-current-success-bytes", + "wal-emergency-forced-samples", + "wal-emergency-max-actions-per-trigger", + "wal-emergency-max-bytes-per-trigger", + "action-gate-contended-calls", + "action-gate-contended-retained", + "action-gate-contended-enqueued", + "action-gate-contended-wal-pin-failures", + "action-gate-probe-total-ns", + "action-gate-probe-max-ns", + "finalization-quiescence-notifications", + "last-sample", + "classes", + "retain-reasons", + "validation", +} +ACTIVE_BOOL_FIELDS = { + "workload-finalized", + "deferred-wal-maintenance-constructed", + "deferred-wal-maintenance-active", + "deferred-wal-conservation-entries-ok", + "deferred-wal-conservation-bytes-ok", +} +ACTIVE_U32_FIELDS = { + "deferred-wal-queued-entries", + "deferred-wal-inflight-entries", + "deferred-wal-high-entries", + "deferred-wal-open-fds", + "deferred-wal-mutation-epoch-identities", + "wal-emergency-max-actions-per-trigger", +} +ACTIVE_U16_FIELDS = { + "max-used-per-mille", + "last-psi-some-per-mille", + "last-psi-full-per-mille", +} +ACTIVE_OPTIONAL_U16_FIELDS = { + "psi-no-advice-baseline-some-per-mille", + "psi-no-advice-baseline-full-per-mille", +} +FALLBACK_FIELDS = { + "schema", + "admission", + "requested-policy-id", + "requested-policy-mode", + "workload-id", + "runtime-abi-id", + "fallback-policy-id", + "fallback-policy-mode", + "reason", + "config", +} +CLASS_FIELDS = { + "class", + "name", + "offers", + "offered-finite-bytes", + "through-eof-offers", + "advice-calls", + "advised-bytes", + "partial-advice-calls", + "advice-errors", +} +RETAIN_FIELDS = {"reason", "calls"} +SAMPLE_FIELDS = { + "monotonic-ns", + "current-bytes", + "effective-limit-bytes", + "file-context-bytes", + "file-dirty-bytes", + "workingset-refault-file", + "psi-some-total-us", + "psi-full-total-us", + "local-high-events", + "local-max-events", + "local-oom-events", + "local-events-available", + "membership-leaf-device", + "membership-leaf-inode", + "pressure-source-device", + "pressure-source-inode", + "pressure-source-depth", +} +CLASSES = ( + (1, "relation-read-normal"), + (2, "relation-read-bulk"), + (3, "relation-read-vacuum"), + (4, "relation-sync-checkpoint"), + (5, "relation-sync-immediate"), + (6, "wal-inactive-durable"), +) +RETAIN_REASONS = ( + "unsupported-class", + "through-eof", + "non-host-backed", + "pressure-state", + "dirty-veto", + "empty-after-inward-alignment", + "rate-limited", + "sampler-unavailable", + "circuit-breaker", + "wal-cache-drop-proof-required", + "wal-whole-segment-required", + "workload-finalized", +) +STATES = { + "warmup", + "retain", + "relief-level1", + "relief-level2", + "relief-level3", + "cooldown", + "degraded", +} +FALLBACK_REASONS = {"unsupported", "invalid-evidence", "unavailable-io"} + + +class ValidationError(Exception): + pass + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ValidationError(message) + + +def duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + require(key not in result, f"duplicate JSON field: {key}") + result[key] = value + return result + + +def read_regular_stable(path: Path, label: str) -> bytes: + before = os.lstat(path) + require( + stat.S_ISREG(before.st_mode) and not stat.S_ISLNK(before.st_mode), + f"{label} must be a regular non-symlink file", + ) + descriptor = os.open( + path, + os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0), + ) + try: + opened = os.fstat(descriptor) + require( + (before.st_dev, before.st_ino) == (opened.st_dev, opened.st_ino), + f"{label} changed while opening", + ) + chunks: list[bytes] = [] + remaining = opened.st_size + while remaining: + chunk = os.read(descriptor, min(remaining, 1024 * 1024)) + require(bool(chunk), f"{label} was truncated while reading") + chunks.append(chunk) + remaining -= len(chunk) + after = os.fstat(descriptor) + require( + ( + opened.st_dev, + opened.st_ino, + opened.st_size, + opened.st_mtime_ns, + opened.st_ctime_ns, + ) + == ( + after.st_dev, + after.st_ino, + after.st_size, + after.st_mtime_ns, + after.st_ctime_ns, + ), + f"{label} changed while reading", + ) + return b"".join(chunks) + finally: + os.close(descriptor) + + +def parse_object(data: bytes, label: str) -> dict[str, Any]: + try: + value = json.loads(data.decode("utf-8"), object_pairs_hook=duplicate_keys) + except (UnicodeDecodeError, json.JSONDecodeError, ValidationError) as error: + raise ValidationError(f"invalid {label}: {error}") from error + require(isinstance(value, dict), f"{label} must be a JSON object") + return value + + +def exact_unsigned(value: Any, label: str, maximum: int = (1 << 64) - 1) -> int: + require(type(value) is int and 0 <= value <= maximum, f"{label} is out of range") + return value + + +def exact_optional_unsigned(value: Any, label: str, maximum: int) -> int | None: + if value is None: + return None + return exact_unsigned(value, label, maximum) + + +def require_canonical_wal_sum(count: int, byte_count: int, label: str) -> None: + """Require bytes to be the sum of count 1/2/4/8/16 MiB segments.""" + require( + (count == 0) == (byte_count == 0) and byte_count % MEBIBYTE == 0, + f"{label} count/byte presence or MiB alignment differs", + ) + units = byte_count // MEBIBYTE + require( + count <= units <= count * 16, + f"{label} bytes exceed canonical whole-WAL bounds", + ) + # Give every segment its mandatory 1 MiB baseline. A segment may then add + # exactly 0, 1, 3, 7, or 15 MiB. The canonical coin system below computes + # the fewest nonzero upgrades needed; the sum is representable iff those + # upgrades fit in the available segment count. + extra_units = units - count + upgrade_count = 0 + for upgrade in (15, 7, 3, 1): + quotient, extra_units = divmod(extra_units, upgrade) + upgrade_count += quotient + require( + extra_units == 0 and upgrade_count <= count, + f"{label} is not a sum of canonical 1/2/4/8/16 MiB WAL segments", + ) + + +def manifest_contract(manifest: dict[str, Any]) -> str: + runtime_abi_id = manifest.get("runtime-abi-id") + require( + isinstance(runtime_abi_id, str) and SHA256_RE.fullmatch(runtime_abi_id) is not None, + "sealed manifest runtime-abi-id is invalid", + ) + policy = manifest.get("file-cache-policy") + require( + isinstance(policy, dict) and set(policy) == MANIFEST_POLICY_FIELDS, + "sealed manifest file-cache-policy fields differ", + ) + expected = { + "requested-policy-id": POLICY_ID, + "approved-config-id": CONFIG_ID, + "config-sha256": CONFIG_SHA256, + "portable-fallback-mode": MANIFEST_FALLBACK_MODE, + } + require(policy == expected, "sealed manifest file-cache-policy differs") + canonical_config = json.dumps( + CONFIG, ensure_ascii=False, separators=(",", ":") + ).encode("utf-8") + require( + hashlib.sha256(canonical_config).hexdigest() == CONFIG_SHA256, + "validator compiled adaptive config digest differs", + ) + return runtime_abi_id + + +def validate_fixed_identity(telemetry: dict[str, Any], runtime_abi_id: str) -> None: + expected = { + "workload-id": "runtime:postgres", + "runtime-abi-id": runtime_abi_id, + "fallback-policy-id": FALLBACK_POLICY_ID, + "fallback-policy-mode": FALLBACK_POLICY_MODE, + } + for field, value in expected.items(): + require( + telemetry[field] == value, + f"adaptive telemetry {field} differs: expected {value!r}, got {telemetry[field]!r}", + ) + actual_config = telemetry["config"] + require( + isinstance(actual_config, dict) and set(actual_config) == set(CONFIG), + "adaptive telemetry config fields differ", + ) + for field, expected_value in CONFIG.items(): + actual_value = actual_config[field] + require( + type(actual_value) is type(expected_value) and actual_value == expected_value, + f"adaptive telemetry config {field} differs", + ) + + +def validate_last_sample( + value: Any, sample_count: int, sample_errors: int, clock_errors: int +) -> None: + if value is None: + require( + sample_count == 0 or sample_errors > 0 or clock_errors > 0, + "adaptive telemetry omitted a successful last sample without a sampler/clock error", + ) + return + require(sample_count > 0, "adaptive telemetry has a last sample without a sample") + require(isinstance(value, dict) and set(value) == SAMPLE_FIELDS, "last-sample fields differ") + for field in SAMPLE_FIELDS - {"local-events-available"}: + maximum = (1 << 32) - 1 if field == "pressure-source-depth" else (1 << 64) - 1 + exact_unsigned(value[field], f"last-sample {field}", maximum) + require( + type(value["local-events-available"]) is bool, + "last-sample local-events-available must be boolean", + ) + require(value["effective-limit-bytes"] > 0, "last-sample effective limit is not finite") + for field in ( + "membership-leaf-device", + "membership-leaf-inode", + "pressure-source-device", + "pressure-source-inode", + ): + require(value[field] > 0, f"last-sample {field} is not a pinned identity") + + +def validate_active(telemetry: dict[str, Any], runtime_abi_id: str) -> dict[str, Any]: + require(set(telemetry) == ACTIVE_FIELDS, "active adaptive telemetry fields differ") + validate_fixed_identity(telemetry, runtime_abi_id) + require(telemetry["schema"] == ACTIVE_SCHEMA, "active adaptive telemetry schema differs") + require(telemetry["policy-id"] == POLICY_ID, "active adaptive policy-id differs") + require(telemetry["policy-mode"] == POLICY_MODE, "active adaptive policy-mode differs") + require(telemetry["state"] in STATES, "active adaptive state is unknown") + + for field in ACTIVE_BOOL_FIELDS: + require(type(telemetry[field]) is bool, f"{field} must be boolean") + optional_u16 = { + field: exact_optional_unsigned(telemetry[field], field, (1 << 16) - 1) + for field in ACTIVE_OPTIONAL_U16_FIELDS + } + require( + (optional_u16["psi-no-advice-baseline-some-per-mille"] is None) + == (optional_u16["psi-no-advice-baseline-full-per-mille"] is None), + "adaptive PSI no-advice baselines must be present or absent together", + ) + for field, value in optional_u16.items(): + require(value is None or value <= 1000, f"{field} exceeds 1000") + + scalar_fields = ACTIVE_FIELDS - { + "schema", + "policy-id", + "policy-mode", + "workload-id", + "runtime-abi-id", + "fallback-policy-id", + "fallback-policy-mode", + "config", + "state", + "last-advice-raw-os-error", + "last-sample", + "classes", + "retain-reasons", + "validation", + } - ACTIVE_BOOL_FIELDS - ACTIVE_OPTIONAL_U16_FIELDS + scalars = { + field: exact_unsigned( + telemetry[field], + field, + (1 << 16) - 1 + if field in ACTIVE_U16_FIELDS + else (1 << 32) - 1 + if field in ACTIVE_U32_FIELDS + else (1 << 64) - 1, + ) + for field in scalar_fields + } + for field in ACTIVE_U16_FIELDS: + require(scalars[field] <= 1000, f"{field} exceeds 1000") + + resolved_page_bytes = scalars["resolved-page-bytes"] + require( + resolved_page_bytes >= 512 and resolved_page_bytes.bit_count() == 1, + "resolved-page-bytes is not a power of two >= 512", + ) + effective_minimum = ( + CONFIG["min-bytes-per-offer"] + resolved_page_bytes - 1 + ) & -resolved_page_bytes + require( + effective_minimum <= CONFIG["max-bytes-per-offer"] + and effective_minimum <= CONFIG["burst-bytes"], + "resolved page cannot fit one configured adaptive offer", + ) + require( + CONFIG["wal-emergency-max-bytes"] >= resolved_page_bytes + and CONFIG["wal-emergency-max-bytes"] % resolved_page_bytes == 0, + "resolved page cannot represent a complete emergency WAL action", + ) + require(scalars["token-bytes"] <= CONFIG["burst-bytes"], "token-bytes exceeds burst") + + require(telemetry["workload-finalized"], "active adaptive receipt is not workload-finalized") + require( + telemetry["deferred-wal-maintenance-constructed"], + "active adaptive receipt did not construct bounded WAL maintenance", + ) + require( + not telemetry["deferred-wal-maintenance-active"], + "active adaptive receipt retained live WAL maintenance", + ) + for field in ( + "deferred-wal-queued-entries", + "deferred-wal-queued-bytes", + "deferred-wal-inflight-entries", + "deferred-wal-inflight-bytes", + "deferred-wal-oldest-age-ns", + "deferred-wal-oldest-overdue-ns", + "deferred-wal-open-fds", + "deferred-wal-mutation-epoch-identities", + ): + require(scalars[field] == 0, f"terminal adaptive receipt has nonzero {field}") + require( + telemetry["deferred-wal-conservation-entries-ok"] + and telemetry["deferred-wal-conservation-bytes-ok"], + "terminal adaptive receipt failed deferred WAL conservation", + ) + + raw_error = telemetry["last-advice-raw-os-error"] + require( + raw_error is None or (type(raw_error) is int and -(1 << 31) <= raw_error < (1 << 31)), + "last-advice-raw-os-error is not null or i32", + ) + validate_last_sample( + telemetry["last-sample"], + scalars["sample-count"], + scalars["sample-errors"], + scalars["clock-errors"], + ) + if scalars["sample-count"] < CONFIG["warmup-samples"]: + require( + telemetry["state"] in {"warmup", "degraded"}, + "pre-warmup adaptive state is not warmup or degraded", + ) + if telemetry["last-sample"] is not None: + last_sample_value = telemetry["last-sample"] + last_used_per_mille = min( + 1000, + last_sample_value["current-bytes"] + * 1000 + // last_sample_value["effective-limit-bytes"], + ) + require( + scalars["max-current-bytes"] >= last_sample_value["current-bytes"] + and scalars["max-file-context-bytes"] + >= last_sample_value["file-context-bytes"] + and scalars["max-file-dirty-bytes"] + >= last_sample_value["file-dirty-bytes"] + and scalars["max-used-per-mille"] >= last_used_per_mille, + "adaptive sample exceeds its recorded high-water telemetry", + ) + require( + last_sample_value["file-dirty-bytes"] + <= last_sample_value["file-context-bytes"], + "last-sample file-dirty bytes exceed file-context bytes", + ) + require( + scalars["last-psi-some-delta-us"] + <= last_sample_value["psi-some-total-us"] + and scalars["last-psi-full-delta-us"] + <= last_sample_value["psi-full-total-us"] + and scalars["last-refault-delta"] + <= last_sample_value["workingset-refault-file"] + and scalars["last-local-high-event-delta"] + <= last_sample_value["local-high-events"] + and scalars["last-local-max-event-delta"] + <= last_sample_value["local-max-events"] + and scalars["last-local-oom-event-delta"] + <= last_sample_value["local-oom-events"], + "adaptive last-sample delta exceeds its cumulative source counter", + ) + require( + scalars["last-psi-some-delta-us"] > 0 + or scalars["last-psi-some-per-mille"] == 0, + "zero PSI-some delta has a nonzero rate", + ) + require( + scalars["last-psi-full-delta-us"] > 0 + or scalars["last-psi-full-per-mille"] == 0, + "zero PSI-full delta has a nonzero rate", + ) + if not last_sample_value["local-events-available"]: + require( + last_sample_value["local-high-events"] == 0 + and last_sample_value["local-max-events"] == 0 + and last_sample_value["local-oom-events"] == 0, + "last-sample unavailable local events have nonzero counters", + ) + + classes = telemetry["classes"] + require(isinstance(classes, list) and len(classes) == len(CLASSES), "adaptive class closure differs") + total_offers = 0 + total_finite_bytes = 0 + total_advice_calls = 0 + total_advised_bytes = 0 + total_advice_errors = 0 + wal_offers = 0 + wal_advice_calls = 0 + wal_advised_bytes = 0 + wal_advice_errors = 0 + wal_finite_offers = 0 + for index, ((class_id, name), record) in enumerate(zip(CLASSES, classes, strict=True)): + require(isinstance(record, dict) and set(record) == CLASS_FIELDS, f"adaptive class {index} fields differ") + require(record["class"] == class_id and type(record["class"]) is int, f"adaptive class {index} id differs") + require(record["name"] == name, f"adaptive class {class_id} name differs") + counters = { + field: exact_unsigned(record[field], f"adaptive class {class_id} {field}") + for field in CLASS_FIELDS - {"class", "name"} + } + require(counters["through-eof-offers"] <= counters["offers"], f"adaptive class {class_id} through-EOF offers exceed offers") + finite_offers = counters["offers"] - counters["through-eof-offers"] + require( + (finite_offers == 0) == (counters["offered-finite-bytes"] == 0), + f"adaptive class {class_id} finite offer count/byte presence differs", + ) + require(counters["partial-advice-calls"] <= counters["advice-calls"], f"adaptive class {class_id} partial advice exceeds advice calls") + if class_id == 6: + require( + counters["partial-advice-calls"] == 0, + "class-6 WAL action reported partial advice", + ) + require(counters["advice-calls"] + counters["advice-errors"] <= finite_offers, f"adaptive class {class_id} advice attempts exceed finite offers") + require(counters["advised-bytes"] % resolved_page_bytes == 0, f"adaptive class {class_id} advised bytes are not page aligned") + require( + counters["advised-bytes"] <= counters["offered-finite-bytes"], + f"adaptive class {class_id} advised bytes exceed finite offered bytes", + ) + require( + (counters["advice-calls"] == 0) == (counters["advised-bytes"] == 0), + f"adaptive class {class_id} advice call/byte presence differs", + ) + if counters["advice-calls"] > 0: + require( + counters["advice-calls"] * resolved_page_bytes + <= counters["advised-bytes"] + <= counters["advice-calls"] * CONFIG["max-bytes-per-offer"], + f"adaptive class {class_id} advised bytes exceed per-call bounds", + ) + if class_id in (1, 4, 5) or ( + class_id in (2, 3) and not CONFIG["relation-pressure-relief"] + ): + require( + counters["advice-calls"] == 0 + and counters["advised-bytes"] == 0 + and counters["partial-advice-calls"] == 0 + and counters["advice-errors"] == 0, + f"non-acting class {class_id} reported advice activity", + ) + total_offers += counters["offers"] + total_finite_bytes += counters["offered-finite-bytes"] + total_advice_calls += counters["advice-calls"] + total_advised_bytes += counters["advised-bytes"] + total_advice_errors += counters["advice-errors"] + if class_id == 6: + wal_finite_offers = finite_offers + wal_offers = counters["offers"] + wal_advice_calls = counters["advice-calls"] + wal_advised_bytes = counters["advised-bytes"] + wal_advice_errors = counters["advice-errors"] + + retain = telemetry["retain-reasons"] + require(isinstance(retain, list) and len(retain) == len(RETAIN_REASONS), "adaptive retain-reason closure differs") + total_retained = 0 + retain_counts: dict[str, int] = {} + for index, (name, record) in enumerate(zip(RETAIN_REASONS, retain, strict=True)): + require(isinstance(record, dict) and set(record) == RETAIN_FIELDS, f"adaptive retain reason {index} fields differ") + require(record["reason"] == name, f"adaptive retain reason {index} differs") + calls = exact_unsigned(record["calls"], f"adaptive retain reason {name} calls") + retain_counts[name] = calls + total_retained += calls + + require( + retain_counts["sampler-unavailable"] == 0 + or scalars["sample-errors"] > 0 + or scalars["clock-errors"] > 0 + or telemetry["state"] == "degraded", + "sampler-unavailable retains lack sampler/clock error or degraded state", + ) + require( + retain_counts["circuit-breaker"] == 0 + or scalars["psi-breaker-trips"] > 0 + or scalars["refault-breaker-trips"] > 0 + or scalars["advice-errors"] > 0 + or telemetry["state"] == "degraded", + "circuit-breaker retains lack breaker/advice history or degraded state", + ) + require( + retain_counts["dirty-veto"] == scalars["dirty-vetoes"], + "dirty-veto retains differ from dirty-veto telemetry", + ) + require( + retain_counts["workload-finalized"] == 0, + "authoritative terminal receipt recorded a post-finalization offer", + ) + + validation = telemetry["validation"] + require(isinstance(validation, list) and len(validation) == 10, "adaptive validation closure differs") + validation_counts = [ + exact_unsigned(value, f"adaptive validation index {index}") + for index, value in enumerate(validation) + ] + require(validation_counts[0] == total_offers, "valid adaptive offers and class offers differ") + require(all(value == 0 for value in validation_counts[1:]), "validated product emitted invalid adaptive offers") + deferred_terminal_dispositions = sum( + scalars[field] + for field in ( + "deferred-wal-capacity-evicted", + "deferred-wal-expired", + "deferred-wal-busy-dropped", + "deferred-wal-invalidated", + "deferred-wal-revoked", + "deferred-wal-flushed-entries", + ) + ) + require( + total_retained + + total_advice_calls + + total_advice_errors + + deferred_terminal_dispositions + == total_offers, + "adaptive terminal offer dispositions do not conserve offers", + ) + require(total_finite_bytes == scalars["range-offered-bytes"], "adaptive offered-byte totals differ") + require(total_advised_bytes == scalars["range-advised-bytes"], "adaptive advised-byte totals differ") + require(scalars["range-advised-bytes"] <= scalars["range-aligned-bytes"] <= scalars["range-offered-bytes"], "adaptive range byte ordering differs") + require(scalars["advised-bytes-since-sample"] <= scalars["range-advised-bytes"], "adaptive advised-since-sample exceeds total") + require( + total_advice_errors + scalars["deferred-wal-invalidated"] + == scalars["advice-errors"], + "adaptive advice-error totals differ from class errors plus invalidations", + ) + require( + raw_error is None or total_advice_errors > 0, + "adaptive raw advice error exists without an advice error", + ) + + aligned_byte_fields = { + "wal-dirty-veto-bypass-bytes", + "range-aligned-bytes", + "range-advised-bytes", + "advised-bytes-since-sample", + "deferred-wal-queued-bytes", + "deferred-wal-inflight-bytes", + "deferred-wal-high-bytes", + "deferred-wal-enqueued-bytes", + "deferred-wal-capacity-evicted-bytes", + "deferred-wal-capacity-rejected-bytes", + "deferred-wal-sequence-rejected-bytes", + "deferred-wal-expired-bytes", + "deferred-wal-attempted-bytes", + "deferred-wal-success-bytes", + "deferred-wal-busy-dropped-bytes", + "deferred-wal-invalidated-bytes", + "deferred-wal-revoked-bytes", + "deferred-wal-error-bytes", + "deferred-wal-flushed-bytes", + "deferred-wal-terminal-bytes", + "wal-emergency-attempted-bytes", + "wal-emergency-success-bytes", + "wal-emergency-current-attempted-bytes", + "wal-emergency-current-success-bytes", + "wal-emergency-max-bytes-per-trigger", + } + for field in aligned_byte_fields: + require( + scalars[field] % resolved_page_bytes == 0, + f"{field} is not resolved-page aligned", + ) + + count_byte_pairs = ( + ("wal-dirty-veto-bypasses", "wal-dirty-veto-bypass-bytes"), + ("deferred-wal-queued-entries", "deferred-wal-queued-bytes"), + ("deferred-wal-inflight-entries", "deferred-wal-inflight-bytes"), + ("deferred-wal-high-entries", "deferred-wal-high-bytes"), + ("deferred-wal-enqueued", "deferred-wal-enqueued-bytes"), + ("deferred-wal-capacity-evicted", "deferred-wal-capacity-evicted-bytes"), + ("deferred-wal-capacity-rejected", "deferred-wal-capacity-rejected-bytes"), + ("deferred-wal-sequence-rejected", "deferred-wal-sequence-rejected-bytes"), + ("deferred-wal-expired", "deferred-wal-expired-bytes"), + ("deferred-wal-attempts", "deferred-wal-attempted-bytes"), + ("deferred-wal-successes", "deferred-wal-success-bytes"), + ("deferred-wal-busy-dropped", "deferred-wal-busy-dropped-bytes"), + ("deferred-wal-invalidated", "deferred-wal-invalidated-bytes"), + ("deferred-wal-revoked", "deferred-wal-revoked-bytes"), + ("deferred-wal-errors", "deferred-wal-error-bytes"), + ("deferred-wal-flushed-entries", "deferred-wal-flushed-bytes"), + ("deferred-wal-terminal-entries", "deferred-wal-terminal-bytes"), + ("wal-emergency-attempts", "wal-emergency-attempted-bytes"), + ("wal-emergency-successes", "wal-emergency-success-bytes"), + ("wal-emergency-current-attempts", "wal-emergency-current-attempted-bytes"), + ("wal-emergency-current-successes", "wal-emergency-current-success-bytes"), + ("wal-emergency-max-actions-per-trigger", "wal-emergency-max-bytes-per-trigger"), + ) + for count_field, byte_field in count_byte_pairs: + require_canonical_wal_sum( + scalars[count_field], + scalars[byte_field], + f"{count_field}/{byte_field}", + ) + require_canonical_wal_sum(wal_advice_calls, wal_advised_bytes, "class-6 advice") + require_canonical_wal_sum( + wal_finite_offers, + classes[5]["offered-finite-bytes"], + "class-6 finite offers", + ) + + require( + scalars["deferred-wal-enqueued"] + == scalars["deferred-wal-queued-entries"] + + scalars["deferred-wal-inflight-entries"] + + scalars["deferred-wal-capacity-evicted"] + + scalars["deferred-wal-expired"] + + scalars["deferred-wal-successes"] + + scalars["deferred-wal-busy-dropped"] + + scalars["deferred-wal-invalidated"] + + scalars["deferred-wal-revoked"] + + scalars["deferred-wal-errors"] + + scalars["deferred-wal-flushed-entries"], + "deferred WAL entry dispositions do not conserve enqueues", + ) + require( + scalars["deferred-wal-enqueued-bytes"] + == scalars["deferred-wal-queued-bytes"] + + scalars["deferred-wal-inflight-bytes"] + + scalars["deferred-wal-capacity-evicted-bytes"] + + scalars["deferred-wal-expired-bytes"] + + scalars["deferred-wal-success-bytes"] + + scalars["deferred-wal-busy-dropped-bytes"] + + scalars["deferred-wal-invalidated-bytes"] + + scalars["deferred-wal-revoked-bytes"] + + scalars["deferred-wal-error-bytes"] + + scalars["deferred-wal-flushed-bytes"], + "deferred WAL byte dispositions do not conserve enqueues", + ) + terminal_entry_sum = sum( + scalars[field] + for field in ( + "deferred-wal-successes", + "deferred-wal-capacity-evicted", + "deferred-wal-expired", + "deferred-wal-busy-dropped", + "deferred-wal-invalidated", + "deferred-wal-revoked", + "deferred-wal-errors", + "deferred-wal-flushed-entries", + ) + ) + terminal_byte_sum = sum( + scalars[field] + for field in ( + "deferred-wal-success-bytes", + "deferred-wal-capacity-evicted-bytes", + "deferred-wal-expired-bytes", + "deferred-wal-busy-dropped-bytes", + "deferred-wal-invalidated-bytes", + "deferred-wal-revoked-bytes", + "deferred-wal-error-bytes", + "deferred-wal-flushed-bytes", + ) + ) + require( + scalars["deferred-wal-terminal-entries"] == terminal_entry_sum + and scalars["deferred-wal-terminal-bytes"] == terminal_byte_sum + and scalars["deferred-wal-enqueued"] == terminal_entry_sum + and scalars["deferred-wal-enqueued-bytes"] == terminal_byte_sum, + "terminal deferred WAL conservation fields differ from exact dispositions", + ) + require( + scalars["deferred-wal-attempts"] + == scalars["deferred-wal-successes"] + + scalars["deferred-wal-busy"] + + scalars["deferred-wal-invalidated"] + + scalars["deferred-wal-errors"], + "deferred WAL attempt dispositions do not conserve attempts", + ) + require( + scalars["deferred-wal-busy"] + == scalars["deferred-wal-busy-requeued"] + + scalars["deferred-wal-busy-dropped"], + "deferred WAL busy dispositions do not conserve attempts", + ) + require( + scalars["deferred-wal-busy-requeued"] == 0 + and scalars["deferred-wal-attempts"] <= scalars["deferred-wal-enqueued"] + and scalars["deferred-wal-attempts"] + <= scalars["deferred-wal-actionable-samples"], + "deferred WAL work exceeds the no-retry, one-fresh-sample contract", + ) + require( + scalars["deferred-wal-attempted-bytes"] + <= scalars["deferred-wal-enqueued-bytes"] + * (CONFIG["deferred-wal-busy-retries"] + 1), + "deferred WAL attempted bytes exceed sealed per-entry retry bounds", + ) + require( + scalars["deferred-wal-attempted-bytes"] + >= scalars["deferred-wal-success-bytes"] + + scalars["deferred-wal-busy-dropped-bytes"] + + scalars["deferred-wal-invalidated-bytes"] + + scalars["deferred-wal-error-bytes"], + "deferred WAL terminal bytes exceed attempted bytes", + ) + require( + scalars["deferred-wal-flushes"] + == sum( + scalars[field] + for field in ( + "deferred-wal-sampler-flushes", + "deferred-wal-clock-flushes", + "deferred-wal-breaker-flushes", + "deferred-wal-advice-error-flushes", + "deferred-wal-drop-flushes", + "deferred-wal-finalization-flushes", + ) + ), + "deferred WAL flush reasons do not conserve flushes", + ) + require( + scalars["deferred-wal-flushes"] + <= scalars["deferred-wal-flushed-entries"] + and scalars["deferred-wal-drop-flushes"] == 0 + and scalars["deferred-wal-finalization-flushes"] <= 1, + "terminal deferred WAL flush accounting exceeds finalization bounds", + ) + require( + scalars["deferred-wal-sampler-flushes"] <= scalars["sample-errors"] + and scalars["deferred-wal-clock-flushes"] <= scalars["clock-errors"] + and scalars["deferred-wal-advice-error-flushes"] + <= scalars["advice-errors"], + "deferred WAL error flush lacks its owning sampler/clock/advice error", + ) + + require( + scalars["wal-emergency-attempts"] + == scalars["wal-emergency-current-attempts"] + + scalars["deferred-wal-attempts"], + "aggregate WAL attempts differ from current plus deferred attempts", + ) + require( + scalars["wal-emergency-attempted-bytes"] + == scalars["wal-emergency-current-attempted-bytes"] + + scalars["deferred-wal-attempted-bytes"], + "aggregate WAL attempted bytes differ from current plus deferred bytes", + ) + require( + scalars["wal-emergency-successes"] + == scalars["wal-emergency-current-successes"] + + scalars["deferred-wal-successes"], + "aggregate WAL successes differ from current plus deferred successes", + ) + require( + scalars["wal-emergency-success-bytes"] + == scalars["wal-emergency-current-success-bytes"] + + scalars["deferred-wal-success-bytes"], + "aggregate WAL success bytes differ from current plus deferred success bytes", + ) + require( + scalars["wal-emergency-current-attempts"] == 0 + and scalars["wal-emergency-current-attempted-bytes"] == 0 + and scalars["wal-emergency-current-successes"] == 0 + and scalars["wal-emergency-current-success-bytes"] == 0 + and scalars["wal-emergency-forced-samples"] == 0, + "adaptive v5 receipt contains a forbidden immediate WAL action", + ) + require( + scalars["wal-emergency-successes"] == wal_advice_calls + and scalars["wal-emergency-success-bytes"] == wal_advised_bytes, + "aggregate WAL successes differ from class-6 advice", + ) + require( + scalars["wal-emergency-current-successes"] + <= scalars["wal-emergency-current-attempts"] + and scalars["deferred-wal-successes"] <= scalars["deferred-wal-attempts"], + "WAL successes exceed attempts", + ) + require( + scalars["wal-emergency-success-bytes"] + <= scalars["wal-emergency-attempted-bytes"] + and scalars["wal-emergency-current-success-bytes"] + <= scalars["wal-emergency-current-attempted-bytes"] + and scalars["deferred-wal-success-bytes"] + <= scalars["deferred-wal-attempted-bytes"], + "WAL successful bytes exceed attempted bytes", + ) + current_wal_errors = ( + scalars["wal-emergency-current-attempts"] + - scalars["wal-emergency-current-successes"] + ) + current_wal_error_bytes = ( + scalars["wal-emergency-current-attempted-bytes"] + - scalars["wal-emergency-current-success-bytes"] + ) + require_canonical_wal_sum( + current_wal_errors, + current_wal_error_bytes, + "current WAL failed outcomes", + ) + deferred_wal_busy_bytes = ( + scalars["deferred-wal-attempted-bytes"] + - scalars["deferred-wal-success-bytes"] + - scalars["deferred-wal-invalidated-bytes"] + - scalars["deferred-wal-error-bytes"] + ) + require_canonical_wal_sum( + scalars["deferred-wal-busy"], + deferred_wal_busy_bytes, + "deferred WAL busy outcomes", + ) + require( + deferred_wal_busy_bytes >= scalars["deferred-wal-busy-dropped-bytes"], + "deferred WAL busy-dropped bytes exceed all busy outcome bytes", + ) + require_canonical_wal_sum( + scalars["deferred-wal-busy-requeued"], + deferred_wal_busy_bytes - scalars["deferred-wal-busy-dropped-bytes"], + "deferred WAL busy-requeued outcomes", + ) + require( + wal_advice_errors + == current_wal_errors + + scalars["deferred-wal-errors"] + + scalars["deferred-wal-pin-errors"], + "class-6 advice errors differ from WAL action errors", + ) + require( + scalars["wal-dirty-veto-bypasses"] + <= scalars["deferred-wal-successes"] + and scalars["wal-dirty-veto-bypass-bytes"] + <= scalars["deferred-wal-success-bytes"], + "WAL dirty-veto bypasses exceed successful descriptor-ledger actions", + ) + + require( + scalars["deferred-wal-high-entries"] <= CONFIG["deferred-wal-max-entries"] + and scalars["deferred-wal-high-entries"] <= CONFIG["deferred-wal-max-fds"] + and scalars["deferred-wal-high-bytes"] <= CONFIG["deferred-wal-max-bytes"], + "deferred WAL high-water mark exceeds configured capacity", + ) + require( + (scalars["deferred-wal-enqueued"] == 0) + == (scalars["deferred-wal-high-entries"] == 0) + and scalars["deferred-wal-high-entries"] + <= scalars["deferred-wal-enqueued"] + and scalars["deferred-wal-high-bytes"] + <= scalars["deferred-wal-enqueued-bytes"], + "deferred WAL high-water mark does not reconcile with enqueues", + ) + require( + scalars["deferred-wal-queued-entries"] + + scalars["deferred-wal-inflight-entries"] + <= scalars["deferred-wal-high-entries"] + and scalars["deferred-wal-queued-bytes"] + + scalars["deferred-wal-inflight-bytes"] + <= scalars["deferred-wal-high-bytes"], + "deferred WAL occupancy exceeds its high-water mark", + ) + require( + scalars["deferred-wal-high-bytes"] + >= scalars["deferred-wal-high-entries"] * resolved_page_bytes + and scalars["deferred-wal-high-bytes"] + <= scalars["deferred-wal-high-entries"] * CONFIG["wal-emergency-max-bytes"], + "deferred WAL high-water bytes cannot represent its entries", + ) + require( + scalars["deferred-wal-enqueued"] + + scalars["wal-emergency-current-attempts"] + + scalars["wal-emergency-oversize-rejects"] + + scalars["deferred-wal-capacity-rejected"] + + scalars["deferred-wal-sequence-rejected"] + + scalars["deferred-wal-pin-errors"] + + scalars["action-gate-contended-wal-pin-failures"] + <= wal_finite_offers + and scalars["deferred-wal-capacity-rejected"] + + scalars["deferred-wal-sequence-rejected"] + <= total_retained + and scalars["deferred-wal-pin-errors"] <= wal_advice_errors, + "deferred WAL counters exceed their owning offer dispositions", + ) + require( + scalars["wal-emergency-current-attempted-bytes"] + + scalars["deferred-wal-enqueued-bytes"] + + scalars["deferred-wal-capacity-rejected-bytes"] + + scalars["deferred-wal-sequence-rejected-bytes"] + <= classes[5]["offered-finite-bytes"], + "WAL action bytes exceed class-6 finite offered bytes", + ) + + require( + scalars["wal-emergency-attempted-bytes"] + <= scalars["wal-emergency-attempts"] * CONFIG["wal-emergency-max-bytes"] + and scalars["wal-emergency-current-attempted-bytes"] + <= scalars["wal-emergency-current-attempts"] + * CONFIG["wal-emergency-max-bytes"] + and scalars["deferred-wal-attempted-bytes"] + <= scalars["deferred-wal-attempts"] * CONFIG["wal-emergency-max-bytes"], + "WAL attempted bytes exceed whole-segment action bounds", + ) + require( + scalars["wal-emergency-max-actions-per-trigger"] <= 1 + and scalars["wal-emergency-max-bytes-per-trigger"] + <= CONFIG["wal-emergency-max-bytes"] + and scalars["wal-emergency-max-bytes-per-trigger"] + <= scalars["wal-emergency-max-actions-per-trigger"] + * CONFIG["wal-emergency-max-bytes"], + "WAL trigger work exceeds the sealed bound", + ) + require( + scalars["wal-emergency-max-actions-per-trigger"] + <= scalars["wal-emergency-attempts"] + and scalars["wal-emergency-max-bytes-per-trigger"] + <= scalars["wal-emergency-attempted-bytes"], + "WAL trigger high-water mark exceeds aggregate attempted work", + ) + require( + (scalars["wal-emergency-max-actions-per-trigger"] == 0) + == (scalars["deferred-wal-attempts"] == 0) + and scalars["deferred-wal-pressure-samples"] <= scalars["sample-count"] + and scalars["deferred-wal-actionable-samples"] + <= scalars["deferred-wal-pressure-samples"] + and ( + scalars["deferred-wal-actionable-samples"] == 0 + or scalars["sample-count"] >= CONFIG["warmup-samples"] + ), + "WAL trigger high-water action count differs from current/deferred execution", + ) + require( + scalars["wal-emergency-oversize-rejects"] <= wal_finite_offers + and scalars["wal-emergency-current-attempts"] <= wal_finite_offers, + "WAL action counters exceed class-6 offers", + ) + + require( + scalars["deferred-wal-revoke-errors"] + <= scalars["deferred-wal-revoke-calls"] + and scalars["deferred-wal-revoke-calls"] <= ((1 << 64) - 1) // 2 + and scalars["deferred-wal-revoke-sequence"] + == 2 * scalars["deferred-wal-revoke-calls"] + and scalars["deferred-wal-revoked"] <= scalars["deferred-wal-enqueued"], + "synchronous deferred WAL revoke accounting differs", + ) + + require( + scalars["action-gate-contended-calls"] + == scalars["action-gate-contended-retained"] + + scalars["action-gate-contended-enqueued"], + "contended action-gate dispositions do not conserve calls", + ) + require( + scalars["action-gate-contended-calls"] <= total_offers + and scalars["action-gate-contended-retained"] <= total_retained + and scalars["action-gate-contended-enqueued"] + <= scalars["deferred-wal-enqueued"] + and scalars["action-gate-contended-wal-pin-failures"] + <= scalars["action-gate-contended-retained"], + "contended action-gate counters exceed owning dispositions", + ) + require( + scalars["action-gate-probe-max-ns"] + <= scalars["action-gate-probe-total-ns"], + "action-gate maximum probe exceeds total probe time", + ) + require( + scalars["finalization-quiescence-notifications"] <= 1, + "finalization emitted more than one terminal quiescence notification", + ) + + last_sample = telemetry["last-sample"] + return { + "outcome": "adaptive-active", + "reason": "none", + "state": telemetry["state"], + "sample_count": scalars["sample-count"], + "last_sample_present": telemetry["last-sample"] is not None, + "last_sample_monotonic_ns": ( + last_sample["monotonic-ns"] if last_sample is not None else None + ), + "last_sample_effective_limit_bytes": ( + last_sample["effective-limit-bytes"] if last_sample is not None else None + ), + "membership_leaf_identity": ( + f"{last_sample['membership-leaf-device']}:" + f"{last_sample['membership-leaf-inode']}" + if last_sample is not None + else None + ), + "pressure_source_identity": ( + f"{last_sample['pressure-source-device']}:" + f"{last_sample['pressure-source-inode']}" + if last_sample is not None + else None + ), + "valid_offers": total_offers, + "advice_calls": total_advice_calls, + "advised_bytes": total_advised_bytes, + "class6_offers": wal_offers, + "class6_advice_calls": wal_advice_calls, + "class6_advised_bytes": wal_advised_bytes, + "class6_advice_errors": wal_advice_errors, + "sample_errors": scalars["sample-errors"], + "clock_errors": scalars["clock-errors"], + "advice_errors": scalars["advice-errors"], + "deferred_wal_invalidated": scalars["deferred-wal-invalidated"], + "psi_breaker_trips": scalars["psi-breaker-trips"], + "refault_breaker_trips": scalars["refault-breaker-trips"], + "deferred_wal_pin_errors": scalars["deferred-wal-pin-errors"], + "deferred_wal_breaker_flushes": scalars["deferred-wal-breaker-flushes"], + "deferred_wal_revoke_errors": scalars["deferred-wal-revoke-errors"], + "deferred_wal_actionable_samples": scalars[ + "deferred-wal-actionable-samples" + ], + "action_gate_contended_wal_pin_failures": scalars[ + "action-gate-contended-wal-pin-failures" + ], + "wal_dirty_veto_bypasses": scalars["wal-dirty-veto-bypasses"], + "wal_dirty_veto_bypass_bytes": scalars["wal-dirty-veto-bypass-bytes"], + } + + +def validate_fallback(telemetry: dict[str, Any], runtime_abi_id: str) -> dict[str, Any]: + require(set(telemetry) == FALLBACK_FIELDS, "adaptive fallback evidence fields differ") + validate_fixed_identity(telemetry, runtime_abi_id) + expected = { + "schema": FALLBACK_SCHEMA, + "admission": "denied", + "requested-policy-id": POLICY_ID, + "requested-policy-mode": POLICY_MODE, + } + for field, value in expected.items(): + require(telemetry[field] == value, f"adaptive fallback {field} differs") + require(telemetry["reason"] in FALLBACK_REASONS, "adaptive fallback reason is unknown") + return { + "outcome": "observe-only-fallback", + "reason": telemetry["reason"], + "state": "not-constructed", + "sample_count": 0, + "last_sample_present": False, + "last_sample_monotonic_ns": None, + "last_sample_effective_limit_bytes": None, + "membership_leaf_identity": None, + "pressure_source_identity": None, + "valid_offers": 0, + "advice_calls": 0, + "advised_bytes": 0, + "class6_offers": 0, + "class6_advice_calls": 0, + "class6_advised_bytes": 0, + "class6_advice_errors": 0, + "sample_errors": 0, + "clock_errors": 0, + "advice_errors": 0, + "psi_breaker_trips": 0, + "refault_breaker_trips": 0, + "deferred_wal_pin_errors": 0, + "deferred_wal_breaker_flushes": 0, + "deferred_wal_revoke_errors": 0, + "deferred_wal_actionable_samples": 0, + "action_gate_contended_wal_pin_failures": 0, + "wal_dirty_veto_bypasses": 0, + "wal_dirty_veto_bypass_bytes": 0, + } + + +def enforce_acceptance_policy( + result: dict[str, Any], + acceptance_policy: str, + cgroup_identity: str | None, + cgroup_memory_max_bytes: int | None, + cgroup_memory_high_bytes: int | None, + cgroup_swap_max_bytes: int | None, + sample_window_start_ns: int | None, + sample_window_end_ns: int | None, + measurement_id: str | None, + target: str | None, +) -> None: + require( + acceptance_policy in ACCEPTANCE_POLICIES, + f"adaptive acceptance policy is unknown: {acceptance_policy!r}", + ) + if acceptance_policy == PORTABLE_ACCEPTANCE_POLICY: + return + + require( + result["outcome"] == "adaptive-active", + "constrained Linux WAL-action evidence requires adaptive-active admission", + ) + require( + result["class6_offers"] > 0, + "constrained Linux WAL-action evidence requires class 6 offers", + ) + require( + result["class6_advice_calls"] > 0, + "constrained Linux WAL-action evidence requires class 6 advice calls", + ) + require( + result["class6_advised_bytes"] > 0, + "constrained Linux WAL-action evidence requires class 6 advised bytes", + ) + require( + result["last_sample_present"], + "constrained Linux WAL-action evidence requires current admitted pressure evidence", + ) + require( + cgroup_identity is not None + and CGROUP_IDENTITY_RE.fullmatch(cgroup_identity) is not None, + "constrained Linux WAL-action evidence requires an exact cgroup device:inode identity", + ) + require( + cgroup_memory_max_bytes is not None + and cgroup_memory_high_bytes is not None + and cgroup_swap_max_bytes is not None + and cgroup_memory_max_bytes > 0 + and cgroup_memory_high_bytes > 0 + and cgroup_memory_high_bytes <= cgroup_memory_max_bytes + and cgroup_swap_max_bytes >= 0, + "constrained Linux WAL-action evidence requires exact finite leaf cgroup limits", + ) + require( + sample_window_start_ns is not None + and sample_window_end_ns is not None + and 0 <= sample_window_start_ns < sample_window_end_ns <= (1 << 64) - 1, + "constrained Linux WAL-action evidence requires an exact monotonic sample window", + ) + require( + measurement_id is not None + and MEASUREMENT_ID_RE.fullmatch(measurement_id) is not None + and target == "wasix", + "constrained Linux WAL-action evidence requires an exact WASIX measurement identity", + ) + require( + result["membership_leaf_identity"] == cgroup_identity, + "adaptive telemetry membership leaf differs from the measured cgroup identity", + ) + require( + result["pressure_source_identity"] == cgroup_identity, + "adaptive telemetry pressure source differs from the measured cgroup identity", + ) + require( + sample_window_start_ns + <= result["last_sample_monotonic_ns"] + <= sample_window_end_ns, + "adaptive telemetry last sample falls outside the measured target lifetime", + ) + require( + result["last_sample_effective_limit_bytes"] + == min(cgroup_memory_max_bytes, cgroup_memory_high_bytes), + "adaptive telemetry effective limit differs from min(leaf MemoryMax, leaf MemoryHigh)", + ) + require( + result["state"] != "degraded" + and result["psi_breaker_trips"] == 0 + and result["refault_breaker_trips"] == 0, + "constrained Linux WAL-action evidence contains degradation or breaker trips", + ) + require( + result["sample_errors"] == 0 + and result["clock_errors"] == 0 + and result["advice_errors"] == result["deferred_wal_invalidated"] + and result["class6_advice_errors"] == 0 + and result["deferred_wal_pin_errors"] == 0 + and result["deferred_wal_breaker_flushes"] == 0 + and result["deferred_wal_revoke_errors"] == 0 + and result["action_gate_contended_wal_pin_failures"] == 0, + "constrained Linux WAL-action evidence contains telemetry, advice, or WAL pin errors", + ) + require( + result["deferred_wal_actionable_samples"] > 0, + "constrained Linux WAL-action evidence lacks a fresh L2/L3 pressure trigger", + ) + + +def validate( + telemetry_path: Path, + manifest_path: Path, + output: Path, + acceptance_policy: str, + cgroup_identity: str | None, + cgroup_memory_max_bytes: int | None, + cgroup_memory_high_bytes: int | None, + cgroup_swap_max_bytes: int | None, + sample_window_start_ns: int | None, + sample_window_end_ns: int | None, + measurement_id: str | None, + target: str | None, +) -> None: + require(not os.path.lexists(output), f"validation output already exists: {output}") + telemetry_data = read_regular_stable(telemetry_path, "adaptive file-cache telemetry") + manifest_data = read_regular_stable(manifest_path, "sealed manifest") + telemetry = parse_object(telemetry_data, "adaptive file-cache telemetry JSON") + manifest = parse_object(manifest_data, "sealed manifest JSON") + runtime_abi_id = manifest_contract(manifest) + schema = telemetry.get("schema") + if schema == ACTIVE_SCHEMA: + result = validate_active(telemetry, runtime_abi_id) + elif schema == FALLBACK_SCHEMA: + result = validate_fallback(telemetry, runtime_abi_id) + else: + raise ValidationError(f"adaptive telemetry schema is not admitted: {schema!r}") + enforce_acceptance_policy( + result, + acceptance_policy, + cgroup_identity, + cgroup_memory_max_bytes, + cgroup_memory_high_bytes, + cgroup_swap_max_bytes, + sample_window_start_ns, + sample_window_end_ns, + measurement_id, + target, + ) + + def receipt_value(value: Any) -> str: + return "none" if value is None else str(value) + + output.parent.mkdir(parents=True, exist_ok=True) + pending = output.with_name(f".{output.name}.pending.{os.getpid()}") + require(not os.path.lexists(pending), f"validation pending output already exists: {pending}") + with pending.open("x", encoding="utf-8", newline="\n") as sink: + sink.write( + "schema_version\tstatus\toutcome\treason\tworkload_id\tpolicy_id\t" + "config_id\tconfig_sha256\tacceptance_policy\truntime_abi_id\t" + "state\tsample_count\t" + "valid_offers\tadvice_calls\tadvised_bytes\t" + "class6_offers\tclass6_advice_calls\tclass6_advised_bytes\t" + "class6_advice_errors\tsample_errors\tclock_errors\tadvice_errors\t" + "wal_dirty_veto_bypasses\twal_dirty_veto_bypass_bytes\t" + "telemetry_sha256\tmanifest_sha256\tvalidator_sha256\t" + "cgroup_identity\tcgroup_memory_max_bytes\t" + "cgroup_memory_high_bytes\tcgroup_swap_max_bytes\t" + "sample_window_start_monotonic_ns\t" + "sample_window_end_monotonic_ns\t" + "membership_leaf_identity\tpressure_source_identity\t" + "last_sample_monotonic_ns\tlast_sample_effective_limit_bytes\t" + "measurement_id\ttarget\n" + ) + sink.write( + f"{RESULT_SCHEMA}\tpassed\t{result['outcome']}\t{result['reason']}\t" + f"runtime:postgres\t{POLICY_ID}\t{CONFIG_ID}\t{CONFIG_SHA256}\t" + f"{acceptance_policy}\t{runtime_abi_id}\t{result['state']}\t" + f"{result['sample_count']}\t" + f"{result['valid_offers']}\t{result['advice_calls']}\t" + f"{result['advised_bytes']}\t{result['class6_offers']}\t" + f"{result['class6_advice_calls']}\t{result['class6_advised_bytes']}\t" + f"{result['class6_advice_errors']}\t{result['sample_errors']}\t" + f"{result['clock_errors']}\t{result['advice_errors']}\t" + f"{result['wal_dirty_veto_bypasses']}\t" + f"{result['wal_dirty_veto_bypass_bytes']}\t" + f"{hashlib.sha256(telemetry_data).hexdigest()}\t" + f"{hashlib.sha256(manifest_data).hexdigest()}\t" + f"{hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}\t" + f"{receipt_value(cgroup_identity)}\t" + f"{receipt_value(cgroup_memory_max_bytes)}\t" + f"{receipt_value(cgroup_memory_high_bytes)}\t" + f"{receipt_value(cgroup_swap_max_bytes)}\t" + f"{receipt_value(sample_window_start_ns)}\t" + f"{receipt_value(sample_window_end_ns)}\t" + f"{receipt_value(result['membership_leaf_identity'])}\t" + f"{receipt_value(result['pressure_source_identity'])}\t" + f"{receipt_value(result['last_sample_monotonic_ns'])}\t" + f"{receipt_value(result['last_sample_effective_limit_bytes'])}\t" + f"{receipt_value(measurement_id)}\t{receipt_value(target)}\n" + ) + sink.flush() + os.fsync(sink.fileno()) + os.chmod(pending, 0o444) + os.rename(pending, output) + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--telemetry", type=Path, required=True) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument( + "--acceptance-policy", + choices=sorted(ACCEPTANCE_POLICIES), + default=PORTABLE_ACCEPTANCE_POLICY, + help=( + "portable-correctness-v1 accepts exact active or fallback evidence; " + "constrained-linux-wal-action-v1 requires active class-6 cache-drop action" + ), + ) + parser.add_argument("--cgroup-identity") + parser.add_argument("--cgroup-memory-max-bytes", type=int) + parser.add_argument("--cgroup-memory-high-bytes", type=int) + parser.add_argument("--cgroup-swap-max-bytes", type=int) + parser.add_argument("--sample-window-start-monotonic-ns", type=int) + parser.add_argument("--sample-window-end-monotonic-ns", type=int) + parser.add_argument("--measurement-id") + parser.add_argument("--target", choices=("wasix",)) + arguments = parser.parse_args(argv) + try: + validate( + arguments.telemetry, + arguments.manifest, + arguments.output, + arguments.acceptance_policy, + arguments.cgroup_identity, + arguments.cgroup_memory_max_bytes, + arguments.cgroup_memory_high_bytes, + arguments.cgroup_swap_max_bytes, + arguments.sample_window_start_monotonic_ns, + arguments.sample_window_end_monotonic_ns, + arguments.measurement_id, + arguments.target, + ) + return 0 + except (OSError, ValidationError) as error: + print(f"adaptive file-cache telemetry validation failed: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-adaptive-file-cache-telemetry.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-adaptive-file-cache-telemetry.test.py new file mode 100755 index 00000000..d49c9790 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-adaptive-file-cache-telemetry.test.py @@ -0,0 +1,1160 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import copy +import json +from pathlib import Path +import runpy +import subprocess +import sys +import tempfile +import unittest + + +SCRIPT = Path(__file__).with_name("validate-adaptive-file-cache-telemetry.py") +ABI_ID = "12" * 32 +VALIDATOR_CONSTANTS = runpy.run_path(str(SCRIPT)) +CONFIG_SHA256 = VALIDATOR_CONSTANTS["CONFIG_SHA256"] +POLICY_ID = VALIDATOR_CONSTANTS["POLICY_ID"] +POLICY_MODE = VALIDATOR_CONSTANTS["POLICY_MODE"] +CONFIG_ID = VALIDATOR_CONSTANTS["CONFIG_ID"] +FALLBACK_POLICY_ID = VALIDATOR_CONSTANTS["FALLBACK_POLICY_ID"] +FALLBACK_POLICY_MODE = VALIDATOR_CONSTANTS["FALLBACK_POLICY_MODE"] +ACTIVE_SCHEMA = VALIDATOR_CONSTANTS["ACTIVE_SCHEMA"] +RESULT_SCHEMA = VALIDATOR_CONSTANTS["RESULT_SCHEMA"] +PORTABLE_POLICY = VALIDATOR_CONSTANTS["PORTABLE_ACCEPTANCE_POLICY"] +CONSTRAINED_POLICY = VALIDATOR_CONSTANTS["CONSTRAINED_ACCEPTANCE_POLICY"] +DEFAULT_SAMPLE_CONTRACT = { + "cgroup-identity": "31:41", + "cgroup-memory-max-bytes": 256 * 1024 * 1024, + "cgroup-memory-high-bytes": 224 * 1024 * 1024, + "cgroup-swap-max-bytes": 0, + "sample-window-start-monotonic-ns": 9_000_000_000, + "sample-window-end-monotonic-ns": 11_000_000_000, + "measurement-id": "sample-b01-p2-wasix", + "target": "wasix", +} + + +def config() -> dict: + return { + "page-alignment": "host-page", + "sample-interval-ns": 250_000_000, + "warmup-samples": 3, + "enter-level1-per-mille": 780, + "exit-level1-per-mille": 720, + "enter-level2-per-mille": 850, + "exit-level2-per-mille": 800, + "enter-level3-per-mille": 920, + "exit-level3-per-mille": 870, + "emergency-headroom-bytes": 24 * 1024 * 1024, + "cooldown-ns": 2_000_000_000, + "circuit-breaker-cooldown-ns": 5_000_000_000, + "healthy-samples-to-recover": 3, + "max-dirty-bytes": 16 * 1024 * 1024, + "max-dirty-per-mille": 80, + "immediate-wal-cache-drop-safe": False, + "relation-pressure-relief": False, + "allow-wal-cache-drop-safe-dirty-bypass": True, + "wal-emergency-max-bytes": 16 * 1024 * 1024, + "deferred-wal-max-entries": 4, + "deferred-wal-max-bytes": 64 * 1024 * 1024, + "deferred-wal-max-fds": 4, + "deferred-wal-ttl-ns": 4_000_000_000, + "deferred-wal-drain-per-trigger": 1, + "deferred-wal-busy-retries": 0, + "bytes-per-second": 32 * 1024 * 1024, + "burst-bytes": 32 * 1024 * 1024, + "max-bytes-per-offer": 16 * 1024 * 1024, + "min-bytes-per-offer": 4096, + "psi-some-breaker-per-mille": 250, + "psi-full-breaker-per-mille": 100, + "refault-min-pages": 256, + "refault-breaker-per-mille": 500, + } + + +def manifest() -> dict: + return { + "runtime-abi-id": ABI_ID, + "file-cache-policy": { + "requested-policy-id": POLICY_ID, + "approved-config-id": CONFIG_ID, + "config-sha256": CONFIG_SHA256, + "portable-fallback-mode": "observe-only", + }, + } + + +def active() -> dict: + names = ( + "relation-read-normal", + "relation-read-bulk", + "relation-read-vacuum", + "relation-sync-checkpoint", + "relation-sync-immediate", + "wal-inactive-durable", + ) + classes = [ + { + "class": index, + "name": name, + "offers": 0, + "offered-finite-bytes": 0, + "through-eof-offers": 0, + "advice-calls": 0, + "advised-bytes": 0, + "partial-advice-calls": 0, + "advice-errors": 0, + } + for index, name in enumerate(names, 1) + ] + classes[0].update({"offers": 1, "offered-finite-bytes": 8192}) + classes[1].update( + { + "offers": 1, + "offered-finite-bytes": 8192, + } + ) + retain_names = ( + "unsupported-class", + "through-eof", + "non-host-backed", + "pressure-state", + "dirty-veto", + "empty-after-inward-alignment", + "rate-limited", + "sampler-unavailable", + "circuit-breaker", + "wal-cache-drop-proof-required", + "wal-whole-segment-required", + "workload-finalized", + ) + retain = [{"reason": name, "calls": 0} for name in retain_names] + retain[0]["calls"] = 1 + retain[3]["calls"] = 1 + value = {field: 0 for field in VALIDATOR_CONSTANTS["ACTIVE_FIELDS"]} + value.update({ + "schema": ACTIVE_SCHEMA, + "policy-id": POLICY_ID, + "policy-mode": POLICY_MODE, + "workload-id": "runtime:postgres", + "runtime-abi-id": ABI_ID, + "fallback-policy-id": FALLBACK_POLICY_ID, + "fallback-policy-mode": FALLBACK_POLICY_MODE, + "config": config(), + "resolved-page-bytes": 4096, + "state": "warmup", + "sample-count": 0, + "state-transitions": 0, + "sample-errors": 0, + "clock-errors": 0, + "psi-breaker-trips": 0, + "refault-breaker-trips": 0, + "dirty-vetoes": 0, + "wal-dirty-veto-bypasses": 0, + "wal-dirty-veto-bypass-bytes": 0, + "range-offered-bytes": 16384, + "range-aligned-bytes": 0, + "range-advised-bytes": 0, + "token-bytes": 32 * 1024 * 1024, + "state-deadline-ns": 0, + "advised-bytes-since-sample": 0, + "advice-errors": 0, + "last-advice-raw-os-error": None, + "max-current-bytes": 0, + "max-used-per-mille": 0, + "max-file-context-bytes": 0, + "max-file-dirty-bytes": 0, + "last-psi-some-delta-us": 0, + "last-psi-full-delta-us": 0, + "last-psi-some-per-mille": 0, + "last-psi-full-per-mille": 0, + "psi-no-advice-baseline-some-per-mille": None, + "psi-no-advice-baseline-full-per-mille": None, + "last-refault-delta": 0, + "last-local-high-event-delta": 0, + "last-local-max-event-delta": 0, + "last-local-oom-event-delta": 0, + "workload-finalized": True, + "deferred-wal-maintenance-constructed": True, + "deferred-wal-maintenance-active": False, + "deferred-wal-conservation-entries-ok": True, + "deferred-wal-conservation-bytes-ok": True, + "last-sample": None, + "classes": classes, + "retain-reasons": retain, + "validation": [2, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }) + return value + + +def fallback() -> dict: + return { + "schema": "oliphaunt.wasix-postmaster.file-cache-admission-fallback.v1", + "admission": "denied", + "requested-policy-id": POLICY_ID, + "requested-policy-mode": POLICY_MODE, + "workload-id": "runtime:postgres", + "runtime-abi-id": ABI_ID, + "fallback-policy-id": FALLBACK_POLICY_ID, + "fallback-policy-mode": FALLBACK_POLICY_MODE, + "reason": "unsupported", + "config": config(), + } + + +def active_wal_action() -> dict: + value = active() + wal_bytes = 1024 * 1024 + value["classes"][1].update( + { + "offers": 0, + "offered-finite-bytes": 0, + "advice-calls": 0, + "advised-bytes": 0, + } + ) + value["classes"][5].update( + { + "offers": 1, + "offered-finite-bytes": wal_bytes, + "advice-calls": 1, + "advised-bytes": wal_bytes, + } + ) + value["retain-reasons"][3]["calls"] = 0 + value.update( + { + "state": "relief-level3", + "sample-count": 3, + "max-current-bytes": 245 * 1024 * 1024, + "max-used-per-mille": 1000, + "max-file-context-bytes": 180 * 1024 * 1024, + "max-file-dirty-bytes": 1024 * 1024, + "last-sample": { + "monotonic-ns": 10_000_000_000, + "current-bytes": 245 * 1024 * 1024, + "effective-limit-bytes": 224 * 1024 * 1024, + "file-context-bytes": 180 * 1024 * 1024, + "file-dirty-bytes": 1024 * 1024, + "workingset-refault-file": 40, + "psi-some-total-us": 100, + "psi-full-total-us": 0, + "local-high-events": 1, + "local-max-events": 0, + "local-oom-events": 0, + "local-events-available": True, + "membership-leaf-device": 31, + "membership-leaf-inode": 41, + "pressure-source-device": 31, + "pressure-source-inode": 41, + "pressure-source-depth": 1, + }, + } + ) + value["range-offered-bytes"] = 8192 + wal_bytes + value["range-aligned-bytes"] = wal_bytes + value["range-advised-bytes"] = wal_bytes + value["advised-bytes-since-sample"] = wal_bytes + value["token-bytes"] = config()["burst-bytes"] + value.update( + { + "deferred-wal-high-entries": 1, + "deferred-wal-high-bytes": wal_bytes, + "deferred-wal-enqueued": 1, + "deferred-wal-enqueued-bytes": wal_bytes, + "deferred-wal-attempts": 1, + "deferred-wal-attempted-bytes": wal_bytes, + "deferred-wal-successes": 1, + "deferred-wal-success-bytes": wal_bytes, + "deferred-wal-pressure-samples": 1, + "deferred-wal-actionable-samples": 1, + "deferred-wal-terminal-entries": 1, + "deferred-wal-terminal-bytes": wal_bytes, + "wal-emergency-attempts": 1, + "wal-emergency-attempted-bytes": wal_bytes, + "wal-emergency-successes": 1, + "wal-emergency-success-bytes": wal_bytes, + "wal-emergency-max-actions-per-trigger": 1, + "wal-emergency-max-bytes-per-trigger": wal_bytes, + } + ) + return value + + +def active_deferred_wal_success() -> dict: + value = active_wal_action() + wal_bytes = 1024 * 1024 + value["classes"][5].update( + { + "offers": 2, + "offered-finite-bytes": 2 * wal_bytes, + "advice-calls": 2, + "advised-bytes": 2 * wal_bytes, + } + ) + value["validation"][0] = 3 + value["range-offered-bytes"] = 8192 + 2 * wal_bytes + value["range-aligned-bytes"] = 2 * wal_bytes + value["range-advised-bytes"] = 2 * wal_bytes + value["advised-bytes-since-sample"] = 2 * wal_bytes + value.update( + { + "deferred-wal-high-entries": 2, + "deferred-wal-high-bytes": 2 * wal_bytes, + "deferred-wal-enqueued": 2, + "deferred-wal-enqueued-bytes": 2 * wal_bytes, + "deferred-wal-attempts": 2, + "deferred-wal-attempted-bytes": 2 * wal_bytes, + "deferred-wal-successes": 2, + "deferred-wal-success-bytes": 2 * wal_bytes, + "deferred-wal-pressure-samples": 2, + "deferred-wal-actionable-samples": 2, + "deferred-wal-terminal-entries": 2, + "deferred-wal-terminal-bytes": 2 * wal_bytes, + "wal-emergency-attempts": 2, + "wal-emergency-attempted-bytes": 2 * wal_bytes, + "wal-emergency-successes": 2, + "wal-emergency-success-bytes": 2 * wal_bytes, + "wal-emergency-max-actions-per-trigger": 1, + "wal-emergency-max-bytes-per-trigger": wal_bytes, + } + ) + return value + + +def active_deferred_wal_finalization_flush() -> dict: + value = active_deferred_wal_success() + wal_bytes = 1024 * 1024 + value["classes"][5].update( + { + "advice-calls": 1, + "advised-bytes": wal_bytes, + } + ) + value["range-advised-bytes"] = wal_bytes + value["advised-bytes-since-sample"] = wal_bytes + value.update( + { + "deferred-wal-attempts": 1, + "deferred-wal-attempted-bytes": wal_bytes, + "deferred-wal-successes": 1, + "deferred-wal-success-bytes": wal_bytes, + "deferred-wal-flushes": 1, + "deferred-wal-flushed-entries": 1, + "deferred-wal-flushed-bytes": wal_bytes, + "deferred-wal-finalization-flushes": 1, + "deferred-wal-pressure-samples": 1, + "deferred-wal-actionable-samples": 1, + "deferred-wal-terminal-entries": 2, + "deferred-wal-terminal-bytes": 2 * wal_bytes, + "wal-emergency-attempts": 1, + "wal-emergency-attempted-bytes": wal_bytes, + "wal-emergency-successes": 1, + "wal-emergency-success-bytes": wal_bytes, + "wal-emergency-max-actions-per-trigger": 1, + "wal-emergency-max-bytes-per-trigger": wal_bytes, + } + ) + return value + + +def active_deferred_wal_revoke() -> dict: + value = active() + wal_bytes = 1024 * 1024 + value["classes"][1].update( + { + "offers": 0, + "offered-finite-bytes": 0, + } + ) + value["classes"][5].update( + { + "offers": 1, + "offered-finite-bytes": wal_bytes, + } + ) + value["retain-reasons"][3]["calls"] = 0 + value["validation"][0] = 2 + value["range-offered-bytes"] = 8192 + wal_bytes + value["range-aligned-bytes"] = wal_bytes + value.update( + { + "deferred-wal-high-entries": 1, + "deferred-wal-high-bytes": wal_bytes, + "deferred-wal-enqueued": 1, + "deferred-wal-enqueued-bytes": wal_bytes, + "deferred-wal-revoked": 1, + "deferred-wal-revoked-bytes": wal_bytes, + "deferred-wal-revoke-calls": 1, + "deferred-wal-revoke-sequence": 2, + "deferred-wal-terminal-entries": 1, + "deferred-wal-terminal-bytes": wal_bytes, + } + ) + return value + + +def active_deferred_wal_invalidated() -> dict: + value = active_wal_action() + wal_bytes = 1024 * 1024 + value["classes"][5]["advice-calls"] = 0 + value["classes"][5]["advised-bytes"] = 0 + value["range-advised-bytes"] = 0 + value["advised-bytes-since-sample"] = 0 + value["advice-errors"] = 1 + value["deferred-wal-successes"] = 0 + value["deferred-wal-success-bytes"] = 0 + value["deferred-wal-invalidated"] = 1 + value["deferred-wal-invalidated-bytes"] = wal_bytes + value["wal-emergency-successes"] = 0 + value["wal-emergency-success-bytes"] = 0 + return value + + +def active_deferred_wal_success_with_invalidation() -> dict: + value = active_deferred_wal_success() + wal_bytes = 1024 * 1024 + value["classes"][5]["offers"] = 3 + value["classes"][5]["offered-finite-bytes"] = 3 * wal_bytes + value["validation"][0] = 4 + value["range-offered-bytes"] = 8192 + 3 * wal_bytes + value["range-aligned-bytes"] = 3 * wal_bytes + value["advice-errors"] = 1 + value.update( + { + "deferred-wal-high-entries": 3, + "deferred-wal-high-bytes": 3 * wal_bytes, + "deferred-wal-enqueued": 3, + "deferred-wal-enqueued-bytes": 3 * wal_bytes, + "deferred-wal-attempts": 3, + "deferred-wal-attempted-bytes": 3 * wal_bytes, + "deferred-wal-invalidated": 1, + "deferred-wal-invalidated-bytes": wal_bytes, + "deferred-wal-pressure-samples": 3, + "deferred-wal-actionable-samples": 3, + "deferred-wal-terminal-entries": 3, + "deferred-wal-terminal-bytes": 3 * wal_bytes, + "wal-emergency-attempts": 3, + "wal-emergency-attempted-bytes": 3 * wal_bytes, + } + ) + return value + + +class ValidateAdaptiveFileCacheTelemetryTests(unittest.TestCase): + def run_validator( + self, + value: dict, + *, + manifest_value: dict | None = None, + raw: str | None = None, + acceptance_policy: str | None = None, + sample_contract: dict[str, str | int] | None = None, + ): + temporary = tempfile.TemporaryDirectory() + root = Path(temporary.name) + snapshot = root / "adaptive.json" + manifest_path = root / "manifest.json" + output = root / "validation.tsv" + snapshot.write_text(raw if raw is not None else json.dumps(value) + "\n", encoding="utf-8") + manifest_path.write_text(json.dumps(manifest_value or manifest()) + "\n", encoding="utf-8") + command = [ + sys.executable, + str(SCRIPT), + "--telemetry", + str(snapshot), + "--manifest", + str(manifest_path), + "--output", + str(output), + ] + if acceptance_policy is not None: + command.extend(("--acceptance-policy", acceptance_policy)) + if acceptance_policy == CONSTRAINED_POLICY: + contract = DEFAULT_SAMPLE_CONTRACT | (sample_contract or {}) + for option, option_value in contract.items(): + command.extend((f"--{option}", str(option_value))) + result = subprocess.run( + command, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + contents = output.read_text(encoding="utf-8") if output.exists() else "" + temporary.cleanup() + return result, contents + + def test_accepts_exact_active_snapshot(self): + result, output = self.run_validator(active()) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("\tadaptive-active\tnone\truntime:postgres\t", output) + self.assertIn(f"\t{PORTABLE_POLICY}\t{ABI_ID}\twarmup\t", output) + + def test_accepts_each_exact_fallback_reason(self): + for reason in ("unsupported", "invalid-evidence", "unavailable-io"): + with self.subTest(reason=reason): + value = fallback() + value["reason"] = reason + result, output = self.run_validator(value) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn(f"\tobserve-only-fallback\t{reason}\t", output) + + def test_constrained_policy_accepts_exact_active_wal_action(self): + result, output = self.run_validator( + active_wal_action(), acceptance_policy=CONSTRAINED_POLICY + ) + self.assertEqual(result.returncode, 0, result.stderr) + header, row = output.splitlines() + fields = dict(zip(header.split("\t"), row.split("\t"), strict=True)) + self.assertEqual(fields["schema_version"], RESULT_SCHEMA) + self.assertEqual(fields["acceptance_policy"], CONSTRAINED_POLICY) + self.assertEqual(fields["outcome"], "adaptive-active") + self.assertEqual(fields["class6_offers"], "1") + self.assertEqual(fields["class6_advice_calls"], "1") + self.assertEqual(fields["class6_advised_bytes"], str(1024 * 1024)) + self.assertEqual(fields["class6_advice_errors"], "0") + self.assertEqual(fields["cgroup_identity"], "31:41") + self.assertEqual(fields["membership_leaf_identity"], "31:41") + self.assertEqual(fields["pressure_source_identity"], "31:41") + self.assertEqual(fields["last_sample_monotonic_ns"], "10000000000") + self.assertEqual(fields["measurement_id"], "sample-b01-p2-wasix") + self.assertEqual(fields["target"], "wasix") + self.assertEqual( + fields["last_sample_effective_limit_bytes"], str(224 * 1024 * 1024) + ) + + def test_constrained_policy_rejects_fallback_or_zero_wal_action(self): + result, output = self.run_validator( + fallback(), acceptance_policy=CONSTRAINED_POLICY + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("requires adaptive-active admission", result.stderr) + + result, output = self.run_validator( + active(), acceptance_policy=CONSTRAINED_POLICY + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("requires class 6 offers", result.stderr) + + value = active_wal_action() + value["classes"][5]["advice-calls"] = 0 + value["classes"][5]["advised-bytes"] = 0 + value["range-advised-bytes"] = 0 + value["advised-bytes-since-sample"] = 0 + value["retain-reasons"][0]["calls"] += 1 + for field in ( + "deferred-wal-high-entries", + "deferred-wal-high-bytes", + "deferred-wal-enqueued", + "deferred-wal-enqueued-bytes", + "deferred-wal-attempts", + "deferred-wal-attempted-bytes", + "deferred-wal-successes", + "deferred-wal-success-bytes", + "deferred-wal-pressure-samples", + "deferred-wal-actionable-samples", + "deferred-wal-terminal-entries", + "deferred-wal-terminal-bytes", + "wal-emergency-attempts", + "wal-emergency-attempted-bytes", + "wal-emergency-successes", + "wal-emergency-success-bytes", + "wal-emergency-current-attempts", + "wal-emergency-current-attempted-bytes", + "wal-emergency-current-successes", + "wal-emergency-current-success-bytes", + "wal-emergency-max-actions-per-trigger", + "wal-emergency-max-bytes-per-trigger", + ): + value[field] = 0 + result, output = self.run_validator( + value, acceptance_policy=CONSTRAINED_POLICY + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("requires class 6 advice calls", result.stderr) + + value = active_wal_action() + value["classes"][5]["advised-bytes"] = 0 + value["range-advised-bytes"] = 0 + value["advised-bytes-since-sample"] = 0 + result, output = self.run_validator( + value, acceptance_policy=CONSTRAINED_POLICY + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("advice call/byte presence differs", result.stderr) + + def test_constrained_policy_rejects_sampler_or_clock_errors(self): + for field in ("sample-errors", "clock-errors"): + with self.subTest(field=field): + value = active_wal_action() + value[field] = 1 + result, output = self.run_validator( + value, acceptance_policy=CONSTRAINED_POLICY + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("contains telemetry, advice, or WAL pin errors", result.stderr) + + def test_constrained_policy_requires_current_pressure_sample_not_completed_warmup(self): + value = active_wal_action() + value["sample-count"] = 0 + value["last-sample"] = None + value["state"] = "warmup" + result, output = self.run_validator( + value, acceptance_policy=CONSTRAINED_POLICY + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("trigger high-water action count differs", result.stderr) + + value = active_wal_action() + value["sample-count"] = config()["warmup-samples"] - 1 + value["state"] = "warmup" + result, output = self.run_validator( + value, acceptance_policy=CONSTRAINED_POLICY + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("trigger high-water action count differs", result.stderr) + + value = active_wal_action() + value["sample-count"] = 0 + value["last-sample"] = None + value["state"] = "warmup" + result, output = self.run_validator( + value, acceptance_policy=PORTABLE_POLICY + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("trigger high-water action count differs", result.stderr) + + def test_constrained_policy_rejects_cgroup_identity_time_or_limit_mismatch(self): + cases = ( + ( + {"cgroup-identity": "31:42"}, + "membership leaf differs from the measured cgroup identity", + ), + ( + {"sample-window-start-monotonic-ns": 10_000_000_001}, + "last sample falls outside the measured target lifetime", + ), + ( + {"cgroup-memory-high-bytes": 223 * 1024 * 1024}, + "effective limit differs from min(leaf MemoryMax, leaf MemoryHigh)", + ), + ) + for contract, message in cases: + with self.subTest(contract=contract): + result, output = self.run_validator( + active_wal_action(), + acceptance_policy=CONSTRAINED_POLICY, + sample_contract=contract, + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn(message, result.stderr) + + value = active_wal_action() + value["last-sample"]["effective-limit-bytes"] = 256 * 1024 * 1024 + result, output = self.run_validator( + value, acceptance_policy=CONSTRAINED_POLICY + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn( + "effective limit differs from min(leaf MemoryMax, leaf MemoryHigh)", + result.stderr, + ) + + def test_rejects_unknown_acceptance_policy(self): + result, output = self.run_validator( + active_wal_action(), acceptance_policy="almost-constrained" + ) + self.assertEqual(result.returncode, 2) + self.assertFalse(output) + self.assertIn("invalid choice", result.stderr) + + def test_rejects_unknown_or_duplicate_fields(self): + value = fallback() + value["activation"] = "environment" + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("fields differ", result.stderr) + raw = json.dumps(fallback())[:-1] + ',"schema":"duplicate"}\n' + result, _ = self.run_validator(fallback(), raw=raw) + self.assertNotEqual(result.returncode, 0) + self.assertIn("duplicate JSON field", result.stderr) + + def test_rejects_manifest_policy_or_runtime_abi_drift(self): + bad_manifest = manifest() + bad_manifest["file-cache-policy"]["config-sha256"] = "34" * 32 + result, _ = self.run_validator(active(), manifest_value=bad_manifest) + self.assertNotEqual(result.returncode, 0) + self.assertIn("file-cache-policy differs", result.stderr) + value = active() + value["runtime-abi-id"] = "56" * 32 + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("runtime-abi-id differs", result.stderr) + + def test_rejects_tuning_or_accounting_drift(self): + value = active() + value["config"]["warmup-samples"] = 4 + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("config warmup-samples differs", result.stderr) + value = active() + value["validation"][0] = 3 + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("class offers differ", result.stderr) + + def test_rejects_json_type_aliases_in_exact_config(self): + value = fallback() + value["config"]["allow-wal-cache-drop-safe-dirty-bypass"] = 1 + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("allow-wal-cache-drop-safe-dirty-bypass differs", result.stderr) + value = fallback() + value["config"]["page-alignment"] = 1 + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("page-alignment differs", result.stderr) + + value = fallback() + value["config"]["immediate-wal-cache-drop-safe"] = True + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("immediate-wal-cache-drop-safe differs", result.stderr) + + value = fallback() + value["config"]["relation-pressure-relief"] = True + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("relation-pressure-relief differs", result.stderr) + + def test_rejects_relation_advice_forged_into_observe_only_embedded_profile(self): + value = active() + value["classes"][1]["advice-calls"] = 1 + value["classes"][1]["advised-bytes"] = 8192 + value["retain-reasons"][3]["calls"] = 0 + value["range-aligned-bytes"] = 8192 + value["range-advised-bytes"] = 8192 + value["advised-bytes-since-sample"] = 8192 + value["token-bytes"] -= 8192 + result, output = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("non-acting class 2 reported advice activity", result.stderr) + + def test_portable_accepts_structurally_conserved_advice_errors_but_constrained_rejects(self): + value = active_wal_action() + value["classes"][5]["advice-calls"] = 0 + value["classes"][5]["advised-bytes"] = 0 + value["classes"][5]["advice-errors"] = 1 + value["range-advised-bytes"] = 0 + value["advised-bytes-since-sample"] = 0 + value["advice-errors"] = 1 + value["last-advice-raw-os-error"] = 22 + value["state"] = "degraded" + value["wal-emergency-successes"] = 0 + value["wal-emergency-success-bytes"] = 0 + value["deferred-wal-successes"] = 0 + value["deferred-wal-success-bytes"] = 0 + value["deferred-wal-errors"] = 1 + value["deferred-wal-error-bytes"] = 1024 * 1024 + result, output = self.run_validator(value) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("\tadaptive-active\t", output) + + strict = value + result, output = self.run_validator( + strict, acceptance_policy=CONSTRAINED_POLICY + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("requires class 6 advice calls", result.stderr) + + def test_active_receipts_are_terminal_under_both_acceptance_policies(self): + cases = ( + ("workload-finalized", False, "not workload-finalized"), + ( + "deferred-wal-maintenance-constructed", + False, + "did not construct bounded WAL maintenance", + ), + ( + "deferred-wal-maintenance-active", + True, + "retained live WAL maintenance", + ), + ("deferred-wal-queued-entries", 1, "nonzero deferred-wal-queued-entries"), + ("deferred-wal-inflight-bytes", 1024 * 1024, "nonzero deferred-wal-inflight-bytes"), + ("deferred-wal-oldest-age-ns", 1, "nonzero deferred-wal-oldest-age-ns"), + ("deferred-wal-open-fds", 1, "nonzero deferred-wal-open-fds"), + ( + "deferred-wal-mutation-epoch-identities", + 1, + "nonzero deferred-wal-mutation-epoch-identities", + ), + ( + "deferred-wal-conservation-entries-ok", + False, + "failed deferred WAL conservation", + ), + ) + for policy in (PORTABLE_POLICY, CONSTRAINED_POLICY): + for field, replacement, message in cases: + with self.subTest(policy=policy, field=field): + value = active_wal_action() + value[field] = replacement + result, output = self.run_validator(value, acceptance_policy=policy) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn(message, result.stderr) + + def test_accepts_exact_deferred_success_and_finalization_flush_accounting(self): + for value in ( + active_deferred_wal_success(), + active_deferred_wal_finalization_flush(), + active_deferred_wal_revoke(), + active_deferred_wal_invalidated(), + ): + with self.subTest(flushes=value["deferred-wal-flushes"]): + result, output = self.run_validator(value) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("\tadaptive-active\t", output) + + def test_constrained_accepts_safe_generation_cancellation_after_real_action(self): + result, output = self.run_validator( + active_deferred_wal_success_with_invalidation(), + acceptance_policy=CONSTRAINED_POLICY, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("\tadaptive-active\t", output) + + def test_rejects_revoke_sequence_or_terminal_conservation_drift(self): + cases = [] + + value = active_deferred_wal_revoke() + value["deferred-wal-revoke-sequence"] = 1 + cases.append((value, "synchronous deferred WAL revoke accounting differs")) + + value = active_deferred_wal_revoke() + value["deferred-wal-terminal-bytes"] = 2 * 1024 * 1024 + cases.append((value, "terminal deferred WAL conservation fields differ")) + + value = active_deferred_wal_revoke() + value["deferred-wal-revoke-errors"] = 2 + cases.append((value, "synchronous deferred WAL revoke accounting differs")) + + for value, message in cases: + with self.subTest(message=message): + result, output = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn(message, result.stderr) + + def test_rejects_deferred_wal_conservation_and_bound_drift(self): + cases: list[tuple[dict, str]] = [] + + value = active_deferred_wal_success() + value["deferred-wal-enqueued"] = 3 + value["deferred-wal-enqueued-bytes"] = 3 * 1024 * 1024 + cases.append((value, "entry dispositions do not conserve enqueues")) + + value = active_deferred_wal_success() + value["deferred-wal-high-entries"] = 0 + value["deferred-wal-high-bytes"] = 0 + cases.append((value, "high-water mark does not reconcile with enqueues")) + + value = active_wal_action() + wal_bytes = 1024 * 1024 + value["deferred-wal-sequence-rejected"] = 1 + value["deferred-wal-sequence-rejected-bytes"] = wal_bytes + cases.append((value, "counters exceed their owning offer dispositions")) + + value = active_deferred_wal_success() + value["deferred-wal-attempts"] = 3 + value["deferred-wal-attempted-bytes"] = 3 * 1024 * 1024 + value["wal-emergency-attempts"] = 3 + value["wal-emergency-attempted-bytes"] = 3 * 1024 * 1024 + cases.append((value, "attempt dispositions do not conserve attempts")) + + value = active_deferred_wal_finalization_flush() + value["deferred-wal-finalization-flushes"] = 0 + cases.append((value, "flush reasons do not conserve flushes")) + + value = active_deferred_wal_finalization_flush() + value["deferred-wal-finalization-flushes"] = 0 + value["deferred-wal-advice-error-flushes"] = 1 + cases.append((value, "error flush lacks its owning sampler/clock/advice error")) + + value = active_deferred_wal_success() + value["wal-emergency-max-actions-per-trigger"] = 3 + value["wal-emergency-max-bytes-per-trigger"] = 3 * 1024 * 1024 + cases.append((value, "WAL trigger work exceeds the sealed bound")) + + value = active_deferred_wal_success() + value["action-gate-contended-calls"] = 1 + cases.append((value, "contended action-gate dispositions do not conserve calls")) + + value = active_deferred_wal_success() + value["finalization-quiescence-notifications"] = 2 + cases.append((value, "more than one terminal quiescence notification")) + + for value, message in cases: + with self.subTest(message=message): + result, output = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn(message, result.stderr) + + def test_rejects_deferred_busy_retry(self): + value = active_deferred_wal_success() + wal_bytes = 1024 * 1024 + value.update( + { + "deferred-wal-attempts": 3, + "deferred-wal-attempted-bytes": 3 * wal_bytes, + "deferred-wal-busy": 1, + "deferred-wal-busy-requeued": 1, + "wal-emergency-attempts": 3, + "wal-emergency-attempted-bytes": 3 * wal_bytes, + "deferred-wal-pressure-samples": 3, + "deferred-wal-actionable-samples": 3, + } + ) + result, output = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn( + "deferred WAL work exceeds the no-retry, one-fresh-sample contract", + result.stderr, + ) + + def test_rejects_cross_class_byte_shift_into_unsupported_class(self): + value = active_wal_action() + wal_bytes = 1024 * 1024 + forged_bytes = 16 * wal_bytes + value["classes"][0]["offered-finite-bytes"] += 15 * wal_bytes + value["classes"][5]["advised-bytes"] = forged_bytes + value["range-offered-bytes"] = 8192 + forged_bytes + value["range-aligned-bytes"] = forged_bytes + value["range-advised-bytes"] = forged_bytes + value["advised-bytes-since-sample"] = forged_bytes + for field in ( + "wal-emergency-attempted-bytes", + "wal-emergency-success-bytes", + "wal-emergency-current-attempted-bytes", + "wal-emergency-current-success-bytes", + "wal-emergency-max-bytes-per-trigger", + ): + value[field] = forged_bytes + result, output = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn( + "adaptive class 6 advised bytes exceed finite offered bytes", + result.stderr, + ) + + def test_rejects_forbidden_immediate_wal_action(self): + value = active_wal_action() + wal_bytes = 1024 * 1024 + value["wal-emergency-current-attempts"] = 1 + value["wal-emergency-current-attempted-bytes"] = wal_bytes + value["wal-emergency-current-successes"] = 1 + value["wal-emergency-current-success-bytes"] = wal_bytes + value["wal-emergency-attempts"] = 2 + value["wal-emergency-attempted-bytes"] = 2 * wal_bytes + value["wal-emergency-successes"] = 2 + value["wal-emergency-success-bytes"] = 2 * wal_bytes + result, output = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn( + "forbidden immediate WAL action", + result.stderr, + ) + + def test_rejects_two_actions_from_one_fresh_pressure_sample(self): + value = active_deferred_wal_success() + value["deferred-wal-pressure-samples"] = 1 + value["deferred-wal-actionable-samples"] = 1 + result, output = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn( + "no-retry, one-fresh-sample contract", + result.stderr, + ) + + def test_rejects_noncanonical_single_wal_action_even_when_mib_aligned(self): + value = active_wal_action() + forged_bytes = 3 * 1024 * 1024 + value["classes"][5]["offered-finite-bytes"] = forged_bytes + value["classes"][5]["advised-bytes"] = forged_bytes + value["range-offered-bytes"] = 8192 + forged_bytes + value["range-aligned-bytes"] = forged_bytes + value["range-advised-bytes"] = forged_bytes + value["advised-bytes-since-sample"] = forged_bytes + for field in ( + "wal-emergency-attempted-bytes", + "wal-emergency-success-bytes", + "wal-emergency-current-attempted-bytes", + "wal-emergency-current-success-bytes", + "wal-emergency-max-bytes-per-trigger", + ): + value[field] = forged_bytes + result, output = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("not a sum of canonical 1/2/4/8/16 MiB", result.stderr) + + value = active_wal_action() + value["classes"][5]["partial-advice-calls"] = 1 + result, output = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("class-6 WAL action reported partial advice", result.stderr) + + value = active_wal_action() + value["classes"][5]["through-eof-offers"] = 1 + result, output = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("finite offer count/byte presence differs", result.stderr) + + def test_accepts_64k_resolved_host_pages(self): + value = active_wal_action() + value["resolved-page-bytes"] = 64 * 1024 + result, output = self.run_validator(value) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("\tadaptive-active\t", output) + + def test_constrained_rejects_breakers_and_contended_pin_failures(self): + breaker = active_wal_action() + breaker["psi-breaker-trips"] = 1 + portable, _ = self.run_validator(breaker) + self.assertEqual(portable.returncode, 0, portable.stderr) + result, output = self.run_validator( + breaker, acceptance_policy=CONSTRAINED_POLICY + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("degradation or breaker trips", result.stderr) + + pin = active_wal_action() + wal_bytes = 1024 * 1024 + pin["classes"][5]["offers"] += 1 + pin["classes"][5]["offered-finite-bytes"] += wal_bytes + pin["validation"][0] += 1 + pin["range-offered-bytes"] += wal_bytes + pin["range-aligned-bytes"] += wal_bytes + pin["retain-reasons"][3]["calls"] += 1 + pin["action-gate-contended-calls"] = 1 + pin["action-gate-contended-retained"] = 1 + pin["action-gate-contended-wal-pin-failures"] = 1 + portable, _ = self.run_validator(pin) + self.assertEqual(portable.returncode, 0, portable.stderr) + result, output = self.run_validator(pin, acceptance_policy=CONSTRAINED_POLICY) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("WAL pin errors", result.stderr) + + hidden_breaker = active_deferred_wal_finalization_flush() + hidden_breaker["deferred-wal-finalization-flushes"] = 0 + hidden_breaker["deferred-wal-breaker-flushes"] = 1 + portable, _ = self.run_validator(hidden_breaker) + self.assertEqual(portable.returncode, 0, portable.stderr) + result, output = self.run_validator( + hidden_breaker, acceptance_policy=CONSTRAINED_POLICY + ) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn("WAL pin errors", result.stderr) + + def test_portable_accepts_degraded_receipt_with_cleared_last_sample(self): + value = active() + value["sample-errors"] = 1 + value["state"] = "degraded" + value["last-sample"] = None + result, output = self.run_validator(value) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("\tadaptive-active\t", output) + + def test_rejects_retain_reasons_without_runtime_causes(self): + cases = [] + value = active() + value["retain-reasons"][0]["calls"] = 0 + value["retain-reasons"][7]["calls"] = 1 + cases.append((value, "sampler-unavailable retains lack")) + + value = active() + value["retain-reasons"][0]["calls"] = 0 + value["retain-reasons"][8]["calls"] = 1 + cases.append((value, "circuit-breaker retains lack")) + + value = active() + value["dirty-vetoes"] = 1 + cases.append((value, "dirty-veto retains differ")) + + value = active() + value["retain-reasons"][0]["calls"] = 0 + value["retain-reasons"][11]["calls"] = 1 + cases.append((value, "post-finalization offer")) + + for value, message in cases: + with self.subTest(message=message): + result, output = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn(message, result.stderr) + + def test_rejects_impossible_sample_deltas_and_wal_temporal_ownership(self): + cases = [] + + value = active_wal_action() + value["last-psi-full-delta-us"] = 1 + cases.append((value, "delta exceeds its cumulative source counter")) + + value = active_wal_action() + value["last-psi-some-per-mille"] = 1 + cases.append((value, "zero PSI-some delta has a nonzero rate")) + + value = active_wal_action() + value["last-sample"]["local-events-available"] = False + cases.append((value, "unavailable local events have nonzero counters")) + + value = active_wal_action() + value["last-sample"]["file-dirty-bytes"] = 181 * 1024 * 1024 + value["max-file-dirty-bytes"] = 181 * 1024 * 1024 + cases.append((value, "file-dirty bytes exceed file-context bytes")) + + value = active_wal_action() + value["wal-emergency-oversize-rejects"] = 1 + cases.append((value, "counters exceed their owning offer dispositions")) + + value = active_wal_action() + value["wal-emergency-forced-samples"] = 1 + cases.append((value, "forbidden immediate WAL action")) + + for value, message in cases: + with self.subTest(message=message): + result, output = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(output) + self.assertIn(message, result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-memory.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-memory.py new file mode 100755 index 00000000..fdc274f2 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-memory.py @@ -0,0 +1,420 @@ +#!/usr/bin/env python3 +"""Validate standalone WASIX checkpoint/recycle memory evidence.""" + +from __future__ import annotations + +import argparse +import csv +import sys +from pathlib import Path + + +HEADER = [ + "schema_version", + "epoch", + "epoch_origin_monotonic_ns", + "monotonic_ns", + "phase", + "process_count", + "pss_kib", + "pss_anon_kib", + "private_kib", + "pagetables_kib", + "cgroup_current_bytes", + "cgroup_peak_bytes", + "cgroup_swap_bytes", + "cgroup_anon_bytes", + "cgroup_file_bytes", + "cgroup_kernel_bytes", + "cgroup_pagetables_bytes", + "cgroup_file_dirty_bytes", + "cgroup_file_writeback_bytes", + "event_high", + "event_max", + "event_oom", + "event_oom_kill", + "psi_some_total_usec", + "psi_full_total_usec", +] +EPOCH_HEADER = [ + "schema_version", + "epoch", + "epoch_origin_monotonic_ns", + "cgroup_path", + "cgroup_identity", + "memory_max_bytes", + "memory_high_bytes", + "memory_swap_max_bytes", +] + + +class EvidenceError(ValueError): + pass + + +def parser() -> argparse.ArgumentParser: + result = argparse.ArgumentParser() + result.add_argument("--samples", type=Path, required=True) + result.add_argument("--epochs", type=Path, required=True) + result.add_argument("--output", type=Path, required=True) + result.add_argument("--gates-output", type=Path, required=True) + result.add_argument("--max-pss-kib", type=int, default=163840) + result.add_argument("--max-pss-anon-kib", type=int, default=98304) + result.add_argument("--max-pagetables-kib", type=int, default=2048) + result.add_argument("--max-current-bytes", type=int, default=268435456) + result.add_argument("--expected-memory-high-bytes", type=int, default=234881024) + result.add_argument("--expected-memory-swap-max-bytes", type=int, default=0) + result.add_argument("--max-dirty-writeback-bytes", type=int, default=100663296) + result.add_argument("--max-high-events-per-second", type=float, default=250.0) + result.add_argument("--max-restart-gap-seconds", type=float, default=30.0) + result.add_argument("--max-psi-some-fraction", type=float, default=0.015) + result.add_argument("--max-psi-full-fraction", type=float, default=0.010) + result.add_argument("--max-quiescent-anon-growth-kib", type=int, default=8192) + result.add_argument("--max-quiescent-private-growth-kib", type=int, default=8192) + result.add_argument("--max-quiescent-pss-growth-kib", type=int, default=24576) + result.add_argument("--max-last-three-anon-range-kib", type=int, default=4096) + return result + + +def integer(row: dict[str, str], field: str) -> int: + value = row[field] + if not value.isdigit(): + raise EvidenceError(f"{field} is not a nonnegative integer: {value!r}") + return int(value) + + +def write_rows(path: Path, rows: list[list[object]]) -> None: + if path.exists() or path.is_symlink(): + raise EvidenceError(f"refusing to replace output: {path}") + with path.open("x", newline="", encoding="utf-8") as stream: + csv.writer(stream, delimiter="\t", lineterminator="\n").writerows(rows) + + +def main() -> int: + args = parser().parse_args() + try: + if not args.samples.is_file() or args.samples.is_symlink(): + raise EvidenceError("samples must be a regular non-symlink file") + with args.samples.open(newline="", encoding="utf-8") as stream: + reader = csv.DictReader(stream, delimiter="\t") + if reader.fieldnames != HEADER: + raise EvidenceError(f"unexpected sample header: {reader.fieldnames!r}") + raw_rows = list(reader) + if not args.epochs.is_file() or args.epochs.is_symlink(): + raise EvidenceError("epochs must be a regular non-symlink file") + with args.epochs.open(newline="", encoding="utf-8") as stream: + epoch_reader = csv.DictReader(stream, delimiter="\t") + if epoch_reader.fieldnames != EPOCH_HEADER: + raise EvidenceError( + f"unexpected cgroup epoch header: {epoch_reader.fieldnames!r}" + ) + raw_epoch_rows = list(epoch_reader) + if len(raw_rows) < 6: + raise EvidenceError("memory evidence requires at least six samples") + rows: list[dict[str, int | str]] = [] + previous_time = -1 + previous_epoch = 0 + allowed_phases = { + "initial-quiescent", + "steady", + "volume-checkpoint", + "recycle-shutdown", + "recycle-startup", + "second-steady", + "post-recycle-quiescent", + } + cumulative_fields = [ + "event_high", + "event_max", + "event_oom", + "event_oom_kill", + "psi_some_total_usec", + "psi_full_total_usec", + ] + previous_cumulative: dict[str, int] | None = None + epoch_bounds: dict[int, list[dict[str, int | str]]] = {} + epoch_origins: dict[int, int] = {} + for raw in raw_rows: + if raw["schema_version"] != "1": + raise EvidenceError("memory sample has an unknown schema version") + epoch = integer(raw, "epoch") + if epoch < 1 or epoch < previous_epoch or epoch > previous_epoch + 1: + raise EvidenceError("memory sample epochs must be positive and contiguous") + if epoch != previous_epoch: + previous_cumulative = None + previous_epoch = epoch + converted: dict[str, int | str] = {"phase": raw["phase"], "epoch": epoch} + if raw["phase"] not in allowed_phases: + raise EvidenceError(f"unknown memory evidence phase: {raw['phase']!r}") + for field in HEADER: + if field not in ("schema_version", "phase", "epoch"): + converted[field] = integer(raw, field) + now = int(converted["monotonic_ns"]) + origin = int(converted["epoch_origin_monotonic_ns"]) + if origin > now: + raise EvidenceError("epoch origin is later than its memory sample") + if epoch in epoch_origins and epoch_origins[epoch] != origin: + raise EvidenceError("epoch origin changed within one cgroup epoch") + epoch_origins[epoch] = origin + if now <= previous_time: + raise EvidenceError("memory sample timestamps are not strictly increasing") + previous_time = now + if previous_cumulative is not None: + for field in cumulative_fields: + if int(converted[field]) < previous_cumulative[field]: + raise EvidenceError(f"cumulative counter moved backwards: {field}") + previous_cumulative = { + field: int(converted[field]) for field in cumulative_fields + } + if ( + int(converted["pss_anon_kib"]) > int(converted["pss_kib"]) + or int(converted["private_kib"]) > int(converted["pss_kib"]) + or int(converted["cgroup_peak_bytes"]) + < int(converted["cgroup_current_bytes"]) + ): + raise EvidenceError("memory sample contains inconsistent accounting") + rows.append(converted) + epoch_bounds.setdefault(epoch, []).append(converted) + if int(converted["process_count"]) <= 0: + raise EvidenceError("memory sample contains no cgroup processes") + + expected_phase_runs = { + 1: [ + "initial-quiescent", + "steady", + "volume-checkpoint", + "recycle-shutdown", + ], + 2: [ + "recycle-startup", + "second-steady", + "post-recycle-quiescent", + ], + } + if set(epoch_bounds) != set(expected_phase_runs): + raise EvidenceError("memory evidence must contain exactly epochs 1 and 2") + for epoch, expected in expected_phase_runs.items(): + observed: list[str] = [] + for row in epoch_bounds[epoch]: + phase = str(row["phase"]) + if not observed or observed[-1] != phase: + observed.append(phase) + if observed != expected: + raise EvidenceError( + f"epoch {epoch} phase order is {observed!r}, expected {expected!r}" + ) + if epoch_origins[2] <= int(epoch_bounds[1][-1]["monotonic_ns"]): + raise EvidenceError( + "epoch 2 must originate after the epoch 1 recycle-shutdown boundary" + ) + if len(raw_epoch_rows) != 2: + raise EvidenceError("cgroup epoch evidence must contain exactly two rows") + epoch_receipts: dict[int, dict[str, str]] = {} + for receipt in raw_epoch_rows: + if receipt["schema_version"] != "1": + raise EvidenceError("cgroup epoch receipt has an unknown schema version") + epoch = integer(receipt, "epoch") + if epoch in epoch_receipts or epoch not in (1, 2): + raise EvidenceError("cgroup epoch receipts must uniquely cover epochs 1 and 2") + origin = integer(receipt, "epoch_origin_monotonic_ns") + memory_max = integer(receipt, "memory_max_bytes") + memory_high = integer(receipt, "memory_high_bytes") + swap_max = integer(receipt, "memory_swap_max_bytes") + if origin != epoch_origins[epoch]: + raise EvidenceError("sample and cgroup receipt epoch origins differ") + if not receipt["cgroup_path"].startswith("/sys/fs/cgroup/"): + raise EvidenceError("cgroup receipt path is outside cgroup v2") + if not receipt["cgroup_identity"]: + raise EvidenceError("cgroup receipt identity is empty") + if ( + memory_max != args.max_current_bytes + or memory_high != args.expected_memory_high_bytes + or swap_max != args.expected_memory_swap_max_bytes + ): + raise EvidenceError("cgroup receipt does not match the embedded memory limits") + epoch_receipts[epoch] = receipt + if set(epoch_receipts) != {1, 2}: + raise EvidenceError("cgroup epoch receipts do not cover epochs 1 and 2") + if ( + epoch_receipts[1]["cgroup_path"] == epoch_receipts[2]["cgroup_path"] + or epoch_receipts[1]["cgroup_identity"] + == epoch_receipts[2]["cgroup_identity"] + ): + raise EvidenceError("recycle must use two distinct fresh cgroup scopes") + + initial = [row for row in rows if row["phase"] == "initial-quiescent"] + post = [row for row in rows if row["phase"] == "post-recycle-quiescent"] + steady = [row for row in rows if row["phase"] == "steady"] + volume = [row for row in rows if row["phase"] == "volume-checkpoint"] + second_steady = [row for row in rows if row["phase"] == "second-steady"] + if ( + len(initial) < 2 + or len(post) < 3 + or not steady + or not volume + or not second_steady + or len(epoch_bounds[1]) < 6 + or len(epoch_bounds[2]) < 4 + or int(epoch_bounds[1][-1]["monotonic_ns"]) + - int(epoch_bounds[1][0]["monotonic_ns"]) < 2_000_000_000 + or int(epoch_bounds[2][-1]["monotonic_ns"]) + - int(epoch_bounds[2][0]["monotonic_ns"]) < 2_000_000_000 + ): + raise EvidenceError( + "required phases are initial-quiescent, steady, volume-checkpoint, " + "second-steady, and at least three post-recycle-quiescent samples " + "across two sufficiently sampled epochs" + ) + wall_elapsed_seconds = ( + int(rows[-1]["monotonic_ns"]) - int(rows[0]["monotonic_ns"]) + ) / 1_000_000_000 + epoch_seconds = { + epoch: ( + int(epoch_rows[-1]["monotonic_ns"]) - epoch_origins[epoch] + ) + / 1_000_000_000 + for epoch, epoch_rows in epoch_bounds.items() + } + observed_epoch_seconds = sum(epoch_seconds.values()) + restart_gap_seconds = ( + epoch_origins[2] - int(epoch_bounds[1][-1]["monotonic_ns"]) + ) / 1_000_000_000 + if wall_elapsed_seconds <= 0 or any(value <= 0 for value in epoch_seconds.values()): + raise EvidenceError("memory evidence has no elapsed time") + # Every qualification epoch uses a newly-created cgroup. Its cumulative + # counters therefore begin at zero at epoch_origin_monotonic_ns. Counting + # the absolute final value includes startup pressure that subtracting the + # first process sample would silently omit. + high_delta = sum( + int(epoch_rows[-1]["event_high"]) + for epoch_rows in epoch_bounds.values() + ) + psi_some_delta = sum( + int(epoch_rows[-1]["psi_some_total_usec"]) + for epoch_rows in epoch_bounds.values() + ) + psi_full_delta = sum( + int(epoch_rows[-1]["psi_full_total_usec"]) + for epoch_rows in epoch_bounds.values() + ) + high_rate = high_delta / observed_epoch_seconds + psi_some = psi_some_delta / (observed_epoch_seconds * 1_000_000) + psi_full = psi_full_delta / (observed_epoch_seconds * 1_000_000) + restart_first = epoch_bounds[2][0] + restart_observed_seconds = ( + int(restart_first["monotonic_ns"]) - epoch_origins[2] + ) / 1_000_000_000 + if restart_observed_seconds <= 0: + raise EvidenceError("restart startup boundary has no observed duration") + restart_high_rate = ( + int(restart_first["event_high"]) / restart_observed_seconds + ) + restart_psi_some = int(restart_first["psi_some_total_usec"]) / ( + restart_observed_seconds * 1_000_000 + ) + restart_psi_full = int(restart_first["psi_full_total_usec"]) / ( + restart_observed_seconds * 1_000_000 + ) + first = initial[-1] + final = post[-1] + last_three = post[-3:] + maxima = { + field: max(int(row[field]) for row in rows) + for field in ( + "pss_kib", + "pss_anon_kib", + "pagetables_kib", + "cgroup_current_bytes", + "cgroup_peak_bytes", + "cgroup_swap_bytes", + ) + } + max_dirty_writeback = max( + int(row["cgroup_file_dirty_bytes"]) + + int(row["cgroup_file_writeback_bytes"]) + for row in rows + ) + deltas = { + field: sum(int(epoch_rows[-1][field]) for epoch_rows in epoch_bounds.values()) + for field in ("event_max", "event_oom", "event_oom_kill") + } + gates: list[tuple[str, str, str, bool]] = [] + + def gate(name: str, expected: str, observed: object, passed: bool) -> None: + gates.append((name, expected, str(observed), passed)) + + gate("peak_pss_kib", f"<={args.max_pss_kib}", maxima["pss_kib"], maxima["pss_kib"] <= args.max_pss_kib) + gate("peak_pss_anon_kib", f"<={args.max_pss_anon_kib}", maxima["pss_anon_kib"], maxima["pss_anon_kib"] <= args.max_pss_anon_kib) + gate("peak_pagetables_kib", f"<={args.max_pagetables_kib}", maxima["pagetables_kib"], maxima["pagetables_kib"] <= args.max_pagetables_kib) + gate("peak_cgroup_current_bytes", f"<={args.max_current_bytes}", maxima["cgroup_current_bytes"], maxima["cgroup_current_bytes"] <= args.max_current_bytes) + gate("cgroup_memory_peak_bytes", f"<={args.max_current_bytes}", maxima["cgroup_peak_bytes"], maxima["cgroup_peak_bytes"] <= args.max_current_bytes) + gate("swap_current_bytes", "0", maxima["cgroup_swap_bytes"], maxima["cgroup_swap_bytes"] == 0) + gate("max_events", "0", deltas["event_max"], deltas["event_max"] == 0) + gate("oom_events", "0", deltas["event_oom"], deltas["event_oom"] == 0) + gate("oom_kill_events", "0", deltas["event_oom_kill"], deltas["event_oom_kill"] == 0) + gate("high_events_per_second", f"<={args.max_high_events_per_second}", f"{high_rate:.9f}", high_rate <= args.max_high_events_per_second) + gate("psi_some_fraction", f"<={args.max_psi_some_fraction}", f"{psi_some:.9f}", psi_some <= args.max_psi_some_fraction) + gate("psi_full_fraction", f"<={args.max_psi_full_fraction}", f"{psi_full:.9f}", psi_full <= args.max_psi_full_fraction) + gate("restart_max_events", "0", restart_first["event_max"], int(restart_first["event_max"]) == 0) + gate("restart_oom_events", "0", restart_first["event_oom"], int(restart_first["event_oom"]) == 0) + gate("restart_oom_kill_events", "0", restart_first["event_oom_kill"], int(restart_first["event_oom_kill"]) == 0) + gate("restart_high_events_per_second", f"<={args.max_high_events_per_second}", f"{restart_high_rate:.9f}", restart_high_rate <= args.max_high_events_per_second) + gate("restart_psi_some_fraction", f"<={args.max_psi_some_fraction}", f"{restart_psi_some:.9f}", restart_psi_some <= args.max_psi_some_fraction) + gate("restart_psi_full_fraction", f"<={args.max_psi_full_fraction}", f"{restart_psi_full:.9f}", restart_psi_full <= args.max_psi_full_fraction) + gate("restart_gap_seconds", f"<={args.max_restart_gap_seconds}", f"{restart_gap_seconds:.9f}", restart_gap_seconds <= args.max_restart_gap_seconds) + gate("dirty_plus_writeback_bytes", f"<={args.max_dirty_writeback_bytes}", max_dirty_writeback, max_dirty_writeback <= args.max_dirty_writeback_bytes) + anon_growth = int(final["pss_anon_kib"]) - int(first["pss_anon_kib"]) + private_growth = int(final["private_kib"]) - int(first["private_kib"]) + pss_growth = int(final["pss_kib"]) - int(first["pss_kib"]) + anon_range = max(int(row["pss_anon_kib"]) for row in last_three) - min( + int(row["pss_anon_kib"]) for row in last_three + ) + gate("quiescent_anon_growth_kib", f"<={args.max_quiescent_anon_growth_kib}", anon_growth, anon_growth <= args.max_quiescent_anon_growth_kib) + gate("quiescent_private_growth_kib", f"<={args.max_quiescent_private_growth_kib}", private_growth, private_growth <= args.max_quiescent_private_growth_kib) + gate("quiescent_pss_growth_kib", f"<={args.max_quiescent_pss_growth_kib}", pss_growth, pss_growth <= args.max_quiescent_pss_growth_kib) + gate("last_three_anon_range_kib", f"<={args.max_last_three_anon_range_kib}", anon_range, anon_range <= args.max_last_three_anon_range_kib) + passed = all(item[3] for item in gates) + write_rows( + args.gates_output, + [["gate", "expected", "observed", "status"]] + + [[name, expected, observed, "passed" if ok else "failed"] for name, expected, observed, ok in gates], + ) + write_rows( + args.output, + [[ + "schema_version", "status", "sample_count", + "wall_elapsed_seconds", "observed_epoch_seconds", + "epoch_1_seconds", "epoch_2_seconds", "restart_gap_seconds", + "restart_observed_seconds", + "peak_pss_kib", "peak_pss_anon_kib", "peak_pagetables_kib", + "peak_cgroup_current_bytes", "cgroup_memory_peak_bytes", + "high_events_per_second", + "psi_some_fraction", "psi_full_fraction", + "max_dirty_writeback_bytes", "quiescent_anon_growth_kib", + "quiescent_private_growth_kib", "quiescent_pss_growth_kib", + "last_three_anon_range_kib", "gates", + "epochs", + ], [ + "oliphaunt.wasix-postmaster.checkpoint-memory.v1", + "passed" if passed else "failed", len(rows), + f"{wall_elapsed_seconds:.6f}", + f"{observed_epoch_seconds:.6f}", + f"{epoch_seconds[1]:.6f}", f"{epoch_seconds[2]:.6f}", + f"{restart_gap_seconds:.6f}", f"{restart_observed_seconds:.6f}", + maxima["pss_kib"], + maxima["pss_anon_kib"], maxima["pagetables_kib"], + maxima["cgroup_current_bytes"], maxima["cgroup_peak_bytes"], + f"{high_rate:.9f}", + f"{psi_some:.9f}", f"{psi_full:.9f}", max_dirty_writeback, + anon_growth, private_growth, pss_growth, anon_range, + args.gates_output, args.epochs, + ]], + ) + return 0 if passed else 1 + except (EvidenceError, OSError, csv.Error) as error: + print(f"checkpoint memory validation failed: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-memory.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-memory.test.py new file mode 100755 index 00000000..80e6fa2c --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-memory.test.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent +VALIDATOR = ROOT / "validate-checkpoint-memory.py" +HEADER = [ + "schema_version", "epoch", "epoch_origin_monotonic_ns", + "monotonic_ns", "phase", "process_count", "pss_kib", "pss_anon_kib", + "private_kib", "pagetables_kib", "cgroup_current_bytes", + "cgroup_peak_bytes", + "cgroup_swap_bytes", "cgroup_anon_bytes", "cgroup_file_bytes", + "cgroup_kernel_bytes", "cgroup_pagetables_bytes", + "cgroup_file_dirty_bytes", "cgroup_file_writeback_bytes", "event_high", + "event_max", "event_oom", "event_oom_kill", "psi_some_total_usec", + "psi_full_total_usec", +] + + +class CheckpointMemoryTest(unittest.TestCase): + def run_fixture( + self, + *, + oom_delta: int = 0, + restart_oom: int = 0, + wrong_phase_order: bool = False, + reuse_cgroup: bool = False, + ) -> subprocess.CompletedProcess[str]: + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + root = Path(temporary.name) + samples = root / "samples.tsv" + epochs = root / "epochs.tsv" + phases = [ + "initial-quiescent", + "initial-quiescent", + "steady", + "volume-checkpoint", + "volume-checkpoint", + "recycle-shutdown", + "recycle-startup", + "second-steady", + "post-recycle-quiescent", + "post-recycle-quiescent", + "post-recycle-quiescent", + ] + if wrong_phase_order: + phases[2], phases[3] = phases[3], phases[2] + with samples.open("x", newline="", encoding="utf-8") as stream: + writer = csv.writer(stream, delimiter="\t", lineterminator="\n") + writer.writerow(HEADER) + for index, phase in enumerate(phases): + writer.writerow( + [ + 1, 1 if index < 6 else 2, + 0 if index < 6 else 6_500_000_000, + (index + 1) * 1_000_000_000, phase, 4, + 70000 + index * 100, 45000 + index * 50, + 50000 + index * 50, 900, 120_000_000, 130_000_000, + 0, 50_000_000, 60_000_000, 3_000_000, 900_000, + 10_000_000, 1_000_000, (index % 6) * 10, 0, + restart_oom + + (oom_delta if index == len(phases) - 1 else 0) + if index >= 6 + else 0, + 0, (index % 6) * 1000, (index % 6) * 500, + ] + ) + with epochs.open("x", newline="", encoding="utf-8") as stream: + writer = csv.writer(stream, delimiter="\t", lineterminator="\n") + writer.writerow( + [ + "schema_version", "epoch", "epoch_origin_monotonic_ns", + "cgroup_path", "cgroup_identity", "memory_max_bytes", + "memory_high_bytes", "memory_swap_max_bytes", + ] + ) + writer.writerow( + [1, 1, 0, "/sys/fs/cgroup/test-e1", "7:101", + 268435456, 234881024, 0] + ) + writer.writerow( + [1, 2, 6_500_000_000, + "/sys/fs/cgroup/test-e1" if reuse_cgroup else "/sys/fs/cgroup/test-e2", + "7:101" if reuse_cgroup else "7:202", + 268435456, 234881024, 0] + ) + return subprocess.run( + [ + str(VALIDATOR), "--samples", str(samples), + "--epochs", str(epochs), + "--output", str(root / "summary.tsv"), + "--gates-output", str(root / "gates.tsv"), + ], + text=True, + capture_output=True, + check=False, + ) + + def test_bounded_evidence_passes(self) -> None: + result = self.run_fixture() + self.assertEqual(result.returncode, 0, result.stderr) + + def test_oom_delta_fails(self) -> None: + result = self.run_fixture(oom_delta=1) + self.assertEqual(result.returncode, 1, result.stderr) + + def test_restart_boundary_oom_fails(self) -> None: + result = self.run_fixture(restart_oom=1, oom_delta=1) + self.assertEqual(result.returncode, 1, result.stderr) + + def test_wrong_phase_order_is_rejected(self) -> None: + result = self.run_fixture(wrong_phase_order=True) + self.assertEqual(result.returncode, 2, result.stderr) + + def test_reused_cgroup_scope_is_rejected(self) -> None: + result = self.run_fixture(reuse_cgroup=True) + self.assertEqual(result.returncode, 2, result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-recycle.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-recycle.py new file mode 100755 index 00000000..81004f1a --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-recycle.py @@ -0,0 +1,579 @@ +#!/usr/bin/env python3 +"""Validate one open-loop PostgreSQL checkpoint-overlap evidence sample.""" + +from __future__ import annotations + +import argparse +import csv +import math +import re +import sys +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Iterable + + +TRANSACTION_HEADER = [ + "schema_version", + "client", + "sequence", + "scheduled_mono_ns", + "start_mono_ns", + "end_mono_ns", + "start_real_ns", + "end_real_ns", + "service_ns", + "lateness_ns", + "status", + "update_count", + "insert_count", + "read_count", + "insert_lsn", +] +FLUSH_HEADER = [ + "schema_version", + "client", + "through_sequence", + "insert_lsn", + "flush_lsn", + "covers", + "status", +] +STATE_HEADER = ["num_timed", "num_requested", "num_done", "wal_bytes"] +START_RE = re.compile(r"checkpoint starting: ([a-z]+)") +COMPLETE_RE = re.compile(r"checkpoint complete: .*?total=([0-9]+(?:\.[0-9]+)?) s") +TIMESTAMP_RE = re.compile( + r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?)(?: ([A-Z]{3,5}|[+-]\d{2}(?::?\d{2})?))?" +) +LSN_RE = re.compile(r"^([0-9A-F]+)/([0-9A-F]{1,8})$") + + +class EvidenceError(ValueError): + pass + + +@dataclass(frozen=True) +class Checkpoint: + reason: str + start_ns: int + end_ns: int + total_seconds: float + + +def regular_file(path: Path) -> Path: + if not path.is_file() or path.is_symlink(): + raise EvidenceError(f"evidence must be a regular non-symlink file: {path}") + return path + + +def read_dict_rows(path: Path, expected_header: list[str]) -> list[dict[str, str]]: + regular_file(path) + with path.open(newline="", encoding="utf-8") as stream: + reader = csv.DictReader(stream, delimiter="\t") + if reader.fieldnames != expected_header: + raise EvidenceError( + f"unexpected TSV header in {path}: {reader.fieldnames!r}" + ) + rows = list(reader) + if not rows: + raise EvidenceError(f"evidence contains no rows: {path}") + return rows + + +def parse_nonnegative(row: dict[str, str], field: str) -> int: + value = row[field] + if not value.isdigit(): + raise EvidenceError(f"{field} is not a nonnegative integer: {value!r}") + return int(value) + + +def read_state(path: Path) -> dict[str, int]: + rows = read_dict_rows(path, STATE_HEADER) + if len(rows) != 1: + raise EvidenceError(f"checkpoint state must contain exactly one row: {path}") + return {field: parse_nonnegative(rows[0], field) for field in STATE_HEADER} + + +def nearest_rank(values: Iterable[int], fraction: float) -> int: + ordered = sorted(values) + if not ordered: + raise EvidenceError("cannot calculate a percentile over no values") + rank = max(1, math.ceil(fraction * len(ordered))) + return ordered[rank - 1] + + +def parse_lsn(value: str) -> int: + match = LSN_RE.fullmatch(value) + if match is None: + raise EvidenceError(f"malformed PostgreSQL LSN: {value!r}") + return (int(match.group(1), 16) << 32) + int(match.group(2), 16) + + +def parse_timestamp_ns(line: str) -> int | None: + match = TIMESTAMP_RE.match(line) + if match is None: + return None + text, zone = match.groups() + parsed = datetime.fromisoformat(text) + if zone in (None, "UTC", "GMT"): + parsed = parsed.replace(tzinfo=timezone.utc) + elif re.fullmatch(r"[+-]\d{2}", zone): + parsed = datetime.fromisoformat(f"{text}{zone}:00") + elif re.fullmatch(r"[+-]\d{4}", zone): + parsed = datetime.fromisoformat(f"{text}{zone[:3]}:{zone[3:]}") + elif zone and zone.startswith(("+", "-")): + parsed = datetime.fromisoformat(f"{text}{zone}") + else: + raise EvidenceError(f"unsupported checkpoint log timezone: {zone!r}") + return int(parsed.timestamp() * 1_000_000_000) + + +def parse_checkpoints(path: Path) -> tuple[list[Checkpoint], bool]: + regular_file(path) + starts: list[tuple[str, int]] = [] + checkpoints: list[Checkpoint] = [] + too_frequent = False + with path.open(encoding="utf-8", errors="strict") as stream: + for line in stream: + too_frequent |= "checkpoints are occurring too frequently" in line + start_match = START_RE.search(line) + complete_match = COMPLETE_RE.search(line) + if start_match is not None: + timestamp = parse_timestamp_ns(line) + if timestamp is None: + raise EvidenceError("checkpoint start lacks a parseable %m timestamp") + starts.append((start_match.group(1), timestamp)) + elif complete_match is not None: + timestamp = parse_timestamp_ns(line) + if timestamp is None: + raise EvidenceError( + "checkpoint completion lacks a parseable %m timestamp" + ) + if not starts: + continue + reason, started = starts.pop(0) + checkpoints.append( + Checkpoint(reason, started, timestamp, float(complete_match.group(1))) + ) + return checkpoints, too_frequent + + +def write_gates(path: Path, gates: list[tuple[str, str, str, bool]]) -> None: + if path.exists() or path.is_symlink(): + raise EvidenceError(f"refusing to replace gate output: {path}") + with path.open("x", encoding="utf-8", newline="") as stream: + writer = csv.writer(stream, delimiter="\t", lineterminator="\n") + writer.writerow(["gate", "expected", "observed", "status"]) + for name, expected, observed, passed in gates: + writer.writerow([name, expected, observed, "passed" if passed else "failed"]) + + +def parser() -> argparse.ArgumentParser: + result = argparse.ArgumentParser() + result.add_argument("--transactions", type=Path, required=True) + result.add_argument("--flushes", type=Path, required=True) + result.add_argument("--checkpoint-before", type=Path, required=True) + result.add_argument("--checkpoint-after", type=Path, required=True) + result.add_argument("--server-log", type=Path, required=True) + result.add_argument("--output", type=Path, required=True) + result.add_argument("--gates-output", type=Path, required=True) + result.add_argument("--target", choices=("native", "wasix"), required=True) + result.add_argument("--mode", choices=("smoke", "diagnostic", "promotional"), required=True) + result.add_argument("--clients", type=int, required=True) + result.add_argument("--duration-seconds", type=int, required=True) + result.add_argument("--tps-per-client", type=int, required=True) + result.add_argument("--stagger-us", type=int, required=True) + result.add_argument("--min-completion-fraction", type=float, default=0.99) + result.add_argument("--min-achieved-tps", type=float, required=True) + result.add_argument("--min-wal-bytes", type=int, required=True) + result.add_argument("--min-checkpoints", type=int, required=True) + result.add_argument("--max-checkpoint-seconds", type=float, default=30.0) + result.add_argument("--max-p95-ms", type=float, default=10.0) + result.add_argument("--max-p99-ms", type=float, default=50.0) + result.add_argument("--max-latency-ms", type=float, default=250.0) + result.add_argument("--max-overlap-ratio", type=float, default=2.0) + result.add_argument("--max-overlap-delta-ms", type=float, default=10.0) + result.add_argument("--min-overlap-samples", type=int, required=True) + result.add_argument("--max-clock-duration-skew-ms", type=float, default=2.0) + result.add_argument("--max-clock-offset-drift-ms", type=float, default=5.0) + result.add_argument("--enforce-performance", action="store_true") + return result + + +def main() -> int: + args = parser().parse_args() + try: + for positive_name in ( + "clients", + "duration_seconds", + "tps_per_client", + "min_wal_bytes", + "min_checkpoints", + "min_overlap_samples", + ): + if getattr(args, positive_name) <= 0: + raise EvidenceError(f"--{positive_name.replace('_', '-')} must be positive") + if not 0 < args.min_completion_fraction <= 1: + raise EvidenceError("--min-completion-fraction must be in (0, 1]") + if not 0 <= args.stagger_us <= 1_000_000: + raise EvidenceError("--stagger-us must be in [0, 1000000]") + + transactions = read_dict_rows(args.transactions, TRANSACTION_HEADER) + flushes = read_dict_rows(args.flushes, FLUSH_HEADER) + before = read_state(args.checkpoint_before) + after = read_state(args.checkpoint_after) + checkpoints, too_frequent = parse_checkpoints(args.server_log) + + expected_rows = args.clients * args.duration_seconds * args.tps_per_client + seen: set[tuple[int, int]] = set() + sequences: dict[int, list[int]] = {client: [] for client in range(1, args.clients + 1)} + successful_lsn: dict[tuple[int, int], str] = {} + scheduled_by_key: dict[tuple[int, int], int] = {} + ends_by_key: dict[tuple[int, int], int] = {} + service_ns: list[int] = [] + window_start_ns: int | None = None + window_end_ns: int | None = None + invalid_rows = 0 + skipped = 0 + clock_duration_skew_ns = 0 + clock_offsets_ns: list[int] = [] + for row in transactions: + if row["schema_version"] != "1": + raise EvidenceError("transaction row has an unknown schema version") + client = parse_nonnegative(row, "client") + sequence = parse_nonnegative(row, "sequence") + key = (client, sequence) + if client not in sequences or key in seen: + invalid_rows += 1 + continue + seen.add(key) + sequences[client].append(sequence) + start_real = parse_nonnegative(row, "start_real_ns") + end_real = parse_nonnegative(row, "end_real_ns") + scheduled_mono = parse_nonnegative(row, "scheduled_mono_ns") + start_mono = parse_nonnegative(row, "start_mono_ns") + end_mono = parse_nonnegative(row, "end_mono_ns") + measured_service = parse_nonnegative(row, "service_ns") + measured_lateness = parse_nonnegative(row, "lateness_ns") + expected_lateness = max(0, start_mono - scheduled_mono) + if ( + end_real < start_real + or start_mono < scheduled_mono + or end_mono < start_mono + or measured_service != end_mono - start_mono + or measured_lateness != expected_lateness + ): + invalid_rows += 1 + scheduled_by_key[key] = scheduled_mono + ends_by_key[key] = end_mono + realtime_duration = end_real - start_real + clock_duration_skew_ns = max( + clock_duration_skew_ns, abs(realtime_duration - measured_service) + ) + clock_offsets_ns.extend( + [start_real - start_mono, end_real - end_mono] + ) + window_start_ns = start_real if window_start_ns is None else min(window_start_ns, start_real) + window_end_ns = end_real if window_end_ns is None else max(window_end_ns, end_real) + if row["status"] == "skipped": + skipped += 1 + continue + if ( + row["status"] != "ok" + or row["update_count"] != "48" + or row["insert_count"] != "16" + or row["read_count"] != "8" + ): + invalid_rows += 1 + continue + try: + parse_lsn(row["insert_lsn"]) + except EvidenceError: + invalid_rows += 1 + continue + successful_lsn[key] = row["insert_lsn"] + service_ns.append(measured_service) + + ordered_sequences: dict[int, list[int]] = {} + for client, values in sequences.items(): + ordered = sorted(values) + ordered_sequences[client] = ordered + if len(ordered) != args.duration_seconds * args.tps_per_client: + invalid_rows += 1 + elif ordered != list(range(ordered[0], ordered[0] + len(ordered))): + invalid_rows += 1 + + interval_ns = 1_000_000_000 // args.tps_per_client + sequence_starts = { + values[0] for values in ordered_sequences.values() if values + } + schedule_base_ns: int | None = None + schedule_grid_errors = 0 + if len(sequence_starts) != 1 or not ordered_sequences[1]: + schedule_grid_errors += 1 + else: + first_sequence = next(iter(sequence_starts)) + schedule_base_ns = scheduled_by_key.get((1, first_sequence)) + if schedule_base_ns is None: + schedule_grid_errors += 1 + else: + for client, values in ordered_sequences.items(): + previous_scheduled: int | None = None + for sequence in values: + scheduled = scheduled_by_key.get((client, sequence)) + expected_scheduled = ( + schedule_base_ns + + (client - 1) * args.stagger_us * 1_000 + + (sequence - first_sequence) * interval_ns + ) + if ( + scheduled is None + or scheduled != expected_scheduled + or ( + previous_scheduled is not None + and scheduled <= previous_scheduled + ) + ): + schedule_grid_errors += 1 + previous_scheduled = scheduled + + flush_clients: set[int] = set() + flush_sequences: dict[int, list[int]] = { + client: [] for client in range(1, args.clients + 1) + } + previous_flush_lsn: dict[int, int] = {} + seen_flushes: set[tuple[int, int]] = set() + invalid_flushes = 0 + for row in flushes: + if row["schema_version"] != "1": + invalid_flushes += 1 + continue + client = parse_nonnegative(row, "client") + sequence = parse_nonnegative(row, "through_sequence") + key = (client, sequence) + try: + insert_lsn = parse_lsn(row["insert_lsn"]) + flush_lsn = parse_lsn(row["flush_lsn"]) + except EvidenceError: + invalid_flushes += 1 + continue + if ( + client not in sequences + or key in seen_flushes + or row["status"] != "ok" + or row["covers"] != "t" + or successful_lsn.get(key) != row["insert_lsn"] + or flush_lsn < insert_lsn + or ( + client in previous_flush_lsn + and flush_lsn < previous_flush_lsn[client] + ) + ): + invalid_flushes += 1 + continue + seen_flushes.add(key) + flush_clients.add(client) + flush_sequences[client].append(sequence) + previous_flush_lsn[client] = flush_lsn + for client, values in sequences.items(): + successful = sorted( + sequence for sequence in values if (client, sequence) in successful_lsn + ) + if not successful: + invalid_flushes += 1 + continue + required = list( + range(successful[0] + 255, successful[-1] + 1, 256) + ) + if not required or required[-1] != successful[-1]: + required.append(successful[-1]) + if flush_sequences[client] != required: + invalid_flushes += 1 + + if window_start_ns is None or window_end_ns is None or not service_ns: + raise EvidenceError("transaction evidence has no successful measured window") + periodic = [ + checkpoint + for checkpoint in checkpoints + if checkpoint.reason == "time" + and checkpoint.start_ns >= window_start_ns + and checkpoint.start_ns <= window_end_ns + ] + overlap_service: list[int] = [] + non_overlap_service: list[int] = [] + for row in transactions: + if row["status"] != "ok": + continue + start_real = int(row["start_real_ns"]) + end_real = int(row["end_real_ns"]) + overlaps = any( + start_real <= checkpoint.end_ns and end_real >= checkpoint.start_ns + for checkpoint in periodic + ) + (overlap_service if overlaps else non_overlap_service).append(int(row["service_ns"])) + + deltas = {field: after[field] - before[field] for field in STATE_HEADER} + if any(delta < 0 for delta in deltas.values()): + raise EvidenceError("checkpoint statistics moved backwards during the sample") + completed = len(service_ns) + completion_fraction = completed / expected_rows + observed_start_ns = min(scheduled_by_key.values()) + observed_end_ns = max(ends_by_key.values()) + observed_window_ns = observed_end_ns - observed_start_ns + if observed_window_ns <= 0: + raise EvidenceError("transaction evidence has no positive monotonic window") + achieved_tps = completed * 1_000_000_000 / observed_window_ns + scheduled_span_ns = max(scheduled_by_key.values()) - observed_start_ns + expected_scheduled_span_ns = ( + (args.duration_seconds * args.tps_per_client - 1) * interval_ns + + (args.clients - 1) * args.stagger_us * 1_000 + ) + p95_ns = nearest_rank(service_ns, 0.95) + p99_ns = nearest_rank(service_ns, 0.99) + max_ns = max(service_ns) + overlap_p99_ns = nearest_rank(overlap_service, 0.99) if overlap_service else 0 + non_overlap_p99_ns = nearest_rank(non_overlap_service, 0.99) if non_overlap_service else 0 + overlap_ratio_limit_ns = int(non_overlap_p99_ns * args.max_overlap_ratio) + overlap_delta_limit_ns = ( + non_overlap_p99_ns + int(args.max_overlap_delta_ms * 1_000_000) + ) + clock_offset_drift_ns = max(clock_offsets_ns) - min(clock_offsets_ns) + + gates: list[tuple[str, str, str, bool]] = [] + + def gate(name: str, expected: str, observed: object, passed: bool) -> None: + gates.append((name, expected, str(observed), passed)) + + gate("transaction_rows", str(expected_rows), len(transactions), len(transactions) == expected_rows) + gate("transaction_shape", "zero-invalid", invalid_rows, invalid_rows == 0) + gate("monotonic_schedule_grid", "exact", schedule_grid_errors, schedule_grid_errors == 0) + gate( + "scheduled_span_ns", + str(expected_scheduled_span_ns), + scheduled_span_ns, + scheduled_span_ns == expected_scheduled_span_ns, + ) + gate( + "realtime_monotonic_duration_skew_ms", + f"<={args.max_clock_duration_skew_ms}", + f"{clock_duration_skew_ns / 1_000_000:.6f}", + clock_duration_skew_ns <= args.max_clock_duration_skew_ms * 1_000_000, + ) + gate( + "realtime_monotonic_offset_drift_ms", + f"<={args.max_clock_offset_drift_ms}", + f"{clock_offset_drift_ns / 1_000_000:.6f}", + clock_offset_drift_ns <= args.max_clock_offset_drift_ms * 1_000_000, + ) + gate("skipped_offers", "0", skipped, skipped == 0) + gate( + "completion_fraction", + f">={args.min_completion_fraction}", + f"{completion_fraction:.6f}", + completion_fraction >= args.min_completion_fraction, + ) + gate( + "achieved_tps", + f">={args.min_achieved_tps}", + f"{achieved_tps:.6f}", + achieved_tps >= args.min_achieved_tps, + ) + gate("flush_receipts", f"{args.clients}-clients-covered", len(flush_clients), invalid_flushes == 0 and len(flush_clients) == args.clients) + gate("requested_checkpoints", "0", deltas["num_requested"], deltas["num_requested"] == 0) + gate("timed_checkpoints", f">={args.min_checkpoints}", deltas["num_timed"], deltas["num_timed"] >= args.min_checkpoints) + gate("completed_checkpoints", f">={args.min_checkpoints}", deltas["num_done"], deltas["num_done"] >= args.min_checkpoints) + gate("logged_periodic_checkpoints", f">={args.min_checkpoints}", len(periodic), len(periodic) >= args.min_checkpoints) + gate("checkpoint_duration", f"<{args.max_checkpoint_seconds}s", max((item.total_seconds for item in periodic), default=0), bool(periodic) and all(item.total_seconds < args.max_checkpoint_seconds for item in periodic)) + gate("checkpoint_frequency_warning", "absent", too_frequent, not too_frequent) + gate("wal_bytes", f">={args.min_wal_bytes}", deltas["wal_bytes"], deltas["wal_bytes"] >= args.min_wal_bytes) + gate("overlap_samples", f">={args.min_overlap_samples}", len(overlap_service), len(overlap_service) >= args.min_overlap_samples) + if args.enforce_performance: + gate("p95_ms", f"<={args.max_p95_ms}", f"{p95_ns / 1_000_000:.6f}", p95_ns <= args.max_p95_ms * 1_000_000) + gate("p99_ms", f"<={args.max_p99_ms}", f"{p99_ns / 1_000_000:.6f}", p99_ns <= args.max_p99_ms * 1_000_000) + gate("max_ms", f"<={args.max_latency_ms}", f"{max_ns / 1_000_000:.6f}", max_ns <= args.max_latency_ms * 1_000_000) + gate( + "checkpoint_overlap_p99_ratio", + f"<={overlap_ratio_limit_ns / 1_000_000:.6f}ms", + f"{overlap_p99_ns / 1_000_000:.6f}", + bool(non_overlap_service) + and overlap_p99_ns <= overlap_ratio_limit_ns, + ) + gate( + "checkpoint_overlap_p99_delta", + f"<={overlap_delta_limit_ns / 1_000_000:.6f}ms", + f"{overlap_p99_ns / 1_000_000:.6f}", + bool(non_overlap_service) + and overlap_p99_ns <= overlap_delta_limit_ns, + ) + + passed = all(item[3] for item in gates) + write_gates(args.gates_output, gates) + if args.output.exists() or args.output.is_symlink(): + raise EvidenceError(f"refusing to replace validation output: {args.output}") + with args.output.open("x", encoding="utf-8", newline="") as stream: + writer = csv.writer(stream, delimiter="\t", lineterminator="\n") + writer.writerow( + [ + "schema_version", + "target", + "mode", + "status", + "performance_enforced", + "expected_transactions", + "completed_transactions", + "completion_fraction", + "achieved_tps", + "observed_window_ns", + "scheduled_span_ns", + "p50_ns", + "p95_ns", + "p99_ns", + "max_ns", + "overlap_samples", + "overlap_p99_ns", + "non_overlap_p99_ns", + "timed_checkpoints", + "requested_checkpoints", + "completed_checkpoints", + "logged_periodic_checkpoints", + "wal_bytes", + "gates", + ] + ) + writer.writerow( + [ + "oliphaunt.wasix-postmaster.checkpoint-sample.v1", + args.target, + args.mode, + "passed" if passed else "failed", + int(args.enforce_performance), + expected_rows, + completed, + f"{completion_fraction:.9f}", + f"{achieved_tps:.6f}", + observed_window_ns, + scheduled_span_ns, + nearest_rank(service_ns, 0.50), + p95_ns, + p99_ns, + max_ns, + len(overlap_service), + overlap_p99_ns, + non_overlap_p99_ns, + deltas["num_timed"], + deltas["num_requested"], + deltas["num_done"], + len(periodic), + deltas["wal_bytes"], + args.gates_output, + ] + ) + return 0 if passed else 1 + except (EvidenceError, OSError, csv.Error) as error: + print(f"checkpoint evidence validation failed: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-recycle.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-recycle.test.py new file mode 100755 index 00000000..52c84d50 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-checkpoint-recycle.test.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent +VALIDATOR = ROOT / "validate-checkpoint-recycle.py" + + +def write_tsv(path: Path, header: list[str], rows: list[list[object]]) -> None: + with path.open("x", newline="", encoding="utf-8") as stream: + writer = csv.writer(stream, delimiter="\t", lineterminator="\n") + writer.writerow(header) + writer.writerows(rows) + + +class CheckpointEvidenceTest(unittest.TestCase): + def run_fixture( + self, *, requested: int = 0, corrupt_schedule: bool = False + ) -> subprocess.CompletedProcess[str]: + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + root = Path(temporary.name) + transactions = root / "transactions.tsv" + flushes = root / "flushes.tsv" + before = root / "before.tsv" + after = root / "after.tsv" + log = root / "server.log" + output = root / "summary.tsv" + gates = root / "gates.tsv" + tx_header = [ + "schema_version", "client", "sequence", "scheduled_mono_ns", + "start_mono_ns", "end_mono_ns", "start_real_ns", "end_real_ns", + "service_ns", "lateness_ns", "status", "update_count", + "insert_count", "read_count", "insert_lsn", + ] + base_real = 1_786_320_000_000_000_000 + rows: list[list[object]] = [] + for client in (1, 2): + for sequence in (1, 2): + scheduled = ( + 1_000_000_000 + + (client - 1) * 1_000_000 + + (sequence - 1) * 1_000_000_000 + ) + if corrupt_schedule and client == 2 and sequence == 2: + scheduled += 1 + start = base_real + scheduled + start_mono = scheduled + rows.append( + [1, client, sequence, scheduled, + start_mono, start_mono + 2_000_000, + start, start + 2_000_000, 2_000_000, 0, "ok", 48, 16, 8, + f"0/{client}{sequence}"] + ) + write_tsv(transactions, tx_header, rows) + write_tsv( + flushes, + ["schema_version", "client", "through_sequence", "insert_lsn", + "flush_lsn", "covers", "status"], + [[1, 1, 2, "0/12", "0/20", "t", "ok"], + [1, 2, 2, "0/22", "0/30", "t", "ok"]], + ) + write_tsv(before, ["num_timed", "num_requested", "num_done", "wal_bytes"], [[10, 4, 14, 100]]) + write_tsv(after, ["num_timed", "num_requested", "num_done", "wal_bytes"], [[11, 4 + requested, 15 + requested, 1100]]) + log.write_text( + "2026-08-10 00:00:01.000500 UTC [1] LOG: checkpoint starting: time\n" + "2026-08-10 00:00:01.001500 UTC [1] LOG: checkpoint complete: wrote 1 buffers; total=0.001 s; sync=0.001 s\n", + encoding="utf-8", + ) + return subprocess.run( + [ + str(VALIDATOR), + "--transactions", str(transactions), + "--flushes", str(flushes), + "--checkpoint-before", str(before), + "--checkpoint-after", str(after), + "--server-log", str(log), + "--output", str(output), + "--gates-output", str(gates), + "--target", "wasix", + "--mode", "smoke", + "--clients", "2", + "--duration-seconds", "2", + "--tps-per-client", "1", + "--stagger-us", "1000", + "--min-achieved-tps", "2", + "--min-wal-bytes", "1000", + "--min-checkpoints", "1", + "--min-overlap-samples", "1", + ], + text=True, + capture_output=True, + check=False, + ) + + def test_valid_smoke_evidence_passes(self) -> None: + result = self.run_fixture() + self.assertEqual(result.returncode, 0, result.stderr) + + def test_requested_checkpoint_fails(self) -> None: + result = self.run_fixture(requested=1) + self.assertEqual(result.returncode, 1, result.stderr) + + def test_non_grid_schedule_fails(self) -> None: + result = self.run_fixture(corrupt_schedule=True) + self.assertEqual(result.returncode, 1, result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-file-cache-telemetry.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-file-cache-telemetry.py new file mode 100755 index 00000000..32eb3530 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-file-cache-telemetry.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 + +"""Validate one exact observe-only file-cache telemetry snapshot.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import stat +import sys +from pathlib import Path +from typing import Any + + +SCHEMA = "oliphaunt.wasix-postmaster.file-cache-telemetry.v2" +RESULT_SCHEMA = "oliphaunt.wasix-postmaster.file-cache-telemetry-validation.v2" +POLICY_ID = "oliphaunt.wasix-postmaster.file-cache.observe-only.v1" +ABI_MODULE = "oliphaunt_postmaster_v1" +ABI_FUNCTION = "fd_cache_offer" +ABI_SIGNATURE = "(i32,i64,i64,i32,i32)->i32_errno" +SHA256_RE = re.compile(r"[0-9a-f]{64}\Z") +TOP_LEVEL_FIELDS = { + "schema", + "policy-id", + "policy-mode", + "workload-id", + "runtime-abi-id", + "abi-module", + "abi-function", + "abi-signature", + "classes", + "validation", +} +CLASS_FIELDS = { + "class", + "name", + "disposition", + "calls", + "finite-bytes", + "through-eof-calls", + "reclaim-eligible-calls", + "reclaim-eligible-finite-bytes", + "reclaim-eligible-through-eof-calls", +} +VALIDATION_FIELDS = { + "valid", + "invalid-range", + "invalid-class", + "invalid-flags", + "bad-descriptor", + "missing-rights", + "non-regular", + "non-host-backed", + "state-fault", + "controller-error", +} +CLASSES = ( + (1, "relation-read-normal"), + (2, "relation-read-bulk"), + (3, "relation-read-vacuum"), + (4, "relation-sync-checkpoint"), + (5, "relation-sync-immediate"), + (6, "wal-inactive-durable"), +) + + +class ValidationError(Exception): + pass + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ValidationError(message) + + +def duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + require(key not in result, f"duplicate JSON field: {key}") + result[key] = value + return result + + +def read_regular_stable(path: Path, label: str) -> bytes: + before = os.lstat(path) + require( + stat.S_ISREG(before.st_mode) and not stat.S_ISLNK(before.st_mode), + f"{label} must be a regular non-symlink file", + ) + descriptor = os.open( + path, + os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0), + ) + try: + opened = os.fstat(descriptor) + require( + (before.st_dev, before.st_ino) == (opened.st_dev, opened.st_ino), + f"{label} changed while opening", + ) + chunks: list[bytes] = [] + remaining = opened.st_size + while remaining: + chunk = os.read(descriptor, min(remaining, 1024 * 1024)) + require(bool(chunk), f"{label} was truncated while reading") + chunks.append(chunk) + remaining -= len(chunk) + after = os.fstat(descriptor) + require( + ( + opened.st_dev, + opened.st_ino, + opened.st_size, + opened.st_mtime_ns, + opened.st_ctime_ns, + ) + == ( + after.st_dev, + after.st_ino, + after.st_size, + after.st_mtime_ns, + after.st_ctime_ns, + ), + f"{label} changed while reading", + ) + return b"".join(chunks) + finally: + os.close(descriptor) + + +def parse_object(data: bytes, label: str) -> dict[str, Any]: + try: + value = json.loads(data.decode("utf-8"), object_pairs_hook=duplicate_keys) + except (UnicodeDecodeError, json.JSONDecodeError, ValidationError) as error: + raise ValidationError(f"invalid {label}: {error}") from error + require(isinstance(value, dict), f"{label} must be a JSON object") + return value + + +def exact_nonnegative(value: Any, label: str) -> int: + require(type(value) is int and value >= 0, f"{label} must be a nonnegative integer") + require(value <= (1 << 64) - 1, f"{label} exceeds u64") + return value + + +def manifest_runtime_abi_id(manifest: dict[str, Any]) -> str: + value = manifest.get("runtime-abi-id") + require( + isinstance(value, str) and SHA256_RE.fullmatch(value) is not None, + "sealed manifest runtime-abi-id is invalid", + ) + return value + + +def validate( + telemetry_path: Path, + manifest_path: Path, + output: Path, + expected_workload: str, +) -> None: + require( + expected_workload in ("runtime:initdb", "runtime:postgres"), + "expected workload is not a sealed product executable", + ) + require(not os.path.lexists(output), f"validation output already exists: {output}") + telemetry_data = read_regular_stable(telemetry_path, "file-cache telemetry") + manifest_data = read_regular_stable(manifest_path, "sealed manifest") + telemetry = parse_object(telemetry_data, "file-cache telemetry JSON") + manifest = parse_object(manifest_data, "sealed manifest JSON") + require(set(telemetry) == TOP_LEVEL_FIELDS, "file-cache telemetry fields differ") + expected_scalars = { + "schema": SCHEMA, + "policy-id": POLICY_ID, + "policy-mode": "observe-only", + "workload-id": expected_workload, + "runtime-abi-id": manifest_runtime_abi_id(manifest), + "abi-module": ABI_MODULE, + "abi-function": ABI_FUNCTION, + "abi-signature": ABI_SIGNATURE, + } + for field, expected in expected_scalars.items(): + require( + telemetry[field] == expected, + f"file-cache telemetry {field} differs: expected {expected!r}, got {telemetry[field]!r}", + ) + + classes = telemetry["classes"] + require(isinstance(classes, list) and len(classes) == len(CLASSES), "cache class closure differs") + total_calls = 0 + total_finite_bytes = 0 + total_through_eof_calls = 0 + wal_reclaim_eligible_calls = 0 + wal_reclaim_eligible_finite_bytes = 0 + wal_reclaim_eligible_through_eof_calls = 0 + for index, ((class_id, name), record) in enumerate(zip(CLASSES, classes, strict=True)): + require(isinstance(record, dict) and set(record) == CLASS_FIELDS, f"cache class {index} fields differ") + require(type(record["class"]) is int and record["class"] == class_id, f"cache class {index} id differs") + require(record["name"] == name, f"cache class {class_id} name differs") + require(record["disposition"] == "retain", f"cache class {class_id} did not retain") + total_calls += exact_nonnegative(record["calls"], f"cache class {class_id} calls") + total_finite_bytes += exact_nonnegative( + record["finite-bytes"], f"cache class {class_id} finite bytes" + ) + total_through_eof_calls += exact_nonnegative( + record["through-eof-calls"], f"cache class {class_id} through-EOF calls" + ) + reclaim_eligible_calls = exact_nonnegative( + record["reclaim-eligible-calls"], + f"cache class {class_id} reclaim-eligible calls", + ) + reclaim_eligible_finite_bytes = exact_nonnegative( + record["reclaim-eligible-finite-bytes"], + f"cache class {class_id} reclaim-eligible finite bytes", + ) + reclaim_eligible_through_eof_calls = exact_nonnegative( + record["reclaim-eligible-through-eof-calls"], + f"cache class {class_id} reclaim-eligible through-EOF calls", + ) + require( + reclaim_eligible_calls <= record["calls"], + f"cache class {class_id} reclaim-eligible calls exceed calls", + ) + require( + reclaim_eligible_finite_bytes <= record["finite-bytes"], + f"cache class {class_id} reclaim-eligible finite bytes exceed finite bytes", + ) + require( + reclaim_eligible_through_eof_calls <= record["through-eof-calls"], + f"cache class {class_id} reclaim-eligible through-EOF calls exceed through-EOF calls", + ) + require( + reclaim_eligible_through_eof_calls <= reclaim_eligible_calls, + f"cache class {class_id} reclaim-eligible dimensions disagree", + ) + if class_id != 6: + require( + reclaim_eligible_calls == 0 + and reclaim_eligible_finite_bytes == 0 + and reclaim_eligible_through_eof_calls == 0, + f"non-WAL cache class {class_id} carried WAL reclaim eligibility", + ) + else: + wal_reclaim_eligible_calls = reclaim_eligible_calls + wal_reclaim_eligible_finite_bytes = reclaim_eligible_finite_bytes + wal_reclaim_eligible_through_eof_calls = reclaim_eligible_through_eof_calls + + validation = telemetry["validation"] + require( + isinstance(validation, dict) and set(validation) == VALIDATION_FIELDS, + "file-cache validation fields differ", + ) + validation_counts = { + field: exact_nonnegative(value, f"validation {field}") + for field, value in validation.items() + } + require(validation_counts["valid"] == total_calls, "valid offer and class call counts differ") + for field in VALIDATION_FIELDS - {"valid"}: + require(validation_counts[field] == 0, f"validated product emitted {field} cache offers") + + output.parent.mkdir(parents=True, exist_ok=True) + pending = output.with_name(f".{output.name}.pending.{os.getpid()}") + require(not os.path.lexists(pending), f"validation pending output already exists: {pending}") + with pending.open("x", encoding="utf-8", newline="\n") as sink: + sink.write( + "schema_version\tstatus\tworkload_id\tpolicy_id\truntime_abi_id\t" + "valid_calls\tfinite_bytes\tthrough_eof_calls\t" + "wal_reclaim_eligible_calls\twal_reclaim_eligible_finite_bytes\t" + "wal_reclaim_eligible_through_eof_calls\ttelemetry_sha256\t" + "manifest_sha256\tvalidator_sha256\n" + ) + sink.write( + f"{RESULT_SCHEMA}\tpassed\t{expected_workload}\t{POLICY_ID}\t" + f"{expected_scalars['runtime-abi-id']}\t{total_calls}\t{total_finite_bytes}\t" + f"{total_through_eof_calls}\t{wal_reclaim_eligible_calls}\t" + f"{wal_reclaim_eligible_finite_bytes}\t" + f"{wal_reclaim_eligible_through_eof_calls}\t" + f"{hashlib.sha256(telemetry_data).hexdigest()}\t" + f"{hashlib.sha256(manifest_data).hexdigest()}\t" + f"{hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}\n" + ) + sink.flush() + os.fsync(sink.fileno()) + os.chmod(pending, 0o444) + os.rename(pending, output) + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--telemetry", type=Path, required=True) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument( + "--expected-workload", + choices=("runtime:initdb", "runtime:postgres"), + required=True, + ) + arguments = parser.parse_args(argv) + try: + validate( + arguments.telemetry, + arguments.manifest, + arguments.output, + arguments.expected_workload, + ) + return 0 + except (OSError, ValidationError) as error: + print(f"file-cache telemetry validation failed: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-file-cache-telemetry.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-file-cache-telemetry.test.py new file mode 100755 index 00000000..bf727696 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-file-cache-telemetry.test.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + + +SCRIPT = Path(__file__).with_name("validate-file-cache-telemetry.py") +ABI_ID = "12" * 32 + + +def telemetry() -> dict: + names = ( + "relation-read-normal", + "relation-read-bulk", + "relation-read-vacuum", + "relation-sync-checkpoint", + "relation-sync-immediate", + "wal-inactive-durable", + ) + classes = [ + { + "class": index, + "name": name, + "disposition": "retain", + "calls": 0, + "finite-bytes": 0, + "through-eof-calls": 0, + "reclaim-eligible-calls": 0, + "reclaim-eligible-finite-bytes": 0, + "reclaim-eligible-through-eof-calls": 0, + } + for index, name in enumerate(names, 1) + ] + classes[0].update({"calls": 2, "finite-bytes": 12288}) + classes[5].update( + { + "calls": 2, + "finite-bytes": 2 * 16 * 1024 * 1024, + "reclaim-eligible-calls": 1, + "reclaim-eligible-finite-bytes": 16 * 1024 * 1024, + } + ) + return { + "schema": "oliphaunt.wasix-postmaster.file-cache-telemetry.v2", + "policy-id": "oliphaunt.wasix-postmaster.file-cache.observe-only.v1", + "policy-mode": "observe-only", + "workload-id": "runtime:postgres", + "runtime-abi-id": ABI_ID, + "abi-module": "oliphaunt_postmaster_v1", + "abi-function": "fd_cache_offer", + "abi-signature": "(i32,i64,i64,i32,i32)->i32_errno", + "classes": classes, + "validation": { + "valid": 4, + "invalid-range": 0, + "invalid-class": 0, + "invalid-flags": 0, + "bad-descriptor": 0, + "missing-rights": 0, + "non-regular": 0, + "non-host-backed": 0, + "state-fault": 0, + "controller-error": 0, + }, + } + + +class ValidateFileCacheTelemetryTests(unittest.TestCase): + def run_validator(self, value: dict, *, manifest_abi: str = ABI_ID, raw: str | None = None): + temporary = tempfile.TemporaryDirectory() + root = Path(temporary.name) + snapshot = root / "telemetry.json" + manifest = root / "manifest.json" + output = root / "validation.tsv" + snapshot.write_text(raw if raw is not None else json.dumps(value) + "\n", encoding="utf-8") + manifest.write_text(json.dumps({"runtime-abi-id": manifest_abi}) + "\n", encoding="utf-8") + result = subprocess.run( + [ + sys.executable, + str(SCRIPT), + "--telemetry", + str(snapshot), + "--manifest", + str(manifest), + "--output", + str(output), + "--expected-workload", + "runtime:postgres", + ], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + contents = output.read_text(encoding="utf-8") if output.exists() else "" + temporary.cleanup() + return result, contents + + def test_accepts_exact_observe_only_snapshot(self): + result, output = self.run_validator(telemetry()) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("\t4\t33566720\t0\t1\t16777216\t0\t", output) + + def test_rejects_manifest_runtime_abi_mismatch(self): + result, _ = self.run_validator(telemetry(), manifest_abi="34" * 32) + self.assertNotEqual(result.returncode, 0) + self.assertIn("runtime-abi-id differs", result.stderr) + + def test_rejects_non_retain_disposition(self): + value = telemetry() + value["classes"][5]["disposition"] = "dont-need" + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("did not retain", result.stderr) + + def test_rejects_validation_error_and_count_mismatch(self): + value = telemetry() + value["validation"]["controller-error"] = 1 + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("controller-error", result.stderr) + value = telemetry() + value["validation"]["valid"] = 5 + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("counts differ", result.stderr) + + def test_rejects_reclaim_eligibility_on_non_wal_class(self): + value = telemetry() + value["classes"][0]["reclaim-eligible-calls"] = 1 + value["classes"][0]["reclaim-eligible-finite-bytes"] = 4096 + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("non-WAL", result.stderr) + + def test_rejects_reclaim_eligibility_exceeding_wal_offers(self): + value = telemetry() + value["classes"][5]["reclaim-eligible-calls"] = 3 + result, _ = self.run_validator(value) + self.assertNotEqual(result.returncode, 0) + self.assertIn("exceed calls", result.stderr) + + def test_rejects_duplicate_json_field(self): + value = telemetry() + raw = json.dumps(value)[:-1] + ',"schema":"duplicate"}\n' + result, _ = self.run_validator(value, raw=raw) + self.assertNotEqual(result.returncode, 0) + self.assertIn("duplicate JSON field", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-host-fd-churn.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-host-fd-churn.sh new file mode 100755 index 00000000..1fc15f1c --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-host-fd-churn.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + echo "usage: ${0##*/} CHECKPOINTS.tsv TARGET MODE ALLOWANCE OUTPUT.tsv" >&2 + exit 64 +} + +[ "$#" -eq 5 ] || usage +checkpoints="$1" +target="$2" +mode="$3" +allowance="$4" +output="$5" + +case "$allowance" in ""|*[!0-9]*) echo "FD allowance must be a nonnegative integer" >&2; exit 64 ;; esac +[ -f "$checkpoints" ] && [ ! -L "$checkpoints" ] || { + printf 'host FD checkpoints are not a regular non-symlink file: %s\n' "$checkpoints" >&2 + exit 1 +} +[ ! -e "$output" ] && [ ! -L "$output" ] || { + printf 'refusing to replace host FD churn summary: %s\n' "$output" >&2 + exit 2 +} +[ -d "$(dirname "$output")" ] || exit 2 + +pending="$(mktemp "$(dirname "$output")/.host-fd-churn.XXXXXX")" +cleanup() { rm -f -- "$pending"; } +trap cleanup EXIT HUP INT TERM + +set +e +awk -F '\t' -v OFS='\t' -v wanted_target="$target" -v wanted_mode="$mode" \ + -v allowance="$allowance" ' + NR == 1 { + expected = "target\tmode\tstage\tmonotonic_ms\ttotal_open_fds\tobserved_processes\texpected_processes\tstatus" + if ($0 != expected) malformed = 1 + next + } + $1 == wanted_target && $2 == wanted_mode { + if (NF != 8 || ($3 != "before" && $3 != "after" && $3 != "quiescent") || + $4 !~ /^[0-9]+$/ || $5 !~ /^[0-9]+$/ || $6 !~ /^[0-9]+$/ || + $7 !~ /^[1-9][0-9]*$/ || $6 != $7 || $8 != "ok" || seen[$3]++) { + malformed = 1 + next + } + count++ + total[$3] = $5 + 0 + } + END { + if (count != 3 || malformed || !seen["before"] || !seen["after"] || !seen["quiescent"]) { + exit 2 + } + growth = total["quiescent"] - total["before"] + status = growth <= allowance ? "passed" : "failed" + print "target", "mode", "before_open_fds", "after_open_fds", "quiescent_open_fds", "quiescent_growth", "allowance", "status" + print wanted_target, wanted_mode, total["before"], total["after"], total["quiescent"], growth, allowance, status + exit status == "passed" ? 0 : 1 + } +' "$checkpoints" >"$pending" +status=$? +set -e +if [ "$status" -eq 2 ]; then + echo "host FD churn checkpoints are incomplete or malformed" >&2 + exit 1 +fi +mv "$pending" "$output" +trap - EXIT HUP INT TERM +exit "$status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-host-fd-churn.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-host-fd-churn.test.sh new file mode 100755 index 00000000..9fddd386 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-host-fd-churn.test.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root="$(mktemp -d)" +trap 'rm -rf -- "$root"' EXIT HUP INT TERM +validator="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/validate-host-fd-churn.sh" + +write_fixture() { + local path="$1" before="$2" after="$3" quiescent="$4" + { + printf 'target\tmode\tstage\tmonotonic_ms\ttotal_open_fds\tobserved_processes\texpected_processes\tstatus\n' + printf 'wasix\treconnect\tbefore\t1\t%s\t1\t1\tok\n' "$before" + printf 'wasix\treconnect\tafter\t2\t%s\t1\t1\tok\n' "$after" + printf 'wasix\treconnect\tquiescent\t3\t%s\t1\t1\tok\n' "$quiescent" + } >"$path" +} + +write_fixture "$root/no-leak.tsv" 40 43 41 +"$validator" "$root/no-leak.tsv" wasix reconnect 2 "$root/no-leak-summary.tsv" +grep -Fq $'wasix\treconnect\t40\t43\t41\t1\t2\tpassed' "$root/no-leak-summary.tsv" + +# This is the exact shape of one retained host FD per reconnect sample. +write_fixture "$root/one-per-sample.tsv" 40 1040 1040 +if "$validator" "$root/one-per-sample.tsv" wasix reconnect 4 "$root/leak-summary.tsv"; then + echo "host FD churn validator accepted a one-FD-per-sample leak" >&2 + exit 1 +fi +grep -Fq $'wasix\treconnect\t40\t1040\t1040\t1000\t4\tfailed' "$root/leak-summary.tsv" + +sed 's/\tok$/\traced/' "$root/no-leak.tsv" >"$root/raced.tsv" +if "$validator" "$root/raced.tsv" wasix reconnect 4 "$root/raced-summary.tsv" >/dev/null 2>&1; then + echo "host FD churn validator accepted raced observations" >&2 + exit 1 +fi + +printf 'host FD churn validation tests passed\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-memory-evidence.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-memory-evidence.sh new file mode 100755 index 00000000..36ddbc40 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-memory-evidence.sh @@ -0,0 +1,518 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: validate-memory-evidence.sh --samples FILE --target NAME + --interval-seconds N --require-phase NAME [--require-phase NAME ...] + --require-cgroup yes|no [--memory-max LIMIT] [--memory-high LIMIT] + [--swap-max LIMIT] + [--max-peak-pss-kib N] [--max-peak-pss-anon-kib N] + [--max-peak-page-table-kib N] + [--max-cgroup-high-events-delta N] + [--max-psi-some-stall-fraction F] + [--max-psi-full-stall-fraction F] + --output FILE + +Validate full-detail memory evidence. Required phases must have race-free smaps +and, when requested, complete cgroup-v2 samples with the exact configured +limits, zero swap use, and no memory.max/OOM events. Missing optional cgroup +files remain unavailable; they are never interpreted as zero. + +Timestamps must increase globally. Cadence gaps are gated between consecutive +samples of the same required phase; setup, checkpoint, backend preparation, +drain, and shutdown transitions are intentionally outside that claim. + +Performance budgets are optional and preserve the unbudgeted validation +contract when omitted. KiB limits apply to the global maximum over all valid +samples in the required phases. Cgroup high-event and PSI budgets sum the +first-to-last counter delta within each required phase; every such phase must +be one contiguous interval with at least two valid cgroup samples. PSI stall +fractions are counter-delta microseconds divided by monotonic elapsed +microseconds and must be between 0 and 1 inclusive. Requesting a cgroup budget +requires usable cgroup evidence even when --require-cgroup is no. +USAGE +} + +samples="" +target="" +interval_seconds="" +require_cgroup="" +memory_max="" +memory_high="" +swap_max="" +max_peak_pss_kib="" +max_peak_pss_anon_kib="" +max_peak_page_table_kib="" +max_cgroup_high_events_delta="" +max_psi_some_stall_fraction="" +max_psi_full_stall_fraction="" +output="" +required_phases=() + +while [ "$#" -gt 0 ]; do + case "$1" in + --samples) shift; [ "$#" -gt 0 ] || exit 2; samples="$1" ;; + --target) shift; [ "$#" -gt 0 ] || exit 2; target="$1" ;; + --interval-seconds) shift; [ "$#" -gt 0 ] || exit 2; interval_seconds="$1" ;; + --require-phase) shift; [ "$#" -gt 0 ] || exit 2; required_phases+=("$1") ;; + --require-cgroup) shift; [ "$#" -gt 0 ] || exit 2; require_cgroup="$1" ;; + --memory-max) shift; [ "$#" -gt 0 ] || exit 2; memory_max="$1" ;; + --memory-high) shift; [ "$#" -gt 0 ] || exit 2; memory_high="$1" ;; + --swap-max) shift; [ "$#" -gt 0 ] || exit 2; swap_max="$1" ;; + --max-peak-pss-kib) shift; [ "$#" -gt 0 ] && [ -n "$1" ] || exit 2; max_peak_pss_kib="$1" ;; + --max-peak-pss-anon-kib) shift; [ "$#" -gt 0 ] && [ -n "$1" ] || exit 2; max_peak_pss_anon_kib="$1" ;; + --max-peak-page-table-kib) shift; [ "$#" -gt 0 ] && [ -n "$1" ] || exit 2; max_peak_page_table_kib="$1" ;; + --max-cgroup-high-events-delta) shift; [ "$#" -gt 0 ] && [ -n "$1" ] || exit 2; max_cgroup_high_events_delta="$1" ;; + --max-psi-some-stall-fraction) shift; [ "$#" -gt 0 ] && [ -n "$1" ] || exit 2; max_psi_some_stall_fraction="$1" ;; + --max-psi-full-stall-fraction) shift; [ "$#" -gt 0 ] && [ -n "$1" ] || exit 2; max_psi_full_stall_fraction="$1" ;; + --output) shift; [ "$#" -gt 0 ] || exit 2; output="$1" ;; + -h|--help) usage; exit 0 ;; + *) printf 'unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +if [ -z "$samples" ] || [ ! -r "$samples" ]; then + echo '--samples must be readable' >&2 + exit 2 +fi +[ -n "$target" ] || { echo '--target is required' >&2; exit 2; } +if ! [[ "$interval_seconds" =~ ^([0-9]+([.][0-9]*)?|[.][0-9]+)$ ]] || + ! awk -v value="$interval_seconds" 'BEGIN { exit !(value > 0) }' +then + echo '--interval-seconds requires a positive number' >&2 + exit 2 +fi +case "$require_cgroup" in yes|no) ;; *) echo '--require-cgroup requires yes or no' >&2; exit 2 ;; esac +[ "${#required_phases[@]}" -gt 0 ] || { echo 'at least one --require-phase is required' >&2; exit 2; } +[ -n "$output" ] || { echo '--output is required' >&2; exit 2; } + +validate_unsigned_budget() { + local option="$1" + local value="$2" + [ -z "$value" ] && return 0 + case "$value" in + *[!0-9]*|"") printf '%s requires a nonnegative integer\n' "$option" >&2; exit 2 ;; + esac + awk -v value="$value" 'BEGIN { exit !(value <= 9007199254740991) }' || { + printf '%s exceeds the exact integer range supported by the validator\n' \ + "$option" >&2 + exit 2 + } +} + +validate_fraction_budget() { + local option="$1" + local value="$2" + [ -z "$value" ] && return 0 + if ! [[ "$value" =~ ^([0-9]+([.][0-9]*)?|[.][0-9]+)$ ]] || + ! awk -v value="$value" 'BEGIN { exit !(value >= 0 && value <= 1) }' + then + printf '%s requires a decimal fraction between 0 and 1 inclusive\n' \ + "$option" >&2 + exit 2 + fi +} + +validate_unsigned_budget --max-peak-pss-kib "$max_peak_pss_kib" +validate_unsigned_budget --max-peak-pss-anon-kib "$max_peak_pss_anon_kib" +validate_unsigned_budget --max-peak-page-table-kib "$max_peak_page_table_kib" +validate_unsigned_budget --max-cgroup-high-events-delta \ + "$max_cgroup_high_events_delta" +validate_fraction_budget --max-psi-some-stall-fraction \ + "$max_psi_some_stall_fraction" +validate_fraction_budget --max-psi-full-stall-fraction \ + "$max_psi_full_stall_fraction" + +smaps_budget_requested=0 +if [ -n "$max_peak_pss_kib$max_peak_pss_anon_kib$max_peak_page_table_kib" ]; then + smaps_budget_requested=1 +fi +cgroup_budget_requested=0 +if [ -n "$max_cgroup_high_events_delta$max_psi_some_stall_fraction$max_psi_full_stall_fraction" ]; then + cgroup_budget_requested=1 +fi + +normalize_limit() { + local value="$1" + awk -v value="$value" ' + BEGIN { + if (value == "") { print ""; exit } + if (value == "max" || value == "infinity") { print "max"; exit } + if (value !~ /^[0-9]+([KMGTPE](iB|B)?|B)?$/) exit 1 + number = value + sub(/[^0-9].*$/, "", number) + suffix = substr(value, length(number) + 1) + gsub(/[iIbB]/, "", suffix) + multiplier = 1 + if (suffix == "K") multiplier = 1024 + else if (suffix == "M") multiplier = 1024 * 1024 + else if (suffix == "G") multiplier = 1024 * 1024 * 1024 + else if (suffix == "T") multiplier = 1024 * 1024 * 1024 * 1024 + else if (suffix == "P") multiplier = 1024 * 1024 * 1024 * 1024 * 1024 + else if (suffix == "E") multiplier = 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + printf "%.0f\n", number * multiplier + } + ' +} + +normalized_memory_max="$(normalize_limit "$memory_max")" || { + printf 'unsupported --memory-max value: %s\n' "$memory_max" >&2; exit 2; +} +normalized_memory_high="$(normalize_limit "$memory_high")" || { + printf 'unsupported --memory-high value: %s\n' "$memory_high" >&2; exit 2; +} +normalized_swap_max="$(normalize_limit "$swap_max")" || { + printf 'unsupported --swap-max value: %s\n' "$swap_max" >&2; exit 2; +} +if [ "$require_cgroup" = yes ]; then + [ -n "$normalized_memory_max" ] || normalized_memory_max=max + [ -n "$normalized_memory_high" ] || normalized_memory_high=max + [ -n "$normalized_swap_max" ] || normalized_swap_max=max +fi + +phase_list="" +for phase in "${required_phases[@]}"; do + case "$phase" in + ""|*'|'*|*$'\t'*|*$'\n'*) echo 'invalid required phase' >&2; exit 2 ;; + esac + case "|$phase_list|" in *"|$phase|"*) echo "duplicate required phase: $phase" >&2; exit 2 ;; esac + if [ -n "$phase_list" ]; then phase_list="$phase_list|$phase"; else phase_list="$phase"; fi +done + +tmp_output="$output.tmp.$$" +trap 'rm -f -- "$tmp_output"' EXIT +set +e +detail="$(awk -F '\t' \ + -v expected_target="$target" \ + -v interval_ms="$(awk -v value="$interval_seconds" 'BEGIN { printf "%.0f", value * 1000 }')" \ + -v required="$phase_list" \ + -v require_cgroup="$require_cgroup" \ + -v expected_memory_max="$normalized_memory_max" \ + -v expected_memory_high="$normalized_memory_high" \ + -v expected_swap_max="$normalized_swap_max" \ + -v smaps_budget_requested="$smaps_budget_requested" \ + -v cgroup_budget_requested="$cgroup_budget_requested" \ + -v max_peak_pss_kib="$max_peak_pss_kib" \ + -v max_peak_pss_anon_kib="$max_peak_pss_anon_kib" \ + -v max_peak_page_table_kib="$max_peak_page_table_kib" \ + -v max_cgroup_high_events_delta="$max_cgroup_high_events_delta" \ + -v max_psi_some_stall_fraction="$max_psi_some_stall_fraction" \ + -v max_psi_full_stall_fraction="$max_psi_full_stall_fraction" ' + function fail(message) { + if (failure == "") failure = message + } + function unsigned(value) { return value ~ /^[0-9]+$/ } + function add_diagnostic(name, value) { + diagnostics = diagnostics ";" name "=" value + } + function observe_cumulative(name, value, row) { + if (!unsigned(value)) { + fail("cgroup-counter-unavailable:" name ":row-" row) + return 0 + } + if (have_cumulative[name] && (value + 0) < previous_cumulative[name]) + fail("cgroup-counter-decreased:" name ":row-" row) + previous_cumulative[name] = value + 0 + have_cumulative[name] = 1 + return 1 + } + BEGIN { + required_count = split(required, required_phase, "|") + for (i = 1; i <= required_count; i++) required_set[required_phase[i]] = 1 + max_gap_ms = interval_ms * 4 + 500 + + if (require_cgroup == "yes") { + cumulative_required["cgroup_scope_memory_peak_bytes"] = 1 + cumulative_required["cgroup_scope_swap_peak_bytes"] = 1 + cumulative_required["cgroup_scope_event_max_total"] = 1 + cumulative_required["cgroup_scope_event_oom_total"] = 1 + cumulative_required["cgroup_scope_event_oom_kill_total"] = 1 + cumulative_required["cgroup_memory_pressure_some_total_usec"] = 1 + cumulative_required["cgroup_memory_pressure_full_total_usec"] = 1 + } + if (max_cgroup_high_events_delta != "") + cumulative_required["cgroup_scope_event_high_total"] = 1 + if (max_psi_some_stall_fraction != "") + cumulative_required["cgroup_memory_pressure_some_total_usec"] = 1 + if (max_psi_full_stall_fraction != "") + cumulative_required["cgroup_memory_pressure_full_total_usec"] = 1 + } + NR == 1 { + for (i = 1; i <= NF; i++) column[$i] = i + needed = "monotonic_ms target phase cgroup_path cgroup_swap_current_bytes cgroup_scope_swap_peak_bytes cgroup_scope_event_max_total cgroup_scope_event_oom_total cgroup_scope_event_oom_kill_total cgroup_memory_max cgroup_memory_high cgroup_swap_max cgroup_scope_memory_peak_bytes cgroup_memory_pressure_some_total_usec cgroup_memory_pressure_full_total_usec smaps_status cgroup_status" + if (max_peak_pss_kib != "") needed = needed " pss_kb_total" + if (max_peak_pss_anon_kib != "") needed = needed " pss_anon_kb_total" + if (max_peak_page_table_kib != "") needed = needed " page_table_kb_total" + if (max_cgroup_high_events_delta != "") + needed = needed " cgroup_scope_event_high_total" + split(needed, names, " ") + for (i in names) if (!(names[i] in column)) fail("missing-column:" names[i]) + next + } + { + rows++ + timestamp = $(column["monotonic_ms"]) + row_target = $(column["target"]) + phase = $(column["phase"]) + smaps_status = $(column["smaps_status"]) + cgroup_status = $(column["cgroup_status"]) + required_row = phase in required_set + if (!unsigned(timestamp)) fail("invalid-monotonic-ms:row-" NR) + if (row_target != expected_target) fail("target-mismatch:row-" NR) + if (have_timestamp) { + if ((timestamp + 0) <= previous_timestamp) fail("non-monotonic-cadence:row-" NR) + else if (required_row && previous_phase == phase && + (timestamp + 0) - previous_timestamp > max_gap_ms) + fail("cadence-gap:row-" NR ":" ((timestamp + 0) - previous_timestamp) "ms") + } + previous_timestamp = timestamp + 0 + previous_phase = phase + have_timestamp = 1 + if (cgroup_budget_requested) { + if (!required_row) { + if (active_budget_phase != "") { + closed_budget_phase[active_budget_phase] = 1 + active_budget_phase = "" + } + } else if (phase != active_budget_phase) { + if (closed_budget_phase[phase]) + fail("budget-phase-noncontiguous:" phase ":row-" NR) + if (active_budget_phase != "") + closed_budget_phase[active_budget_phase] = 1 + active_budget_phase = phase + } + } + if (!required_row) next + phase_rows[phase]++ + if (smaps_status != "ok") fail("smaps-" smaps_status ":" phase ":row-" NR) + else { + phase_smaps_ok[phase]++ + if (max_peak_pss_kib != "") { + value = $(column["pss_kb_total"]) + if (!unsigned(value)) { + fail("smaps-counter-unavailable:pss_kb_total:row-" NR) + invalid_peak_pss = 1 + } + else if (!have_peak_pss || (value + 0) > peak_pss_kib) { + peak_pss_kib = value + 0 + have_peak_pss = 1 + } + } + if (max_peak_pss_anon_kib != "") { + value = $(column["pss_anon_kb_total"]) + if (!unsigned(value)) { + fail("smaps-counter-unavailable:pss_anon_kb_total:row-" NR) + invalid_peak_pss_anon = 1 + } + else if (!have_peak_pss_anon || (value + 0) > peak_pss_anon_kib) { + peak_pss_anon_kib = value + 0 + have_peak_pss_anon = 1 + } + } + if (max_peak_page_table_kib != "") { + value = $(column["page_table_kb_total"]) + if (!unsigned(value)) { + fail("smaps-counter-unavailable:page_table_kb_total:row-" NR) + invalid_peak_page_table = 1 + } + else if (!have_peak_page_table || (value + 0) > peak_page_table_kib) { + peak_page_table_kib = value + 0 + have_peak_page_table = 1 + } + } + } + if (require_cgroup == "yes" || cgroup_budget_requested) { + if (cgroup_status != "ok") { + fail("cgroup-" cgroup_status ":" phase ":row-" NR) + next + } + phase_cgroup_ok[phase]++ + row_cgroup_budget_valid = 1 + path = $(column["cgroup_path"]) + if (path == "") { + fail("cgroup-path-missing:" phase ":row-" NR) + row_cgroup_budget_valid = 0 + } + if (cgroup_path == "") cgroup_path = path + else if (path != cgroup_path) { + fail("cgroup-path-changed:row-" NR) + row_cgroup_budget_valid = 0 + } + if (require_cgroup == "yes") { + memory_max = $(column["cgroup_memory_max"]) + memory_high = $(column["cgroup_memory_high"]) + swap_max = $(column["cgroup_swap_max"]) + if (memory_max != expected_memory_max) fail("memory-max-mismatch:row-" NR) + if (memory_high != expected_memory_high) fail("memory-high-mismatch:row-" NR) + if (swap_max != expected_swap_max) fail("swap-max-mismatch:row-" NR) + swap_current = $(column["cgroup_swap_current_bytes"]) + swap_peak = $(column["cgroup_scope_swap_peak_bytes"]) + event_max = $(column["cgroup_scope_event_max_total"]) + event_oom = $(column["cgroup_scope_event_oom_total"]) + event_oom_kill = $(column["cgroup_scope_event_oom_kill_total"]) + if (!unsigned(swap_current) || swap_current != 0) + fail("swap-current-nonzero:row-" NR) + if (!unsigned(swap_peak) || swap_peak != 0) + fail("swap-peak-nonzero:row-" NR) + if (!unsigned(event_max) || event_max != 0) + fail("memory-max-event:row-" NR) + if (!unsigned(event_oom) || event_oom != 0) + fail("memory-oom-event:row-" NR) + if (!unsigned(event_oom_kill) || event_oom_kill != 0) + fail("memory-oom-kill-event:row-" NR) + } + for (name in cumulative_required) { + if (!observe_cumulative(name, $(column[name]), NR)) + row_cgroup_budget_valid = 0 + } + + if (cgroup_budget_requested && row_cgroup_budget_valid) { + phase_cgroup_budget_samples[phase]++ + if (!have_phase_cgroup_budget[phase]) { + phase_first_ms[phase] = timestamp + 0 + if (max_cgroup_high_events_delta != "") + phase_first_high[phase] = $(column["cgroup_scope_event_high_total"]) + 0 + if (max_psi_some_stall_fraction != "") + phase_first_psi_some[phase] = $(column["cgroup_memory_pressure_some_total_usec"]) + 0 + if (max_psi_full_stall_fraction != "") + phase_first_psi_full[phase] = $(column["cgroup_memory_pressure_full_total_usec"]) + 0 + have_phase_cgroup_budget[phase] = 1 + } + phase_last_ms[phase] = timestamp + 0 + if (max_cgroup_high_events_delta != "") + phase_last_high[phase] = $(column["cgroup_scope_event_high_total"]) + 0 + if (max_psi_some_stall_fraction != "") + phase_last_psi_some[phase] = $(column["cgroup_memory_pressure_some_total_usec"]) + 0 + if (max_psi_full_stall_fraction != "") + phase_last_psi_full[phase] = $(column["cgroup_memory_pressure_full_total_usec"]) + 0 + } + } + } + END { + if (NR < 2) fail("no-samples") + for (i = 1; i <= required_count; i++) { + phase = required_phase[i] + if (!phase_rows[phase]) fail("phase-missing:" phase) + else if (!phase_smaps_ok[phase]) fail("phase-without-valid-smaps:" phase) + if (require_cgroup == "yes" && !phase_cgroup_ok[phase]) + fail("phase-without-valid-cgroup:" phase) + } + + if (smaps_budget_requested) + add_diagnostic("budget.scope", "required-phases") + if (max_peak_pss_kib != "") { + add_diagnostic("budget.max_peak_pss_kib", max_peak_pss_kib) + if (invalid_peak_pss || !have_peak_pss) { + fail("budget-observation-unavailable:peak-pss-kib") + add_diagnostic("observed.peak_pss_kib", "unavailable") + } else { + add_diagnostic("observed.peak_pss_kib", sprintf("%.0f", peak_pss_kib)) + if (peak_pss_kib > (max_peak_pss_kib + 0)) + fail("budget-exceeded:peak-pss-kib:observed-" sprintf("%.0f", peak_pss_kib) ":limit-" max_peak_pss_kib) + } + } + if (max_peak_pss_anon_kib != "") { + add_diagnostic("budget.max_peak_pss_anon_kib", max_peak_pss_anon_kib) + if (invalid_peak_pss_anon || !have_peak_pss_anon) { + fail("budget-observation-unavailable:peak-pss-anon-kib") + add_diagnostic("observed.peak_pss_anon_kib", "unavailable") + } else { + add_diagnostic("observed.peak_pss_anon_kib", sprintf("%.0f", peak_pss_anon_kib)) + if (peak_pss_anon_kib > (max_peak_pss_anon_kib + 0)) + fail("budget-exceeded:peak-pss-anon-kib:observed-" sprintf("%.0f", peak_pss_anon_kib) ":limit-" max_peak_pss_anon_kib) + } + } + if (max_peak_page_table_kib != "") { + add_diagnostic("budget.max_peak_page_table_kib", max_peak_page_table_kib) + if (invalid_peak_page_table || !have_peak_page_table) { + fail("budget-observation-unavailable:peak-page-table-kib") + add_diagnostic("observed.peak_page_table_kib", "unavailable") + } else { + add_diagnostic("observed.peak_page_table_kib", sprintf("%.0f", peak_page_table_kib)) + if (peak_page_table_kib > (max_peak_page_table_kib + 0)) + fail("budget-exceeded:peak-page-table-kib:observed-" sprintf("%.0f", peak_page_table_kib) ":limit-" max_peak_page_table_kib) + } + } + + cgroup_budget_evaluable = cgroup_budget_requested + if (cgroup_budget_requested) { + if (!smaps_budget_requested) + add_diagnostic("budget.scope", "required-phases") + for (i = 1; i <= required_count; i++) { + phase = required_phase[i] + if (phase_cgroup_budget_samples[phase] < 2 || + !have_phase_cgroup_budget[phase] || + phase_last_ms[phase] <= phase_first_ms[phase]) { + fail("budget-cgroup-interval-unavailable:" phase) + cgroup_budget_evaluable = 0 + continue + } + elapsed_ms = phase_last_ms[phase] - phase_first_ms[phase] + total_budget_elapsed_ms += elapsed_ms + if (max_cgroup_high_events_delta != "") + total_high_events_delta += phase_last_high[phase] - phase_first_high[phase] + if (max_psi_some_stall_fraction != "") + total_psi_some_delta_usec += phase_last_psi_some[phase] - phase_first_psi_some[phase] + if (max_psi_full_stall_fraction != "") + total_psi_full_delta_usec += phase_last_psi_full[phase] - phase_first_psi_full[phase] + } + if (cgroup_budget_evaluable) { + elapsed_usec = total_budget_elapsed_ms * 1000 + add_diagnostic("observed.cgroup_budget_elapsed_ms", sprintf("%.0f", total_budget_elapsed_ms)) + if (max_cgroup_high_events_delta != "") { + add_diagnostic("budget.max_cgroup_high_events_delta", max_cgroup_high_events_delta) + add_diagnostic("observed.cgroup_high_events_delta", sprintf("%.0f", total_high_events_delta)) + if (total_high_events_delta > (max_cgroup_high_events_delta + 0)) + fail("budget-exceeded:cgroup-high-events-delta:observed-" sprintf("%.0f", total_high_events_delta) ":limit-" max_cgroup_high_events_delta) + } + if (max_psi_some_stall_fraction != "") { + psi_some_fraction = total_psi_some_delta_usec / elapsed_usec + add_diagnostic("budget.max_psi_some_stall_fraction", max_psi_some_stall_fraction) + add_diagnostic("observed.psi_some_delta_usec", sprintf("%.0f", total_psi_some_delta_usec)) + add_diagnostic("observed.psi_some_stall_fraction", sprintf("%.9f", psi_some_fraction)) + if (total_psi_some_delta_usec > elapsed_usec) + fail("psi-some-stall-fraction-out-of-range") + else if (total_psi_some_delta_usec > (max_psi_some_stall_fraction + 0) * elapsed_usec) + fail("budget-exceeded:psi-some-stall-fraction:observed-" sprintf("%.9f", psi_some_fraction) ":limit-" max_psi_some_stall_fraction) + } + if (max_psi_full_stall_fraction != "") { + psi_full_fraction = total_psi_full_delta_usec / elapsed_usec + add_diagnostic("budget.max_psi_full_stall_fraction", max_psi_full_stall_fraction) + add_diagnostic("observed.psi_full_delta_usec", sprintf("%.0f", total_psi_full_delta_usec)) + add_diagnostic("observed.psi_full_stall_fraction", sprintf("%.9f", psi_full_fraction)) + if (total_psi_full_delta_usec > elapsed_usec) + fail("psi-full-stall-fraction-out-of-range") + else if (total_psi_full_delta_usec > (max_psi_full_stall_fraction + 0) * elapsed_usec) + fail("budget-exceeded:psi-full-stall-fraction:observed-" sprintf("%.9f", psi_full_fraction) ":limit-" max_psi_full_stall_fraction) + } + } else { + if (max_cgroup_high_events_delta != "") + add_diagnostic("budget.max_cgroup_high_events_delta", max_cgroup_high_events_delta) + if (max_psi_some_stall_fraction != "") + add_diagnostic("budget.max_psi_some_stall_fraction", max_psi_some_stall_fraction) + if (max_psi_full_stall_fraction != "") + add_diagnostic("budget.max_psi_full_stall_fraction", max_psi_full_stall_fraction) + add_diagnostic("observed.cgroup_budget_interval", "unavailable") + } + } + + if (failure != "") { print failure diagnostics; exit 1 } + print "validated-full-memory-evidence" diagnostics + } +' "$samples")" +validation_status=$? +set -e + +detail="$(printf '%s' "$detail" | tr '\t\r\n' ' ')" +if [ "$validation_status" -eq 0 ]; then status=passed; else status=failed; fi +sample_count="$(awk 'END { print (NR > 0 ? NR - 1 : 0) }' "$samples")" +{ + printf 'target\tstatus\tdetail\tsamples\n' + printf '%s\t%s\t%s\t%s\n' "$target" "$status" "$detail" "$sample_count" +} >"$tmp_output" +mv "$tmp_output" "$output" +trap - EXIT +exit "$validation_status" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-memory-evidence.test.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-memory-evidence.test.sh new file mode 100755 index 00000000..4603178b --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-memory-evidence.test.sh @@ -0,0 +1,255 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +validator="$root/bin/validate-memory-evidence.sh" +tmp="$(mktemp -d)" +trap 'rm -rf -- "$tmp"' EXIT + +header='monotonic_ms target phase pss_kb_total pss_anon_kb_total page_table_kb_total cgroup_path cgroup_swap_current_bytes cgroup_scope_swap_peak_bytes cgroup_scope_event_high_total cgroup_scope_event_max_total cgroup_scope_event_oom_total cgroup_scope_event_oom_kill_total cgroup_memory_max cgroup_memory_high cgroup_swap_max cgroup_scope_memory_peak_bytes cgroup_memory_pressure_some_total_usec cgroup_memory_pressure_full_total_usec smaps_status cgroup_status' + +write_fixture() { + local destination="$1" + local second_smaps="${2:-ok}" + local second_cgroup="${3:-ok}" + local second_swap="${4:-0}" + local second_event_max="${5:-0}" + local second_memory_max="${6:-268435456}" + local second_time="${7:-1500}" + local phase="${8:-fanout:indexed-read}" + printf '%b\n' "$header" >"$destination" + printf '1000\twasix\tfanout:indexed-read\t1000\t700\t50\t/user.slice/test.scope\t0\t0\t10\t0\t0\t0\t268435456\t134217728\t0\t1000000\t1000\t500\tok\tok\n' \ + >>"$destination" + printf '%s\twasix\t%s\t1200\t800\t60\t/user.slice/test.scope\t%s\t0\t15\t%s\t0\t0\t%s\t134217728\t0\t1100000\t51000\t20500\t%s\t%s\n' \ + "$second_time" "$phase" "$second_swap" "$second_event_max" \ + "$second_memory_max" "$second_smaps" "$second_cgroup" >>"$destination" +} + +common_args=( + --target wasix + --interval-seconds 0.5 + --require-phase fanout:indexed-read + --require-cgroup yes + --memory-max 256M + --memory-high 128M + --swap-max 0 +) + +run_valid() { + "$validator" --samples "$1" "${common_args[@]}" --output "$2" + grep -Fq $'wasix\tpassed\tvalidated-full-memory-evidence\t2' "$2" +} + +expect_failure() { + local fixture="$1" + local expected="$2" + if "$validator" --samples "$fixture" "${common_args[@]}" \ + --output "$tmp/failure.out.tsv" + then + printf 'expected validation failure for %s\n' "$fixture" >&2 + exit 1 + fi + grep -Fq "$expected" "$tmp/failure.out.tsv" +} + +expect_budget_failure() { + local expected="$1" + shift + if "$validator" --samples "$tmp/valid.tsv" "${common_args[@]}" "$@" \ + --output "$tmp/budget-failure.out.tsv" + then + printf 'expected budget validation failure containing %s\n' "$expected" >&2 + exit 1 + fi + grep -Fq "$expected" "$tmp/budget-failure.out.tsv" +} + +write_fixture "$tmp/valid.tsv" +run_valid "$tmp/valid.tsv" "$tmp/valid.out.tsv" + +# A fully specified budget above every observation passes and records explicit +# units, limits, elapsed time, counter deltas, and derived fractions. +"$validator" --samples "$tmp/valid.tsv" "${common_args[@]}" \ + --max-peak-pss-kib 1300 \ + --max-peak-pss-anon-kib 900 \ + --max-peak-page-table-kib 70 \ + --max-cgroup-high-events-delta 6 \ + --max-psi-some-stall-fraction 0.11 \ + --max-psi-full-stall-fraction 0.05 \ + --output "$tmp/budget-positive.out.tsv" +grep -Fq 'budget.max_peak_pss_kib=1300;observed.peak_pss_kib=1200' \ + "$tmp/budget-positive.out.tsv" +grep -Fq 'observed.cgroup_budget_elapsed_ms=500' "$tmp/budget-positive.out.tsv" +grep -Fq 'observed.cgroup_high_events_delta=5' "$tmp/budget-positive.out.tsv" +grep -Fq 'observed.psi_some_delta_usec=50000;observed.psi_some_stall_fraction=0.100000000' \ + "$tmp/budget-positive.out.tsv" +grep -Fq 'observed.psi_full_delta_usec=20000;observed.psi_full_stall_fraction=0.040000000' \ + "$tmp/budget-positive.out.tsv" + +# Every budget is inclusive at its exact boundary. +"$validator" --samples "$tmp/valid.tsv" "${common_args[@]}" \ + --max-peak-pss-kib 1200 \ + --max-peak-pss-anon-kib 800 \ + --max-peak-page-table-kib 60 \ + --max-cgroup-high-events-delta 5 \ + --max-psi-some-stall-fraction 0.1 \ + --max-psi-full-stall-fraction 0.04 \ + --output "$tmp/budget-boundary.out.tsv" +grep -Fq $'wasix\tpassed\tvalidated-full-memory-evidence;budget.scope=required-phases' \ + "$tmp/budget-boundary.out.tsv" + +expect_budget_failure 'budget-exceeded:peak-pss-kib:observed-1200:limit-1199' \ + --max-peak-pss-kib 1199 +expect_budget_failure 'budget-exceeded:peak-pss-anon-kib:observed-800:limit-799' \ + --max-peak-pss-anon-kib 799 +expect_budget_failure 'budget-exceeded:peak-page-table-kib:observed-60:limit-59' \ + --max-peak-page-table-kib 59 +expect_budget_failure 'budget-exceeded:cgroup-high-events-delta:observed-5:limit-4' \ + --max-cgroup-high-events-delta 4 +expect_budget_failure 'budget-exceeded:psi-some-stall-fraction:observed-0.100000000:limit-0.099' \ + --max-psi-some-stall-fraction 0.099 +expect_budget_failure 'budget-exceeded:psi-full-stall-fraction:observed-0.040000000:limit-0.039' \ + --max-psi-full-stall-fraction 0.039 + +# Required phases are aggregated independently. Pressure accumulated during an +# intervening non-required phase is not charged to either measured phase. +printf '%b\n' "$header" >"$tmp/multi-phase.tsv" +{ + printf '1000\twasix\tfanout:indexed-read\t1000\t700\t50\t/user.slice/test.scope\t0\t0\t10\t0\t0\t0\t268435456\t134217728\t0\t1000000\t1000\t500\tok\tok\n' + printf '1500\twasix\tfanout:indexed-read\t1200\t800\t60\t/user.slice/test.scope\t0\t0\t15\t0\t0\t0\t268435456\t134217728\t0\t1100000\t51000\t20500\tok\tok\n' + printf '2000\twasix\tidle\t1250\t825\t61\t/user.slice/test.scope\t0\t0\t100\t0\t0\t0\t268435456\t134217728\t0\t1150000\t200000\t100000\tok\tok\n' + printf '2500\twasix\tfanout:indexed-update\t1300\t850\t62\t/user.slice/test.scope\t0\t0\t100\t0\t0\t0\t268435456\t134217728\t0\t1200000\t200000\t100000\tok\tok\n' + printf '3000\twasix\tfanout:indexed-update\t1400\t900\t70\t/user.slice/test.scope\t0\t0\t102\t0\t0\t0\t268435456\t134217728\t0\t1250000\t225000\t110000\tok\tok\n' +} >>"$tmp/multi-phase.tsv" +"$validator" --samples "$tmp/multi-phase.tsv" --target wasix \ + --interval-seconds 0.5 \ + --require-phase fanout:indexed-read \ + --require-phase fanout:indexed-update \ + --require-cgroup yes --memory-max 256M --memory-high 128M --swap-max 0 \ + --max-peak-pss-kib 1400 \ + --max-cgroup-high-events-delta 7 \ + --max-psi-some-stall-fraction 0.075 \ + --max-psi-full-stall-fraction 0.03 \ + --output "$tmp/multi-phase.out.tsv" +grep -Fq 'observed.cgroup_budget_elapsed_ms=1000' "$tmp/multi-phase.out.tsv" +grep -Fq 'observed.cgroup_high_events_delta=7' "$tmp/multi-phase.out.tsv" +grep -Fq 'observed.psi_some_stall_fraction=0.075000000' "$tmp/multi-phase.out.tsv" +grep -Fq 'observed.psi_full_stall_fraction=0.030000000' "$tmp/multi-phase.out.tsv" + +printf '%b\n' "$header" >"$tmp/no-cgroup.tsv" +printf '1000\tnative\tfanout:indexed-read\t1000\t700\t50\t\t\t\t\t\t\t\t\t\t\t\t\t\tok\tdisabled\n' \ + >>"$tmp/no-cgroup.tsv" +"$validator" --samples "$tmp/no-cgroup.tsv" --target native \ + --interval-seconds 0.5 --require-phase fanout:indexed-read \ + --require-cgroup no --output "$tmp/no-cgroup.out.tsv" +grep -Fq $'native\tpassed\tvalidated-full-memory-evidence\t1' \ + "$tmp/no-cgroup.out.tsv" + +write_fixture "$tmp/missing-phase-source.tsv" ok ok 0 0 268435456 1500 idle +sed 's/fanout:indexed-read/idle/g' "$tmp/missing-phase-source.tsv" \ + >"$tmp/missing-phase.tsv" +expect_failure "$tmp/missing-phase.tsv" 'phase-missing:fanout:indexed-read' +write_fixture "$tmp/raced.tsv" raced +expect_failure "$tmp/raced.tsv" 'smaps-raced:fanout:indexed-read' +write_fixture "$tmp/unavailable.tsv" ok unavailable +expect_failure "$tmp/unavailable.tsv" 'cgroup-unavailable:fanout:indexed-read' +write_fixture "$tmp/swap.tsv" ok ok 4096 +expect_failure "$tmp/swap.tsv" 'swap-current-nonzero' +write_fixture "$tmp/max-event.tsv" ok ok 0 1 +expect_failure "$tmp/max-event.tsv" 'memory-max-event' +write_fixture "$tmp/limit.tsv" ok ok 0 0 999 +expect_failure "$tmp/limit.tsv" 'memory-max-mismatch' +write_fixture "$tmp/cadence.tsv" ok ok 0 0 268435456 4000 +expect_failure "$tmp/cadence.tsv" 'cadence-gap' + +# Dynamic preparation can legitimately take longer than the sampler cadence. +# The first required-phase row begins a new evidence interval rather than +# inheriting a setup/checkpoint gap that it could not observe. +printf '%b\n' "$header" >"$tmp/phase-transition-gap.tsv" +{ + printf '1000\twasix\tcheckpoint:indexed-read\t1000\t700\t50\t/user.slice/test.scope\t0\t0\t10\t0\t0\t0\t268435456\t134217728\t0\t1000000\t1000\t500\tok\tok\n' + printf '4000\twasix\tfanout:indexed-read\t1200\t800\t60\t/user.slice/test.scope\t0\t0\t15\t0\t0\t0\t268435456\t134217728\t0\t1100000\t51000\t20500\tok\tok\n' +} >>"$tmp/phase-transition-gap.tsv" +run_valid "$tmp/phase-transition-gap.tsv" "$tmp/phase-transition-gap.out.tsv" + +# A budget request independently requires its evidence source. This remains +# fail-closed even when the general cgroup contract was not requested. +if "$validator" --samples "$tmp/no-cgroup.tsv" --target native \ + --interval-seconds 0.5 --require-phase fanout:indexed-read \ + --require-cgroup no --max-cgroup-high-events-delta 0 \ + --output "$tmp/budget-unavailable-cgroup.out.tsv" +then + echo 'expected unavailable cgroup budget failure' >&2 + exit 1 +fi +grep -Fq 'cgroup-disabled:fanout:indexed-read' \ + "$tmp/budget-unavailable-cgroup.out.tsv" + +head -n 2 "$tmp/valid.tsv" >"$tmp/one-cgroup-sample.tsv" +if "$validator" --samples "$tmp/one-cgroup-sample.tsv" "${common_args[@]}" \ + --max-psi-some-stall-fraction 1 \ + --output "$tmp/budget-unavailable-interval.out.tsv" +then + echo 'expected unavailable cgroup interval failure' >&2 + exit 1 +fi +grep -Fq 'budget-cgroup-interval-unavailable:fanout:indexed-read' \ + "$tmp/budget-unavailable-interval.out.tsv" + +awk -F '\t' -v OFS='\t' ' + NR == 1 { for (i = 1; i <= NF; i++) column[$i] = i } + NR == 2 { $(column["pss_kb_total"]) = "" } + { print } +' "$tmp/valid.tsv" >"$tmp/missing-pss.tsv" +if "$validator" --samples "$tmp/missing-pss.tsv" "${common_args[@]}" \ + --max-peak-pss-kib 2000 --output "$tmp/budget-unavailable-pss.out.tsv" +then + echo 'expected unavailable PSS budget failure' >&2 + exit 1 +fi +grep -Fq 'smaps-counter-unavailable:pss_kb_total' \ + "$tmp/budget-unavailable-pss.out.tsv" + +awk -F '\t' -v OFS='\t' ' + NR == 1 { for (i = 1; i <= NF; i++) column[$i] = i } + NR == 2 { $(column["cgroup_memory_pressure_some_total_usec"]) = "" } + { print } +' "$tmp/valid.tsv" >"$tmp/missing-psi.tsv" +if "$validator" --samples "$tmp/missing-psi.tsv" --target wasix \ + --interval-seconds 0.5 --require-phase fanout:indexed-read \ + --require-cgroup no --max-psi-some-stall-fraction 1 \ + --output "$tmp/budget-unavailable-psi.out.tsv" +then + echo 'expected unavailable PSI counter failure' >&2 + exit 1 +fi +grep -Fq 'cgroup-counter-unavailable:cgroup_memory_pressure_some_total_usec' \ + "$tmp/budget-unavailable-psi.out.tsv" + +for invalid in -1 1.01 nan 1e-2; do + if "$validator" --samples "$tmp/valid.tsv" "${common_args[@]}" \ + --max-psi-some-stall-fraction "$invalid" \ + --output "$tmp/invalid-fraction.out.tsv" >/dev/null 2>&1 + then + printf 'expected invalid PSI fraction rejection: %s\n' "$invalid" >&2 + exit 1 + fi +done +if "$validator" --samples "$tmp/valid.tsv" "${common_args[@]}" \ + --max-peak-pss-kib 1.5 --output "$tmp/invalid-kib.out.tsv" >/dev/null 2>&1 +then + echo 'expected invalid KiB budget rejection' >&2 + exit 1 +fi +for invalid in '' 9007199254740992; do + if "$validator" --samples "$tmp/valid.tsv" "${common_args[@]}" \ + --max-cgroup-high-events-delta "$invalid" \ + --output "$tmp/invalid-high-events.out.tsv" >/dev/null 2>&1 + then + printf 'expected invalid high-event budget rejection: %s\n' "$invalid" >&2 + exit 1 + fi +done + +printf 'passed: memory evidence validation\n' diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-sealed-loader-audit.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-sealed-loader-audit.py new file mode 100755 index 00000000..df82c266 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-sealed-loader-audit.py @@ -0,0 +1,756 @@ +#!/usr/bin/env python3 + +"""Validate direct immutable activation evidence emitted by sealed Wasmer.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import stat +import sys +from pathlib import Path +from typing import Any + + +SCHEMA = "oliphaunt.wasix-postmaster.sealed-loader-receipt.v2" +SUMMARY_SCHEMA = "oliphaunt.wasix-postmaster.attested-start-runtime-summary.v1" +MEMORY_IMAGE_SCHEMA = "oliphaunt.wasix-postmaster.memory-image.v2" +DETERMINISTIC_START_PROOF_SCHEMA = ( + "oliphaunt.wasix-postmaster.deterministic-start-proof.v1" +) +RESULT_SCHEMA = "oliphaunt.wasix-postmaster.sealed-loader-audit-validation.v3" +MAX_U64 = (1 << 64) - 1 +SHA256_RE = re.compile(r"[0-9a-f]{64}\Z") +FIELDS = { + "schema", + "pid", + "artifact_kind", + "module_sha256", + "snapshot_mode", + "logical_bytes", + "source_bytes_read", + "source_bytes_written", + "snapshot_bytes_written", + "mapping_bytes_hashed", + "sync_calls", + "read_advice_applicable", + "read_advice_supported", + "read_advice_calls", + "read_advice_successes", + "read_advice_first_errno", + "source_cache_eviction_applicable", + "source_cache_eviction_supported", + "source_cache_eviction_calls", + "source_cache_eviction_successes", + "source_cache_eviction_errno", + "snapshot_cache_eviction_applicable", + "snapshot_cache_eviction_supported", + "snapshot_cache_eviction_calls", + "snapshot_cache_eviction_successes", + "snapshot_cache_eviction_errno", + "mapping_cache_eviction_applicable", + "mapping_cache_eviction_supported", + "mapping_cache_eviction_calls", + "mapping_cache_eviction_successes", + "mapping_cache_eviction_errno", + "residency_after_hash_inspect", + "residency_after_archive_release", + "source_residency_before_eviction", + "source_residency_after_eviction", + "residency_after_eviction", + "write_policy", +} +SUMMARY_FIELDS = { + "schema", + "pid", + "artifact_kind", + "terminal", + "module_sha256", + "memory_image_schema", + "proof_sha256", + "proof_output_sha256", + "mapped_size", + "ordinary_start_completed_instances", + "fresh_zeroed_instances", + "nonfresh_instances", + "validation_attempts", + "full_compare_attempts", + "full_compare_successes", + "full_compare_failures", + "compared_bytes", + "reuse_successes", + "reuse_failures", + "skipped_bytes", + "remap_successes", + "remap_failures", + "counter_overflow", +} +SUMMARY_COUNTER_FIELDS = ( + "ordinary_start_completed_instances", + "fresh_zeroed_instances", + "nonfresh_instances", + "validation_attempts", + "full_compare_attempts", + "full_compare_successes", + "full_compare_failures", + "compared_bytes", + "reuse_successes", + "reuse_failures", + "skipped_bytes", + "remap_successes", + "remap_failures", +) +RESIDENCY_FIELDS = { + "state", + "page_size", + "total_pages", + "resident_pages", + "resident_bytes", + "errno", +} +DIRECT_MODES = {"direct-immutable-inode", "direct-read-only-filesystem"} +EXPECTED_ARTIFACT_NAMES = { + "runtime:initdb", + "runtime:postgres", + "runtime:libpq.so.5.18", + "runtime:dict_snowball.so", + "runtime:plpgsql.so", +} +EXECUTABLE_NAMES = ("runtime:initdb", "runtime:postgres") + + +class ValidationError(Exception): + pass + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ValidationError(message) + + +def duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + require(key not in result, f"duplicate JSON field: {key}") + result[key] = value + return result + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def read_regular(path: Path, label: str) -> bytes: + before = os.lstat(path) + require(stat.S_ISREG(before.st_mode) and not stat.S_ISLNK(before.st_mode), f"{label} must be a regular non-symlink file") + descriptor = os.open( + path, + os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0), + ) + try: + opened = os.fstat(descriptor) + require((before.st_dev, before.st_ino) == (opened.st_dev, opened.st_ino), f"{label} changed while opening") + chunks: list[bytes] = [] + while True: + chunk = os.read(descriptor, 1024 * 1024) + if not chunk: + break + chunks.append(chunk) + after = os.fstat(descriptor) + require( + (opened.st_dev, opened.st_ino, opened.st_size, opened.st_mtime_ns) + == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns), + f"{label} changed while reading", + ) + return b"".join(chunks) + finally: + os.close(descriptor) + + +def manifest_module_evidence( + manifest_data: bytes, +) -> tuple[dict[str, str], dict[str, dict[str, Any]]]: + try: + manifest = json.loads(manifest_data.decode("utf-8"), object_pairs_hook=duplicate_keys) + except (UnicodeDecodeError, json.JSONDecodeError, ValidationError) as error: + raise ValidationError(f"invalid sealed manifest: {error}") from error + require(isinstance(manifest, dict), "sealed manifest must be an object") + artifacts = manifest.get("artifacts") + require(isinstance(artifacts, list) and len(artifacts) == 5, "sealed manifest artifact closure differs") + modules: dict[str, str] = {} + attested_memory: dict[str, dict[str, Any]] = {} + for artifact in artifacts: + require(isinstance(artifact, dict), "sealed manifest artifact must be an object") + name = artifact.get("name") + require(isinstance(name, str) and name in EXPECTED_ARTIFACT_NAMES, "sealed manifest artifact closure differs") + module_hash = artifact.get("module-sha256") + require(isinstance(module_hash, str) and SHA256_RE.fullmatch(module_hash), f"invalid module SHA-256 for {name}") + require(name not in modules, f"duplicate manifest artifact: {name}") + require(module_hash not in modules.values(), f"duplicate manifest module hash: {module_hash}") + modules[name] = module_hash + if name not in EXECUTABLE_NAMES: + continue + memory = artifact.get("preinitialized-memory") + require(isinstance(memory, dict), f"missing preinitialized-memory metadata for {name}") + require( + memory.get("schema") == MEMORY_IMAGE_SCHEMA, + f"preinitialized-memory schema differs for {name}", + ) + require( + memory.get("module-sha256") == module_hash, + f"preinitialized-memory module SHA-256 differs for {name}", + ) + mapped_size = memory.get("mapped-size") + require( + type(mapped_size) is int and 0 < mapped_size <= MAX_U64, + f"preinitialized-memory mapped size is invalid for {name}", + ) + proof = memory.get("deterministic-start-proof") + require(isinstance(proof, dict), f"missing deterministic-start proof for {name}") + require( + proof.get("schema") == DETERMINISTIC_START_PROOF_SCHEMA, + f"deterministic-start proof schema differs for {name}", + ) + require( + proof.get("module-sha256") == module_hash, + f"deterministic-start proof module SHA-256 differs for {name}", + ) + proof_sha256 = proof.get("proof-sha256") + proof_output_sha256 = memory.get("deterministic-start-proof-output-sha256") + require( + isinstance(proof_sha256, str) and SHA256_RE.fullmatch(proof_sha256), + f"deterministic-start proof SHA-256 is invalid for {name}", + ) + require( + isinstance(proof_output_sha256, str) + and SHA256_RE.fullmatch(proof_output_sha256), + f"deterministic-start proof output SHA-256 is invalid for {name}", + ) + attested_memory[module_hash] = { + "memory_image_schema": memory["schema"], + "proof_sha256": proof_sha256, + "proof_output_sha256": proof_output_sha256, + "mapped_size": mapped_size, + } + require(set(modules) == EXPECTED_ARTIFACT_NAMES, "sealed manifest artifact closure differs") + require( + set(attested_memory) == {modules[name] for name in EXECUTABLE_NAMES}, + "sealed manifest attested executable closure differs", + ) + return modules, attested_memory + + +def exact_nonnegative(value: Any, label: str) -> int: + require(type(value) is int and value >= 0, f"{label} must be a nonnegative integer") + return value + + +def exact_u64(value: Any, label: str) -> int: + require( + type(value) is int and 0 <= value <= MAX_U64, + f"{label} must be an unsigned 64-bit integer", + ) + return value + + +def exact_bool(value: Any, label: str) -> bool: + require(type(value) is bool, f"{label} must be a boolean") + return value + + +def validate_advice( + record: dict[str, Any], + *, + prefix: str, + errno_field: str, + expected_applicable: bool, + expected_calls: int, + line_number: int, +) -> tuple[int, int]: + label = f"{prefix} line {line_number}" + applicable = exact_bool(record[f"{prefix}_applicable"], f"{label} applicable") + supported = exact_bool(record[f"{prefix}_supported"], f"{label} supported") + calls = exact_nonnegative(record[f"{prefix}_calls"], f"{label} calls") + successes = exact_nonnegative(record[f"{prefix}_successes"], f"{label} successes") + errno = record[errno_field] + require(applicable is expected_applicable, f"{label} applicability differs") + require(successes <= calls, f"{label} successes exceed calls") + if not applicable: + require(calls == 0 and successes == 0 and errno is None, f"{label} issued an inapplicable call") + return calls, successes + require(supported, f"{label} is unsupported") + require(calls == expected_calls, f"{label} call count differs") + require(successes == calls, f"{label} advisory call failed") + require(errno is None, f"{label} success unexpectedly carries errno") + return calls, successes + + +def validate_residency( + value: Any, + *, + label: str, + logical_bytes: int, + expected_state: str, +) -> int: + require(isinstance(value, dict) and set(value) == RESIDENCY_FIELDS, f"{label} fields differ") + state = value["state"] + require(state == expected_state, f"{label} state differs: expected {expected_state}, got {state!r}") + if state == "not-applicable": + for field in RESIDENCY_FIELDS - {"state"}: + require(value[field] is None, f"{label} not-applicable field {field} must be null") + return 0 + + page_size = exact_nonnegative(value["page_size"], f"{label} page_size") + total_pages = exact_nonnegative(value["total_pages"], f"{label} total_pages") + resident_pages = exact_nonnegative(value["resident_pages"], f"{label} resident_pages") + resident_bytes = exact_nonnegative(value["resident_bytes"], f"{label} resident_bytes") + require(value["errno"] is None, f"{label} measured checkpoint carries errno") + require(page_size >= 512 and page_size & (page_size - 1) == 0, f"{label} page_size is invalid") + expected_pages = (logical_bytes + page_size - 1) // page_size + require(total_pages == expected_pages, f"{label} total_pages differs") + require(resident_pages <= total_pages, f"{label} resident_pages exceed total_pages") + if resident_pages == 0: + possible_bytes = {0} + else: + tail_bytes = logical_bytes - (total_pages - 1) * page_size + possible_bytes = { + min(logical_bytes, resident_pages * page_size), + (resident_pages - 1) * page_size + tail_bytes, + } + require(resident_bytes in possible_bytes, f"{label} resident byte/page accounting differs") + return resident_bytes + + +def parse_audit( + data: bytes, +) -> tuple[list[tuple[int, dict[str, Any]]], list[tuple[int, dict[str, Any]]]]: + require(data and data.endswith(b"\n"), "sealed loader audit must be nonempty and newline-terminated") + require(b"\r" not in data, "sealed loader audit contains a carriage return") + records: list[tuple[int, dict[str, Any]]] = [] + summaries: list[tuple[int, dict[str, Any]]] = [] + for line_number, raw in enumerate(data.splitlines(), 1): + try: + record = json.loads(raw.decode("utf-8"), object_pairs_hook=duplicate_keys) + except (UnicodeDecodeError, json.JSONDecodeError, ValidationError) as error: + raise ValidationError(f"invalid audit JSON on line {line_number}: {error}") from error + require(isinstance(record, dict), f"audit record must be an object on line {line_number}") + schema = record.get("schema") + require(isinstance(schema, str), f"audit schema is missing on line {line_number}") + if schema == SUMMARY_SCHEMA: + require(set(record) == SUMMARY_FIELDS, f"attested-start summary fields differ on line {line_number}") + require(type(record["pid"]) is int and record["pid"] > 0, f"invalid summary pid on line {line_number}") + require( + record["artifact_kind"] == "attested-start-runtime-summary", + f"invalid summary artifact kind on line {line_number}", + ) + require(record["terminal"] is True, f"attested-start summary is not terminal on line {line_number}") + for field in ("module_sha256", "proof_sha256", "proof_output_sha256"): + require( + isinstance(record[field], str) and SHA256_RE.fullmatch(record[field]), + f"invalid summary {field} on line {line_number}", + ) + require( + record["memory_image_schema"] == MEMORY_IMAGE_SCHEMA, + f"unknown summary memory image schema on line {line_number}", + ) + require( + exact_u64(record["mapped_size"], f"summary mapped_size line {line_number}") > 0, + f"summary mapped_size must be positive on line {line_number}", + ) + for field in SUMMARY_COUNTER_FIELDS: + exact_u64(record[field], f"summary {field} line {line_number}") + exact_bool(record["counter_overflow"], f"summary counter_overflow line {line_number}") + summaries.append((line_number, record)) + continue + + require(schema == SCHEMA, f"unknown audit schema on line {line_number}: {schema!r}") + require(set(record) == FIELDS, f"loader audit fields differ on line {line_number}") + require(type(record["pid"]) is int and record["pid"] > 0, f"invalid audit pid on line {line_number}") + require(record["artifact_kind"] in ("aot", "preinitialized-memory"), f"invalid artifact kind on line {line_number}") + require(isinstance(record["module_sha256"], str) and SHA256_RE.fullmatch(record["module_sha256"]), f"invalid module SHA-256 on line {line_number}") + require(record["snapshot_mode"] in DIRECT_MODES, f"non-direct snapshot mode on line {line_number}") + logical = exact_nonnegative(record["logical_bytes"], f"logical_bytes line {line_number}") + require(logical > 0, f"logical_bytes must be positive on line {line_number}") + source_read = exact_nonnegative(record["source_bytes_read"], f"source_bytes_read line {line_number}") + require(source_read in (0, logical), f"source_bytes_read differs from direct-mode contract on line {line_number}") + require(exact_nonnegative(record["source_bytes_written"], f"source_bytes_written line {line_number}") == 0, f"source bytes were written on line {line_number}") + require(exact_nonnegative(record["snapshot_bytes_written"], f"snapshot_bytes_written line {line_number}") == 0, f"snapshot bytes were written on line {line_number}") + require(exact_nonnegative(record["mapping_bytes_hashed"], f"mapping_bytes_hashed line {line_number}") == logical, f"mapping hash coverage differs on line {line_number}") + require(exact_nonnegative(record["sync_calls"], f"sync_calls line {line_number}") == 0, f"loader issued sync calls on line {line_number}") + validate_advice( + record, + prefix="read_advice", + errno_field="read_advice_first_errno", + expected_applicable=True, + expected_calls=2, + line_number=line_number, + ) + validate_advice( + record, + prefix="source_cache_eviction", + errno_field="source_cache_eviction_errno", + expected_applicable=True, + expected_calls=1, + line_number=line_number, + ) + validate_advice( + record, + prefix="snapshot_cache_eviction", + errno_field="snapshot_cache_eviction_errno", + expected_applicable=False, + expected_calls=1, + line_number=line_number, + ) + validate_advice( + record, + prefix="mapping_cache_eviction", + errno_field="mapping_cache_eviction_errno", + expected_applicable=record["artifact_kind"] == "preinitialized-memory", + expected_calls=1, + line_number=line_number, + ) + validate_residency( + record["residency_after_hash_inspect"], + label=f"residency_after_hash_inspect line {line_number}", + logical_bytes=logical, + expected_state="measured", + ) + validate_residency( + record["residency_after_archive_release"], + label=f"residency_after_archive_release line {line_number}", + logical_bytes=logical, + expected_state=("measured" if record["artifact_kind"] == "aot" else "not-applicable"), + ) + validate_residency( + record["source_residency_before_eviction"], + label=f"source_residency_before_eviction line {line_number}", + logical_bytes=logical, + expected_state="measured", + ) + validate_residency( + record["source_residency_after_eviction"], + label=f"source_residency_after_eviction line {line_number}", + logical_bytes=logical, + expected_state="measured", + ) + validate_residency( + record["residency_after_eviction"], + label=f"residency_after_eviction line {line_number}", + logical_bytes=logical, + expected_state="measured", + ) + require(record["write_policy"] == "none-immutable-source", f"write policy differs on line {line_number}") + records.append((line_number, record)) + require(records, "sealed loader audit contains no loader receipts") + require(summaries, "sealed loader audit contains no attested-start summaries") + return records, summaries + + +def validate_attested_start_summary( + record: dict[str, Any], + expected: dict[str, Any], + *, + line_number: int, +) -> None: + label = f"attested-start summary line {line_number}" + for field in ( + "memory_image_schema", + "proof_sha256", + "proof_output_sha256", + "mapped_size", + ): + require(record[field] == expected[field], f"{label} {field} differs from sealed manifest") + + require(not record["counter_overflow"], f"{label} reports counter overflow") + ordinary_starts = record["ordinary_start_completed_instances"] + fresh_instances = record["fresh_zeroed_instances"] + nonfresh_instances = record["nonfresh_instances"] + validations = record["validation_attempts"] + compare_attempts = record["full_compare_attempts"] + compare_successes = record["full_compare_successes"] + compare_failures = record["full_compare_failures"] + reuse_successes = record["reuse_successes"] + reuse_failures = record["reuse_failures"] + remap_successes = record["remap_successes"] + remap_failures = record["remap_failures"] + mapped_size = record["mapped_size"] + + require(ordinary_starts > 0, f"{label} has no ordinary start completions") + require( + ordinary_starts == fresh_instances + nonfresh_instances, + f"{label} ordinary-start/fresh-memory conservation differs", + ) + require(nonfresh_instances == 0, f"{label} observed a non-fresh memory instance") + require( + validations == ordinary_starts, + f"{label} ordinary-start/validation conservation differs", + ) + require( + compare_attempts == compare_successes + compare_failures, + f"{label} full-compare conservation differs", + ) + require( + validations == compare_attempts + reuse_successes + reuse_failures, + f"{label} validation compare/reuse conservation differs", + ) + require(compare_attempts == 1, f"{label} did not perform exactly one full comparison") + require(compare_failures == 0, f"{label} reports a full comparison failure") + require(compare_successes == 1, f"{label} did not complete the first full comparison") + require(reuse_failures == 0, f"{label} reports a cached-validation failure") + require( + reuse_successes == ordinary_starts - 1, + f"{label} validation reuse count differs", + ) + + expected_compared_bytes = mapped_size * compare_successes + expected_skipped_bytes = mapped_size * reuse_successes + require( + expected_compared_bytes <= MAX_U64 and expected_skipped_bytes <= MAX_U64, + f"{label} byte-accounting product overflows u64", + ) + require( + record["compared_bytes"] == expected_compared_bytes, + f"{label} compared byte accounting differs", + ) + require( + record["skipped_bytes"] == expected_skipped_bytes, + f"{label} skipped byte accounting differs", + ) + + successful_validations = compare_successes + reuse_successes + require( + remap_successes + remap_failures == successful_validations, + f"{label} validation/remap conservation differs", + ) + require(remap_failures == 0, f"{label} reports a memory-image remap failure") + require( + remap_successes == ordinary_starts, + f"{label} successful remap count differs from ordinary starts", + ) + + +def validate( + audit: Path, + manifest: Path, + output: Path, + *, + required_snapshot_mode: str | None = None, + expected_initdb_executions: int = 1, + expected_postgres_executions: int = 1, +) -> None: + require(not os.path.lexists(output), f"validation output already exists: {output}") + require( + required_snapshot_mode is None or required_snapshot_mode in DIRECT_MODES, + "required snapshot mode is not a direct loader mode", + ) + require(expected_initdb_executions > 0, "expected initdb executions must be positive") + require(expected_postgres_executions > 0, "expected postgres executions must be positive") + audit_data = read_regular(audit, "sealed loader audit") + manifest_data = read_regular(manifest, "sealed manifest") + manifest_modules, manifest_attested_memory = manifest_module_evidence(manifest_data) + expected = {name: manifest_modules[name] for name in EXECUTABLE_NAMES} + parsed_records, parsed_summaries = parse_audit(audit_data) + records = [record for _, record in parsed_records] + summaries = [record for _, record in parsed_summaries] + allowed_module_hashes = set(manifest_modules.values()) + for line_number, record in parsed_records: + require( + record["module_sha256"] in allowed_module_hashes, + f"audit module SHA-256 is not in sealed manifest on line {line_number}", + ) + for line_number, summary in parsed_summaries: + require( + summary["module_sha256"] in allowed_module_hashes, + f"summary module SHA-256 is not in sealed manifest on line {line_number}", + ) + if required_snapshot_mode is not None: + for line_number, record in parsed_records: + require( + record["snapshot_mode"] == required_snapshot_mode, + f"snapshot mode differs from required {required_snapshot_mode} on line {line_number}", + ) + by_key: dict[tuple[str, str], list[dict[str, Any]]] = {} + for record in records: + by_key.setdefault((record["module_sha256"], record["artifact_kind"]), []).append(record) + # An outer initdb invocation necessarily execs the sealed postgres module + # for bootstrap/single-user initialization in the same native executor + # process. The loader receipt records module activations, not merely outer + # CLI invocations, so a valid initdb+postmaster lifecycle contains one + # postgres activation on every initdb pid plus one on every outer postgres + # pid. Classify the outer invocation by the presence of the initdb module; + # the product CLI admits only these two outer executables. This preserves + # exact population accounting without misreporting bootstrap activations as + # additional postmasters. + activation_pids: dict[str, set[int]] = {} + for name, module_hash in expected.items(): + aot = by_key.get((module_hash, "aot"), []) + memory = by_key.get((module_hash, "preinitialized-memory"), []) + aot_pids = [record["pid"] for record in aot] + memory_pids = [record["pid"] for record in memory] + require(len(set(aot_pids)) == len(aot_pids), f"{name} AOT audit pids are not unique") + require(len(set(memory_pids)) == len(memory_pids), f"{name} memory audit pids are not unique") + require(set(aot_pids) == set(memory_pids), f"{name} AOT and memory audit pids differ") + activation_pids[name] = set(aot_pids) + + initdb_pids = activation_pids["runtime:initdb"] + postgres_activation_pids = activation_pids["runtime:postgres"] + require( + len(initdb_pids) == expected_initdb_executions, + f"runtime:initdb must have exactly {expected_initdb_executions} outer execution pids", + ) + require( + initdb_pids <= postgres_activation_pids, + "every initdb execution must activate bootstrap postgres on the same pid", + ) + outer_postgres_pids = postgres_activation_pids - initdb_pids + require( + len(outer_postgres_pids) == expected_postgres_executions, + f"runtime:postgres must have exactly {expected_postgres_executions} outer execution pids", + ) + expected_postgres_activations = expected_initdb_executions + expected_postgres_executions + require( + len(postgres_activation_pids) == expected_postgres_activations, + "runtime:postgres activation population differs from initdb bootstrap plus outer executions", + ) + + memory_activations: dict[tuple[int, str], int] = {} + for line_number, record in parsed_records: + if record["artifact_kind"] != "preinitialized-memory": + continue + module_hash = record["module_sha256"] + require( + module_hash in manifest_attested_memory, + f"preinitialized-memory activation has no attested v2 manifest metadata on line {line_number}", + ) + require( + record["logical_bytes"] == manifest_attested_memory[module_hash]["mapped_size"], + f"preinitialized-memory activation size differs from sealed manifest on line {line_number}", + ) + key = (record["pid"], module_hash) + require(key not in memory_activations, f"duplicate preinitialized-memory activation for pid/module {key}") + memory_activations[key] = line_number + + summaries_by_activation: dict[tuple[int, str], dict[str, Any]] = {} + for line_number, summary in parsed_summaries: + key = (summary["pid"], summary["module_sha256"]) + require(key not in summaries_by_activation, f"duplicate attested-start summary for pid/module {key}") + require(key in memory_activations, f"orphan attested-start summary for pid/module {key}") + require( + line_number > memory_activations[key], + f"attested-start summary precedes its memory activation for pid/module {key}", + ) + validate_attested_start_summary( + summary, + manifest_attested_memory[summary["module_sha256"]], + line_number=line_number, + ) + summaries_by_activation[key] = summary + missing_summaries = set(memory_activations) - set(summaries_by_activation) + require( + not missing_summaries, + f"missing attested-start summary for pid/module {sorted(missing_summaries)!r}", + ) + executable_pids = { + "runtime:initdb": sorted(initdb_pids), + "runtime:postgres": sorted(outer_postgres_pids), + } + output.parent.mkdir(parents=True, exist_ok=True) + pending = output.with_name(f".{output.name}.pending.{os.getpid()}") + require(not os.path.lexists(pending), f"validation pending output already exists: {pending}") + audit_sha = hashlib.sha256(audit_data).hexdigest() + manifest_sha = hashlib.sha256(manifest_data).hexdigest() + validator_sha = sha256_file(Path(__file__)) + summary_totals = { + field: sum(summary[field] for summary in summaries) + for field in SUMMARY_COUNTER_FIELDS + } + overflow_summaries = sum(summary["counter_overflow"] for summary in summaries) + with pending.open("x", encoding="utf-8", newline="\n") as sink: + sink.write( + "schema_version\tstatus\trecords\taot_records\tmemory_records\t" + "initdb_executions\tpostgres_executions\tinitdb_pids\tpostgres_pids\t" + "required_snapshot_mode\taudit_sha256\tmanifest_sha256\tvalidator_sha256\t" + "read_advice_calls\tread_advice_successes\tsource_cache_eviction_calls\t" + "source_cache_eviction_successes\tsnapshot_cache_eviction_calls\t" + "snapshot_cache_eviction_successes\tmapping_cache_eviction_calls\t" + "mapping_cache_eviction_successes\tresidency_after_hash_inspect_bytes\t" + "residency_after_archive_release_bytes\tsource_residency_before_eviction_bytes\t" + "source_residency_after_eviction_bytes\tresidency_after_eviction_bytes\t" + "attested_summary_records\t" + + "\t".join(SUMMARY_COUNTER_FIELDS) + + "\tcounter_overflow_records\n" + ) + read_calls = sum(record["read_advice_calls"] for record in records) + read_successes = sum(record["read_advice_successes"] for record in records) + source_eviction_calls = sum(record["source_cache_eviction_calls"] for record in records) + source_eviction_successes = sum(record["source_cache_eviction_successes"] for record in records) + snapshot_eviction_calls = sum(record["snapshot_cache_eviction_calls"] for record in records) + snapshot_eviction_successes = sum(record["snapshot_cache_eviction_successes"] for record in records) + mapping_eviction_calls = sum(record["mapping_cache_eviction_calls"] for record in records) + mapping_eviction_successes = sum(record["mapping_cache_eviction_successes"] for record in records) + hash_resident_bytes = sum(record["residency_after_hash_inspect"]["resident_bytes"] for record in records) + archive_resident_bytes = sum( + record["residency_after_archive_release"]["resident_bytes"] or 0 + for record in records + ) + source_before_eviction_bytes = sum(record["source_residency_before_eviction"]["resident_bytes"] for record in records) + source_after_eviction_bytes = sum(record["source_residency_after_eviction"]["resident_bytes"] for record in records) + eviction_resident_bytes = sum(record["residency_after_eviction"]["resident_bytes"] for record in records) + sink.write( + f"{RESULT_SCHEMA}\tpassed\t{len(records)}\t" + f"{sum(record['artifact_kind'] == 'aot' for record in records)}\t" + f"{sum(record['artifact_kind'] == 'preinitialized-memory' for record in records)}\t" + f"{expected_initdb_executions}\t{expected_postgres_executions}\t" + f"{','.join(str(pid) for pid in executable_pids['runtime:initdb'])}\t" + f"{','.join(str(pid) for pid in executable_pids['runtime:postgres'])}\t" + f"{required_snapshot_mode or 'any-direct'}\t" + f"{audit_sha}\t{manifest_sha}\t{validator_sha}\t" + f"{read_calls}\t{read_successes}\t" + f"{source_eviction_calls}\t{source_eviction_successes}\t" + f"{snapshot_eviction_calls}\t{snapshot_eviction_successes}\t" + f"{mapping_eviction_calls}\t{mapping_eviction_successes}\t" + f"{hash_resident_bytes}\t{archive_resident_bytes}\t" + f"{source_before_eviction_bytes}\t{source_after_eviction_bytes}\t" + f"{eviction_resident_bytes}\t{len(summaries)}\t" + + "\t".join(str(summary_totals[field]) for field in SUMMARY_COUNTER_FIELDS) + + f"\t{overflow_summaries}\n" + ) + sink.flush() + os.fsync(sink.fileno()) + os.chmod(pending, 0o444) + os.rename(pending, output) + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--audit", type=Path, required=True) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--required-snapshot-mode", choices=sorted(DIRECT_MODES)) + parser.add_argument("--expected-initdb-executions", type=int, default=1) + parser.add_argument("--expected-postgres-executions", type=int, default=1) + arguments = parser.parse_args(argv) + try: + validate( + arguments.audit, + arguments.manifest, + arguments.output, + required_snapshot_mode=arguments.required_snapshot_mode, + expected_initdb_executions=arguments.expected_initdb_executions, + expected_postgres_executions=arguments.expected_postgres_executions, + ) + return 0 + except (OSError, ValidationError) as error: + print(f"sealed loader audit validation failed: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-sealed-loader-audit.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-sealed-loader-audit.test.py new file mode 100755 index 00000000..1e51cfa6 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-sealed-loader-audit.test.py @@ -0,0 +1,633 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import importlib.util +import json +import sys +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).with_name("validate-sealed-loader-audit.py") +SPEC = importlib.util.spec_from_file_location("validate_sealed_loader_audit", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +MODULE = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = MODULE +SPEC.loader.exec_module(MODULE) + + +INITDB = "1" * 64 +POSTGRES = "2" * 64 +PROOF_SHA256 = { + INITDB: "6" * 64, + POSTGRES: "7" * 64, +} +PROOF_OUTPUT_SHA256 = { + INITDB: "8" * 64, + POSTGRES: "9" * 64, +} +MAPPED_SIZE = 4096 + + +def memory_metadata(module_hash: str) -> dict[str, object]: + return { + "schema": MODULE.MEMORY_IMAGE_SCHEMA, + "module-sha256": module_hash, + "mapped-size": MAPPED_SIZE, + "deterministic-start-proof": { + "schema": MODULE.DETERMINISTIC_START_PROOF_SCHEMA, + "module-sha256": module_hash, + "proof-sha256": PROOF_SHA256[module_hash], + }, + "deterministic-start-proof-output-sha256": PROOF_OUTPUT_SHA256[module_hash], + } + + +def manifest() -> dict[str, object]: + artifacts = [ + { + "name": "runtime:initdb", + "module-sha256": INITDB, + "preinitialized-memory": memory_metadata(INITDB), + }, + { + "name": "runtime:postgres", + "module-sha256": POSTGRES, + "preinitialized-memory": memory_metadata(POSTGRES), + }, + {"name": "runtime:libpq.so.5.18", "module-sha256": "3" * 64}, + {"name": "runtime:dict_snowball.so", "module-sha256": "4" * 64}, + {"name": "runtime:plpgsql.so", "module-sha256": "5" * 64}, + ] + return {"artifacts": artifacts} + + +def record(module_hash: str, kind: str, pid: int) -> dict[str, object]: + logical = MAPPED_SIZE + measured = { + "state": "measured", + "page_size": 4096, + "total_pages": 1, + "resident_pages": 1, + "resident_bytes": logical, + "errno": None, + } + after_eviction = dict(measured) + after_eviction["resident_pages"] = 0 + after_eviction["resident_bytes"] = 0 + return { + "schema": MODULE.SCHEMA, + "pid": pid, + "artifact_kind": kind, + "module_sha256": module_hash, + "snapshot_mode": "direct-immutable-inode", + "logical_bytes": logical, + "source_bytes_read": 0 if kind == "aot" else logical, + "source_bytes_written": 0, + "snapshot_bytes_written": 0, + "mapping_bytes_hashed": logical, + "sync_calls": 0, + "read_advice_applicable": True, + "read_advice_supported": True, + "read_advice_calls": 2, + "read_advice_successes": 2, + "read_advice_first_errno": None, + "source_cache_eviction_applicable": True, + "source_cache_eviction_supported": True, + "source_cache_eviction_calls": 1, + "source_cache_eviction_successes": 1, + "source_cache_eviction_errno": None, + "snapshot_cache_eviction_applicable": False, + "snapshot_cache_eviction_supported": True, + "snapshot_cache_eviction_calls": 0, + "snapshot_cache_eviction_successes": 0, + "snapshot_cache_eviction_errno": None, + "mapping_cache_eviction_applicable": kind == "preinitialized-memory", + "mapping_cache_eviction_supported": True, + "mapping_cache_eviction_calls": 1 if kind == "preinitialized-memory" else 0, + "mapping_cache_eviction_successes": 1 if kind == "preinitialized-memory" else 0, + "mapping_cache_eviction_errno": None, + "residency_after_hash_inspect": dict(measured), + "residency_after_archive_release": ( + dict(measured) + if kind == "aot" + else { + "state": "not-applicable", + "page_size": None, + "total_pages": None, + "resident_pages": None, + "resident_bytes": None, + "errno": None, + } + ), + "source_residency_before_eviction": dict(measured), + "source_residency_after_eviction": dict(after_eviction), + "residency_after_eviction": dict(after_eviction), + "write_policy": "none-immutable-source", + } + + +def summary(module_hash: str, pid: int, instances: int = 1) -> dict[str, object]: + assert instances > 0 + reuse_successes = instances - 1 + return { + "schema": MODULE.SUMMARY_SCHEMA, + "pid": pid, + "artifact_kind": "attested-start-runtime-summary", + "terminal": True, + "module_sha256": module_hash, + "memory_image_schema": MODULE.MEMORY_IMAGE_SCHEMA, + "proof_sha256": PROOF_SHA256[module_hash], + "proof_output_sha256": PROOF_OUTPUT_SHA256[module_hash], + "mapped_size": MAPPED_SIZE, + "ordinary_start_completed_instances": instances, + "fresh_zeroed_instances": instances, + "nonfresh_instances": 0, + "validation_attempts": instances, + "full_compare_attempts": 1, + "full_compare_successes": 1, + "full_compare_failures": 0, + "compared_bytes": MAPPED_SIZE, + "reuse_successes": reuse_successes, + "reuse_failures": 0, + "skipped_bytes": MAPPED_SIZE * reuse_successes, + "remap_successes": instances, + "remap_failures": 0, + "counter_overflow": False, + } + + +def lifecycle_records( + initdb_pids: tuple[int, ...] = (101,), + postgres_pids: tuple[int, ...] = (202,), +) -> list[dict[str, object]]: + records: list[dict[str, object]] = [] + for pid in initdb_pids: + for module_hash in (INITDB, POSTGRES): + for kind in ("aot", "preinitialized-memory"): + records.append(record(module_hash, kind, pid)) + for pid in postgres_pids: + for kind in ("aot", "preinitialized-memory"): + records.append(record(POSTGRES, kind, pid)) + for pid in initdb_pids: + records.extend((summary(INITDB, pid), summary(POSTGRES, pid))) + for pid in postgres_pids: + records.append(summary(POSTGRES, pid)) + return records + + +def find_summary( + records: list[dict[str, object]], module_hash: str, pid: int +) -> dict[str, object]: + return next( + item + for item in records + if item.get("schema") == MODULE.SUMMARY_SCHEMA + and item.get("module_sha256") == module_hash + and item.get("pid") == pid + ) + + +class LoaderAuditTests(unittest.TestCase): + def write_fixture( + self, + root: Path, + records: list[dict[str, object]], + manifest_value: dict[str, object] | None = None, + ) -> tuple[Path, Path]: + manifest_path = root / "manifest.json" + audit_path = root / "audit.jsonl" + manifest_path.write_text(json.dumps(manifest_value or manifest()) + "\n", encoding="utf-8") + audit_path.write_text( + "".join(json.dumps(item, separators=(",", ":")) + "\n" for item in records), + encoding="utf-8", + ) + return manifest_path, audit_path + + def assert_rejected( + self, + records: list[dict[str, object]], + message: str, + manifest_value: dict[str, object] | None = None, + ) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + manifest_path, audit_path = self.write_fixture(root, records, manifest_value) + with self.assertRaisesRegex(MODULE.ValidationError, message): + MODULE.validate(audit_path, manifest_path, root / "validation.tsv") + + def test_exact_direct_aot_and_memory_evidence_passes(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + records = lifecycle_records() + manifest_path, audit_path = self.write_fixture(root, records) + output = root / "validation.tsv" + MODULE.validate( + audit_path, + manifest_path, + output, + required_snapshot_mode="direct-immutable-inode", + ) + self.assertIn( + "\tpassed\t6\t3\t3\t1\t1\t101\t202\tdirect-immutable-inode\t", + output.read_text(), + ) + header, result = output.read_text().splitlines() + self.assertTrue(header.startswith("schema_version\tstatus\trecords\t")) + self.assertIn("\tattested_summary_records\t", header) + self.assertTrue(result.startswith(f"{MODULE.RESULT_SCHEMA}\tpassed\t")) + self.assertTrue( + result.endswith( + "12\t12\t6\t6\t0\t0\t3\t3\t24576\t12288\t24576\t0\t0\t" + "3\t3\t3\t0\t3\t3\t3\t0\t12288\t0\t0\t0\t3\t0\t0" + ) + ) + + def test_exact_manifest_dynamic_module_evidence_is_accepted(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + records = lifecycle_records() + records.extend( + ( + record("3" * 64, "aot", 101), + record("5" * 64, "aot", 202), + ) + ) + manifest_path, audit_path = self.write_fixture(root, records) + output = root / "validation.tsv" + MODULE.validate(audit_path, manifest_path, output) + self.assertIn("\tpassed\t8\t5\t3\t1\t1\t101\t202\t", output.read_text()) + + def test_unknown_dynamic_module_hash_is_rejected(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + records = lifecycle_records() + records.append(record("9" * 64, "aot", 202)) + manifest_path, audit_path = self.write_fixture(root, records) + with self.assertRaisesRegex( + MODULE.ValidationError, + "audit module SHA-256 is not in sealed manifest", + ): + MODULE.validate(audit_path, manifest_path, root / "validation.tsv") + + def test_duplicate_hash_across_manifest_artifacts_is_rejected(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + manifest_value = manifest() + artifacts = manifest_value["artifacts"] + assert isinstance(artifacts, list) + assert isinstance(artifacts[2], dict) + artifacts[2]["module-sha256"] = POSTGRES + manifest_path, audit_path = self.write_fixture( + root, + lifecycle_records(), + manifest_value, + ) + with self.assertRaisesRegex( + MODULE.ValidationError, + "duplicate manifest module hash", + ): + MODULE.validate(audit_path, manifest_path, root / "validation.tsv") + + def test_repeated_direct_executions_are_counted_by_distinct_pid(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + records = lifecycle_records((101, 102), (201, 202, 203)) + manifest_path, audit_path = self.write_fixture(root, records) + output = root / "validation.tsv" + MODULE.validate( + audit_path, + manifest_path, + output, + required_snapshot_mode="direct-immutable-inode", + expected_initdb_executions=2, + expected_postgres_executions=3, + ) + self.assertIn("\tpassed\t14\t7\t7\t2\t3\t101,102\t201,202,203\t", output.read_text()) + + def test_missing_initdb_bootstrap_postgres_activation_is_rejected(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + records = [ + record(INITDB, "aot", 101), + record(INITDB, "preinitialized-memory", 101), + record(POSTGRES, "aot", 202), + record(POSTGRES, "preinitialized-memory", 202), + summary(INITDB, 101), + summary(POSTGRES, 202), + ] + manifest_path, audit_path = self.write_fixture(root, records) + with self.assertRaisesRegex( + MODULE.ValidationError, + "every initdb execution must activate bootstrap postgres", + ): + MODULE.validate(audit_path, manifest_path, root / "validation.tsv") + + def test_reflink_and_any_write_are_rejected(self) -> None: + for field, value, message in ( + ("snapshot_mode", "reflink", "non-direct snapshot mode"), + ("source_bytes_written", 1, "source bytes were written"), + ("snapshot_bytes_written", 1, "snapshot bytes were written"), + ("sync_calls", 1, "loader issued sync calls"), + ): + with self.subTest(field=field), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + records = lifecycle_records() + records[2][field] = value + manifest_path, audit_path = self.write_fixture(root, records) + with self.assertRaisesRegex(MODULE.ValidationError, message): + MODULE.validate(audit_path, manifest_path, root / "validation.tsv") + + def test_read_only_filesystem_mode_is_rejected_when_immutable_is_required(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + records = lifecycle_records() + records[3]["snapshot_mode"] = "direct-read-only-filesystem" + manifest_path, audit_path = self.write_fixture(root, records) + with self.assertRaisesRegex(MODULE.ValidationError, "snapshot mode differs from required"): + MODULE.validate( + audit_path, + manifest_path, + root / "validation.tsv", + required_snapshot_mode="direct-immutable-inode", + ) + + def test_missing_memory_activation_and_pid_mismatch_are_rejected(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + records = lifecycle_records() + records[1]["pid"] = 102 + manifest_path, audit_path = self.write_fixture(root, records) + with self.assertRaisesRegex(MODULE.ValidationError, "AOT and memory audit pids differ"): + MODULE.validate(audit_path, manifest_path, root / "validation.tsv") + + def test_failed_eviction_and_malformed_residency_are_rejected(self) -> None: + for mutate, message in ( + ( + lambda item: item.update( + read_advice_supported=False, + read_advice_calls=0, + read_advice_successes=0, + ), + "read_advice.*unsupported", + ), + ( + lambda item: item.update( + source_cache_eviction_successes=0, + source_cache_eviction_errno=5, + ), + "source_cache_eviction.*advisory call failed", + ), + ( + lambda item: item["residency_after_eviction"].update( + resident_pages=0, + resident_bytes=4096, + ), + "resident byte/page accounting differs", + ), + ): + with self.subTest(message=message), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + records = lifecycle_records() + mutate(records[0]) + manifest_path, audit_path = self.write_fixture(root, records) + with self.assertRaisesRegex(MODULE.ValidationError, message): + MODULE.validate(audit_path, manifest_path, root / "validation.tsv") + + def test_multi_instance_reuse_summary_passes_and_is_aggregated(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + records = lifecycle_records() + target = find_summary(records, POSTGRES, 202) + target.clear() + target.update(summary(POSTGRES, 202, instances=4)) + manifest_path, audit_path = self.write_fixture(root, records) + output = root / "validation.tsv" + MODULE.validate(audit_path, manifest_path, output) + header, values = output.read_text().splitlines() + result = dict(zip(header.split("\t"), values.split("\t"), strict=True)) + self.assertEqual(result["attested_summary_records"], "3") + self.assertEqual(result["ordinary_start_completed_instances"], "6") + self.assertEqual(result["full_compare_successes"], "3") + self.assertEqual(result["reuse_successes"], "3") + self.assertEqual(result["compared_bytes"], str(3 * MAPPED_SIZE)) + self.assertEqual(result["skipped_bytes"], str(3 * MAPPED_SIZE)) + self.assertEqual(result["remap_successes"], "6") + + def test_missing_duplicate_and_orphan_summaries_are_rejected(self) -> None: + records = lifecycle_records() + records.remove(find_summary(records, POSTGRES, 202)) + self.assert_rejected(records, "missing attested-start summary") + + records = lifecycle_records() + records.append(dict(find_summary(records, POSTGRES, 202))) + self.assert_rejected(records, "duplicate attested-start summary") + + records = lifecycle_records() + find_summary(records, POSTGRES, 202)["pid"] = 999 + self.assert_rejected(records, "orphan attested-start summary") + + def test_summary_module_proof_and_size_mismatches_are_rejected(self) -> None: + for field, value, message in ( + ("module_sha256", "3" * 64, "orphan attested-start summary"), + ("proof_sha256", "a" * 64, "proof_sha256 differs from sealed manifest"), + ( + "proof_output_sha256", + "b" * 64, + "proof_output_sha256 differs from sealed manifest", + ), + ("mapped_size", MAPPED_SIZE * 2, "mapped_size differs from sealed manifest"), + ): + with self.subTest(field=field): + records = lifecycle_records() + find_summary(records, INITDB, 101)[field] = value + self.assert_rejected(records, message) + + records = lifecycle_records() + manifest_value = manifest() + artifacts = manifest_value["artifacts"] + assert isinstance(artifacts, list) and isinstance(artifacts[0], dict) + memory = artifacts[0]["preinitialized-memory"] + assert isinstance(memory, dict) + memory["mapped-size"] = MAPPED_SIZE * 2 + initdb_summary = find_summary(records, INITDB, 101) + initdb_summary["mapped_size"] = MAPPED_SIZE * 2 + initdb_summary["compared_bytes"] = MAPPED_SIZE * 2 + self.assert_rejected( + records, + "activation size differs from sealed manifest", + manifest_value, + ) + + def test_start_validation_and_remap_conservation_are_rejected(self) -> None: + for mutate, message in ( + ( + lambda item: item.__setitem__( + "ordinary_start_completed_instances", 2 + ), + "ordinary-start/fresh-memory conservation differs", + ), + ( + lambda item: item.__setitem__("validation_attempts", 2), + "ordinary-start/validation conservation differs", + ), + ( + lambda item: item.update( + fresh_zeroed_instances=0, + nonfresh_instances=1, + ), + "observed a non-fresh memory instance", + ), + ( + lambda item: item.__setitem__("remap_successes", 0), + "validation/remap conservation differs", + ), + ): + with self.subTest(message=message): + records = lifecycle_records() + mutate(find_summary(records, INITDB, 101)) + self.assert_rejected(records, message) + + def test_compare_reuse_and_byte_conservation_are_rejected(self) -> None: + records = lifecycle_records() + target = find_summary(records, POSTGRES, 202) + target.clear() + target.update(summary(POSTGRES, 202, instances=2)) + target.update( + full_compare_attempts=2, + full_compare_successes=2, + compared_bytes=MAPPED_SIZE * 2, + reuse_successes=0, + skipped_bytes=0, + ) + self.assert_rejected(records, "did not perform exactly one full comparison") + + records = lifecycle_records() + target = find_summary(records, POSTGRES, 202) + target.clear() + target.update(summary(POSTGRES, 202, instances=3)) + target["reuse_successes"] = 1 + self.assert_rejected(records, "validation compare/reuse conservation differs") + + records = lifecycle_records() + find_summary(records, POSTGRES, 202)["compared_bytes"] = MAPPED_SIZE - 1 + self.assert_rejected(records, "compared byte accounting differs") + + records = lifecycle_records() + target = find_summary(records, POSTGRES, 202) + target.clear() + target.update(summary(POSTGRES, 202, instances=3)) + target["skipped_bytes"] = MAPPED_SIZE + self.assert_rejected(records, "skipped byte accounting differs") + + def test_runtime_failures_counter_overflow_and_math_overflow_are_rejected(self) -> None: + records = lifecycle_records() + target = find_summary(records, POSTGRES, 202) + target.update( + full_compare_successes=0, + full_compare_failures=1, + compared_bytes=0, + remap_successes=0, + ) + self.assert_rejected(records, "reports a full comparison failure") + + records = lifecycle_records() + target = find_summary(records, POSTGRES, 202) + target.clear() + target.update(summary(POSTGRES, 202, instances=2)) + target.update( + reuse_successes=0, + reuse_failures=1, + skipped_bytes=0, + remap_successes=1, + ) + self.assert_rejected(records, "reports a cached-validation failure") + + records = lifecycle_records() + target = find_summary(records, POSTGRES, 202) + target.update(remap_successes=0, remap_failures=1) + self.assert_rejected(records, "reports a memory-image remap failure") + + records = lifecycle_records() + find_summary(records, POSTGRES, 202)["counter_overflow"] = True + self.assert_rejected(records, "reports counter overflow") + + records = lifecycle_records() + find_summary(records, POSTGRES, 202)["validation_attempts"] = MODULE.MAX_U64 + 1 + self.assert_rejected(records, "must be an unsigned 64-bit integer") + + records = lifecycle_records() + target = find_summary(records, POSTGRES, 202) + target.clear() + target.update(summary(POSTGRES, 202)) + target.update( + ordinary_start_completed_instances=MODULE.MAX_U64, + fresh_zeroed_instances=MODULE.MAX_U64, + validation_attempts=MODULE.MAX_U64, + reuse_successes=MODULE.MAX_U64 - 1, + skipped_bytes=0, + remap_successes=MODULE.MAX_U64, + ) + self.assert_rejected(records, "byte-accounting product overflows u64") + + def test_unknown_loader_and_summary_fields_and_schemas_fail_closed(self) -> None: + for mutate, message in ( + ( + lambda records: records[0].__setitem__("unknown", 1), + "loader audit fields differ", + ), + ( + lambda records: records[0].__setitem__("schema", "unknown.loader.v1"), + "unknown audit schema", + ), + ( + lambda records: find_summary(records, INITDB, 101).__setitem__( + "unknown", 1 + ), + "attested-start summary fields differ", + ), + ( + lambda records: find_summary(records, INITDB, 101).__setitem__( + "schema", "unknown.summary.v1" + ), + "unknown audit schema", + ), + ( + lambda records: find_summary(records, INITDB, 101).__setitem__( + "memory_image_schema", "unknown.memory.v1" + ), + "unknown summary memory image schema", + ), + ): + with self.subTest(message=message): + records = lifecycle_records() + mutate(records) + self.assert_rejected(records, message) + + for field, value, message in ( + ("schema", "unknown.memory.v1", "preinitialized-memory schema differs"), + ( + "proof-schema", + "unknown.proof.v1", + "deterministic-start proof schema differs", + ), + ): + with self.subTest(manifest_field=field): + manifest_value = manifest() + artifacts = manifest_value["artifacts"] + assert isinstance(artifacts, list) and isinstance(artifacts[0], dict) + memory = artifacts[0]["preinitialized-memory"] + assert isinstance(memory, dict) + if field == "proof-schema": + proof = memory["deterministic-start-proof"] + assert isinstance(proof, dict) + proof["schema"] = value + else: + memory[field] = value + self.assert_rejected(lifecycle_records(), message, manifest_value) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wal-recycle.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wal-recycle.py new file mode 100755 index 00000000..83ae5666 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wal-recycle.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Validate bounded PostgreSQL WAL recycling from host-side snapshots.""" + +from __future__ import annotations + +import argparse +import csv +import re +import sys +from collections import defaultdict +from pathlib import Path + + +HEADER = ["schema_version", "snapshot", "ordinal", "name", "size", "device", "inode"] +EXPECTED_SNAPSHOTS = [ + "before-steady", + "after-steady", + "after-volume", + "plateau-1", + "plateau-2", + "plateau-3", +] +WAL_NAME = re.compile(r"^[0-9A-F]{24}$") +RECYCLED = re.compile(r"([0-9]+) recycled") + + +class EvidenceError(ValueError): + pass + + +def parser() -> argparse.ArgumentParser: + result = argparse.ArgumentParser() + result.add_argument("--snapshots", type=Path, required=True) + result.add_argument("--server-log", type=Path, required=True) + result.add_argument("--output", type=Path, required=True) + result.add_argument("--gates-output", type=Path, required=True) + result.add_argument("--max-plateau-range-bytes", type=int, default=33554432) + result.add_argument("--max-plateau-count-range", type=int, default=2) + result.add_argument("--max-latest-bytes", type=int, default=536870912) + return result + + +def write(path: Path, rows: list[list[object]]) -> None: + if path.exists() or path.is_symlink(): + raise EvidenceError(f"refusing to replace output: {path}") + with path.open("x", newline="", encoding="utf-8") as stream: + csv.writer(stream, delimiter="\t", lineterminator="\n").writerows(rows) + + +def main() -> int: + args = parser().parse_args() + try: + for path in (args.snapshots, args.server_log): + if not path.is_file() or path.is_symlink(): + raise EvidenceError(f"evidence must be regular and non-symlink: {path}") + snapshots: dict[str, list[tuple[str, int, int, int]]] = {} + snapshot_order: list[str] = [] + inode_history: dict[tuple[int, int], dict[str, set[str]]] = defaultdict( + lambda: defaultdict(set) + ) + snapshot_names: dict[str, set[str]] = defaultdict(set) + snapshot_inodes: dict[str, set[tuple[int, int]]] = defaultdict(set) + with args.snapshots.open(newline="", encoding="utf-8") as stream: + reader = csv.DictReader(stream, delimiter="\t") + if reader.fieldnames != HEADER: + raise EvidenceError(f"unexpected WAL snapshot header: {reader.fieldnames!r}") + for row in reader: + if row["schema_version"] != "1" or not WAL_NAME.fullmatch(row["name"]): + raise EvidenceError("WAL snapshot contains a malformed row") + numeric = [row[field] for field in ("ordinal", "size", "device", "inode")] + if any(not value.isdigit() for value in numeric): + raise EvidenceError("WAL snapshot numeric field is malformed") + if row["snapshot"] not in snapshots: + if row["snapshot"] in snapshot_order: + raise EvidenceError("WAL snapshot rows must be contiguous") + snapshots[row["snapshot"]] = [] + snapshot_order.append(row["snapshot"]) + elif snapshot_order[-1] != row["snapshot"]: + raise EvidenceError("WAL snapshot rows must be contiguous") + ordinal = int(row["ordinal"]) + record = ( + row["name"], + int(row["size"]), + int(row["device"]), + int(row["inode"]), + ) + identity = (record[2], record[3]) + if ordinal != len(snapshots[row["snapshot"]]) + 1: + raise EvidenceError( + "WAL snapshot ordinals must restart at one and be contiguous" + ) + if ( + record[0] in snapshot_names[row["snapshot"]] + or identity in snapshot_inodes[row["snapshot"]] + ): + raise EvidenceError( + "a WAL name or filesystem identity is duplicated within one snapshot" + ) + if snapshots[row["snapshot"]] and record[0] <= snapshots[row["snapshot"]][-1][0]: + raise EvidenceError( + "WAL snapshot rows must be in ascending segment-name order" + ) + if record[1] <= 0: + raise EvidenceError("WAL segment size must be positive") + snapshots[row["snapshot"]].append(record) + snapshot_names[row["snapshot"]].add(record[0]) + snapshot_inodes[row["snapshot"]].add(identity) + inode_history[identity][row["snapshot"]].add(record[0]) + if snapshot_order != EXPECTED_SNAPSHOTS: + raise EvidenceError( + f"WAL snapshots are {snapshot_order!r}, expected {EXPECTED_SNAPSHOTS!r}" + ) + if any(not snapshots[label] for label in EXPECTED_SNAPSHOTS): + raise EvidenceError("every required WAL snapshot must contain a segment") + recycled_log_count = 0 + with args.server_log.open(encoding="utf-8", errors="strict") as stream: + for line in stream: + match = RECYCLED.search(line) + if match is not None: + recycled_log_count += int(match.group(1)) + reused_inodes = 0 + reuse_transitions = 0 + for by_snapshot in inode_history.values(): + identity_reused = False + for earlier, later in zip( + EXPECTED_SNAPSHOTS, EXPECTED_SNAPSHOTS[1:] + ): + if ( + earlier in by_snapshot + and later in by_snapshot + and by_snapshot[earlier] != by_snapshot[later] + ): + identity_reused = True + reuse_transitions += 1 + reused_inodes += int(identity_reused) + last_three = snapshot_order[-3:] + byte_totals = [sum(item[1] for item in snapshots[label]) for label in last_three] + file_counts = [len(snapshots[label]) for label in last_three] + byte_range = max(byte_totals) - min(byte_totals) + count_range = max(file_counts) - min(file_counts) + latest_bytes = byte_totals[-1] + gates = [ + ("logged_recycling", ">0", recycled_log_count, recycled_log_count > 0), + ("inode_reuse_under_new_name", ">0", reused_inodes, reused_inodes > 0), + ("cross_snapshot_reuse_transitions", ">0", reuse_transitions, reuse_transitions > 0), + ( + "last_three_byte_range", + f"<={args.max_plateau_range_bytes}", + byte_range, + byte_range <= args.max_plateau_range_bytes, + ), + ( + "last_three_count_range", + f"<={args.max_plateau_count_range}", + count_range, + count_range <= args.max_plateau_count_range, + ), + ( + "latest_wal_bytes", + f"<={args.max_latest_bytes}", + latest_bytes, + latest_bytes <= args.max_latest_bytes, + ), + ] + passed = all(item[3] for item in gates) + write( + args.gates_output, + [["gate", "expected", "observed", "status"]] + + [ + [name, expected, observed, "passed" if ok else "failed"] + for name, expected, observed, ok in gates + ], + ) + write( + args.output, + [[ + "schema_version", "status", "snapshot_count", + "logged_recycled_files", "reused_inode_count", + "cross_snapshot_reuse_transitions", + "last_three_byte_range", "last_three_count_range", + "latest_wal_bytes", "gates", + ], [ + "oliphaunt.wasix-postmaster.wal-recycle.v1", + "passed" if passed else "failed", len(snapshot_order), + recycled_log_count, reused_inodes, reuse_transitions, + byte_range, count_range, + latest_bytes, args.gates_output, + ]], + ) + return 0 if passed else 1 + except (EvidenceError, OSError, csv.Error) as error: + print(f"WAL recycle validation failed: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wal-recycle.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wal-recycle.test.py new file mode 100755 index 00000000..9738fc6e --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wal-recycle.test.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent +VALIDATOR = ROOT / "validate-wal-recycle.py" + + +class WalRecycleTest(unittest.TestCase): + def run_fixture( + self, *, recycled: int = 2, reuse_inode: bool = True, bad_ordinal: bool = False + ) -> subprocess.CompletedProcess[str]: + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + root = Path(temporary.name) + snapshots = root / "snapshots.tsv" + with snapshots.open("x", newline="", encoding="utf-8") as stream: + writer = csv.writer(stream, delimiter="\t", lineterminator="\n") + writer.writerow(["schema_version", "snapshot", "ordinal", "name", "size", "device", "inode"]) + labels = [ + "before-steady", "after-steady", "after-volume", + "plateau-1", "plateau-2", "plateau-3", + ] + for index, label in enumerate(labels, start=1): + writer.writerow( + [ + 1, + label, + 2 if bad_ordinal and index == 1 else 1, + f"0000000100000000000000{index:02X}", + 16777216, + 7, + 101 if reuse_inode else 100 + index, + ] + ) + log = root / "server.log" + log.write_text( + f"2026-08-10 00:00:00 UTC LOG: checkpoint complete: 0 added, 0 removed, {recycled} recycled; total=1.0 s\n", + encoding="utf-8", + ) + return subprocess.run( + [ + str(VALIDATOR), "--snapshots", str(snapshots), + "--server-log", str(log), "--output", str(root / "summary.tsv"), + "--gates-output", str(root / "gates.tsv"), + ], + text=True, + capture_output=True, + check=False, + ) + + def test_recycled_inode_and_plateau_pass(self) -> None: + result = self.run_fixture() + self.assertEqual(result.returncode, 0, result.stderr) + + def test_missing_log_recycle_fails(self) -> None: + result = self.run_fixture(recycled=0) + self.assertEqual(result.returncode, 1, result.stderr) + + def test_same_inode_is_required_across_snapshots(self) -> None: + result = self.run_fixture(reuse_inode=False) + self.assertEqual(result.returncode, 1, result.stderr) + + def test_ordinals_restart_for_each_snapshot(self) -> None: + result = self.run_fixture(bad_ordinal=True) + self.assertEqual(result.returncode, 2, result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-cold-ownership.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-cold-ownership.py new file mode 100755 index 00000000..fd784c87 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-cold-ownership.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +"""Validate one cold carrier/PGDATA launch and emit its canonical sample row.""" + +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +import math +import os +import re +import sys +from pathlib import Path +from typing import Any, NoReturn + + +SCHEMA = "oliphaunt.wasix-postmaster.cold-ownership-sample.v1" +PROOF_SCHEMA = "oliphaunt.wasix-postmaster.cold-residency.v1" +SNAPSHOT_SCHEMA = "oliphaunt.wasix-postmaster.cgroup-v2-snapshot.v1" +SHA256 = re.compile(r"^[0-9a-f]{64}$") +DEVICE = re.compile(r"^[0-9]+:[0-9]+$") +IO_METRICS = ("rbytes", "wbytes", "rios", "wios") + +HEADER = [ + "schema_version", + "target", + "status", + "execution_identity_sha256", + "residency_receipt_sha256", + "carrier_root", + "pgdata_root", + "regular_path_count", + "unique_file_count", + "logical_bytes", + "page_count", + "resident_before_pages", + "resident_after_pages", + "proof_end_monotonic_ns", + "spawn_monotonic_ns", + "boundary_to_spawn_ms", + "first_query_monotonic_ns", + "spawn_to_first_query_ms", + "readiness_attempts", + "first_query_snapshot_sha256", + "final_snapshot_sha256", + "cgroup_path", + "cgroup_identity", + "memory_max_bytes", + "memory_high_bytes", + "swap_max_bytes", + "whole_scope_memory_peak_bytes", + "whole_scope_swap_peak_bytes", + "peak_file_dirty_bytes", + "peak_file_writeback_bytes", + "io_observation_status", + "io_controller_status", + "io_missing_reason", + "io_first_touch_status", + "io_read_bytes", + "io_write_bytes", + "io_read_ios", + "io_write_ios", + "resource_sample_count", + "full_valid_sample_count", + "resource_samples_sha256", + "validator_sha256", +] + + +class ValidationError(RuntimeError): + pass + + +def fail(message: str) -> NoReturn: + raise ValidationError(message) + + +def require(condition: bool, message: str) -> None: + if not condition: + fail(message) + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def load_json(path: Path, schema: str) -> dict[str, Any]: + require(path.is_file() and not path.is_symlink(), f"evidence must be a regular non-symlink file: {path}") + with path.open("r", encoding="utf-8") as stream: + value = json.load(stream) + require(isinstance(value, dict), f"JSON evidence root must be an object: {path}") + require(value.get("schema_version") == schema, f"unexpected evidence schema in {path}") + require(value.get("status") == "passed", f"evidence is not passed: {path}") + return value + + +def integer(value: Any, label: str) -> int: + require(isinstance(value, int) and not isinstance(value, bool) and value >= 0, f"{label} must be unsigned") + return value + + +def size_bytes(value: str) -> int: + match = re.fullmatch(r"([0-9]+)([KMGTPE]?)(?:i?B)?", value) + if not match: + fail(f"invalid cgroup size: {value}") + number = int(match.group(1)) + exponent = "KMGTPE".find(match.group(2)) + 1 if match.group(2) else 0 + return number * (1024**exponent) + + +def io_observation( + snapshot: dict[str, Any], label: str +) -> tuple[str, str, str, dict[str, int] | None]: + io = snapshot.get("io") + require(isinstance(io, dict), f"{label} snapshot io evidence must be an object") + status = io.get("status") + controller_status = io.get("controller_status") + missing_reason = io.get("missing_reason") + require( + controller_status in {"available", "missing"}, + f"{label} snapshot has invalid io controller status", + ) + if status == "unavailable": + expected_reason = ( + "io-controller-missing" + if controller_status == "missing" + else "io-stat-missing" + ) + require( + missing_reason == expected_reason, + f"{label} snapshot has incoherent unavailable io reason", + ) + require(io.get("source") is None, f"{label} unavailable io source must be null") + require(io.get("devices") is None, f"{label} unavailable io devices must be null") + require(io.get("totals") is None, f"{label} unavailable io totals must be null") + return status, controller_status, expected_reason, None + require(status == "available", f"{label} snapshot has invalid io status") + require(missing_reason is None, f"{label} available io reason must be null") + require(io.get("source") == "io.stat", f"{label} available io source is not io.stat") + devices = io.get("devices") + require(isinstance(devices, list), f"{label} available io devices must be a list") + totals = io.get("totals") + require(isinstance(totals, dict), f"{label} available io totals must be an object") + expected_totals: dict[str, int] = {name: 0 for name in IO_METRICS} + seen_devices: set[str] = set() + for index, device in enumerate(devices): + require(isinstance(device, dict), f"{label} io device {index} must be an object") + device_name = device.get("device") + require( + isinstance(device_name, str) + and DEVICE.fullmatch(device_name) is not None + and device_name not in seen_devices, + f"{label} io device {index} has invalid or duplicate identity", + ) + seen_devices.add(device_name) + device_metrics = device.get("metrics") + require( + isinstance(device_metrics, dict), + f"{label} io device {device_name} metrics must be an object", + ) + require( + set(IO_METRICS) <= set(device_metrics), + f"{label} io device {device_name} lacks required metrics", + ) + for name, value in device_metrics.items(): + require( + isinstance(name, str) + and name != "" + and not any(character.isspace() for character in name), + f"{label} io device {device_name} has invalid metric name", + ) + expected_totals[name] = expected_totals.get(name, 0) + integer( + value, f"{label}.io.devices.{device_name}.{name}" + ) + require(set(totals) == set(expected_totals), f"{label} io totals keys do not match devices") + for name, expected in expected_totals.items(): + require( + integer(totals.get(name), f"{label}.io.totals.{name}") == expected, + f"{label} io total {name} does not match devices", + ) + metrics = { + name: integer(totals.get(name), f"{label}.io.totals.{name}") + for name in IO_METRICS + } + return status, controller_status, "none", metrics + + +def memory_value(snapshot: dict[str, Any], name: str) -> int: + return integer(snapshot.get("memory", {}).get(name), f"memory.{name}") + + +def memory_stat(snapshot: dict[str, Any], name: str) -> int: + return integer(snapshot.get("memory", {}).get("stat", {}).get(name), f"memory.stat.{name}") + + +def validate_resource_samples(path: Path, cgroup_path: str) -> tuple[int, int, int, int, int, int]: + require(path.is_file() and not path.is_symlink(), f"resource samples missing: {path}") + with path.open("r", encoding="utf-8", newline="") as stream: + rows = list(csv.DictReader(stream, delimiter="\t")) + require(rows, "resource sampler produced no rows") + valid = 0 + memory_peak = 0 + swap_peak = 0 + dirty_peak = 0 + writeback_peak = 0 + for row in rows: + require(row.get("target") == "wasix", "cold resource sample contains another target") + if row.get("smaps_status") == "ok" and row.get("cgroup_status") == "ok": + require(row.get("cgroup_path") == cgroup_path, "resource sample cgroup path changed") + valid += 1 + for key, destination in ( + ("cgroup_scope_memory_peak_bytes", "memory"), + ("cgroup_scope_swap_peak_bytes", "swap"), + ("cgroup_memory_stat_file_dirty_bytes", "dirty"), + ("cgroup_memory_stat_file_writeback_bytes", "writeback"), + ): + assigned = row.get(key, "") + require(assigned.isdigit(), f"valid resource sample has malformed {key}") + number = int(assigned) + if destination == "memory": + memory_peak = max(memory_peak, number) + elif destination == "swap": + swap_peak = max(swap_peak, number) + elif destination == "dirty": + dirty_peak = max(dirty_peak, number) + else: + writeback_peak = max(writeback_peak, number) + require(valid > 0, "full sampler produced no exact smaps+cgroup sample") + return len(rows), valid, memory_peak, swap_peak, dirty_peak, writeback_peak + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--residency-receipt", required=True, type=Path) + parser.add_argument("--first-query-snapshot", required=True, type=Path) + parser.add_argument("--final-snapshot", required=True, type=Path) + parser.add_argument("--resource-samples", required=True, type=Path) + parser.add_argument("--execution-identity", required=True, type=Path) + parser.add_argument("--carrier-root", required=True) + parser.add_argument("--pgdata-root", required=True) + parser.add_argument("--spawn-monotonic-ns", required=True, type=int) + parser.add_argument("--first-query-monotonic-ns", required=True, type=int) + parser.add_argument("--readiness-attempts", required=True, type=int) + parser.add_argument("--memory-max", required=True) + parser.add_argument("--memory-high", required=True) + parser.add_argument("--swap-max", required=True) + parser.add_argument("--max-boundary-to-spawn-ms", default=1000, type=int) + parser.add_argument("--output", required=True, type=Path) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + proof = load_json(args.residency_receipt, PROOF_SCHEMA) + first = load_json(args.first_query_snapshot, SNAPSHOT_SCHEMA) + final = load_json(args.final_snapshot, SNAPSHOT_SCHEMA) + execution_sha = sha256_file(args.execution_identity) + require(SHA256.fullmatch(execution_sha) is not None, "invalid execution identity SHA-256") + require( + proof.get("bindings", {}).get("execution_identity_sha256") == execution_sha, + "residency proof is not bound to this execution identity", + ) + roots = proof.get("roots") + require(isinstance(roots, list) and len(roots) == 2, "proof must contain exactly carrier and PGDATA roots") + by_role = {root.get("role"): root for root in roots if isinstance(root, dict)} + require(set(by_role) == {"carrier", "pgdata"}, "proof root roles are not exact") + require(by_role["carrier"].get("path") == args.carrier_root, "carrier root changed") + require(by_role["carrier"].get("require_read_only") is True, "carrier was not mode-gated read-only") + require(by_role["pgdata"].get("path") == args.pgdata_root, "PGDATA root changed") + totals = proof.get("totals", {}) + resident_after = integer(totals.get("resident_after_pages"), "resident_after_pages") + require(resident_after == 0, "cold boundary retained regular-file pages") + page_count = integer(totals.get("page_count"), "page_count") + require(page_count > 0, "cold boundary covered no file pages") + proof_end = integer( + proof.get("timestamps", {}).get("proof_completed_monotonic_ns"), + "proof_completed_monotonic_ns", + ) + require(args.spawn_monotonic_ns >= proof_end, "spawn predates cold boundary") + require(args.first_query_monotonic_ns >= args.spawn_monotonic_ns, "first query predates spawn") + boundary_ms = (args.spawn_monotonic_ns - proof_end) / 1_000_000.0 + startup_ms = (args.first_query_monotonic_ns - args.spawn_monotonic_ns) / 1_000_000.0 + require(boundary_ms <= args.max_boundary_to_spawn_ms, "cold proof was not immediately followed by launch") + require(args.readiness_attempts > 0, "readiness attempt count must be positive") + + first_cgroup = first.get("cgroup", {}) + final_cgroup = final.get("cgroup", {}) + cgroup_path = first_cgroup.get("path") + cgroup_identity = first_cgroup.get("identity") + require(isinstance(cgroup_path, str) and cgroup_path.startswith("/"), "invalid cgroup path") + require(isinstance(cgroup_identity, str) and cgroup_identity, "invalid cgroup identity") + require(final_cgroup.get("path") == cgroup_path, "final snapshot moved cgroups") + require(final_cgroup.get("identity") == cgroup_identity, "final snapshot cgroup identity changed") + require( + integer(first.get("captured_monotonic_ns"), "first snapshot time") >= args.first_query_monotonic_ns, + "first-query snapshot predates first query", + ) + require( + integer(final.get("captured_monotonic_ns"), "final snapshot time") + >= integer(first.get("captured_monotonic_ns"), "first snapshot time"), + "final snapshot predates first snapshot", + ) + max_bytes = size_bytes(args.memory_max) + high_bytes = size_bytes(args.memory_high) + swap_bytes = size_bytes(args.swap_max) + for label, expected in (("max", max_bytes), ("high", high_bytes), ("swap_max", swap_bytes)): + require(first.get("memory", {}).get(label) == expected, f"first snapshot memory.{label} limit mismatch") + require(final.get("memory", {}).get(label) == expected, f"final snapshot memory.{label} limit mismatch") + + sample_count, full_count, sample_memory_peak, sample_swap_peak, sample_dirty, sample_writeback = ( + validate_resource_samples(args.resource_samples, cgroup_path) + ) + whole_memory_peak = max(memory_value(first, "peak"), memory_value(final, "peak"), sample_memory_peak) + whole_swap_peak = max(memory_value(first, "swap_peak"), memory_value(final, "swap_peak"), sample_swap_peak) + dirty_peak = max(memory_stat(first, "file_dirty"), memory_stat(final, "file_dirty"), sample_dirty) + writeback_peak = max( + memory_stat(first, "file_writeback"), memory_stat(final, "file_writeback"), sample_writeback + ) + require(whole_memory_peak > 0 and whole_memory_peak <= max_bytes, "whole-scope memory peak violates hard limit") + require(whole_swap_peak <= swap_bytes, "whole-scope swap peak violates hard limit") + require(memory_stat(first, "file") > 0, "first query charged no file pages to measured scope") + first_io_status, first_io_controller, first_io_reason, first_io = io_observation( + first, "first" + ) + io_status, io_controller, io_missing_reason, final_io = io_observation(final, "final") + require(io_status == first_io_status, "io observation status changed between snapshots") + require(io_controller == first_io_controller, "io controller status changed between snapshots") + require(io_missing_reason == first_io_reason, "io missing reason changed between snapshots") + if final_io is None: + require(first_io is None, "unavailable final io has available first snapshot") + io_first_touch_status = "unavailable" + io_read_bytes: int | str = "" + io_write_bytes: int | str = "" + io_read_ios: int | str = "" + io_write_ios: int | str = "" + else: + require(first_io is not None, "available final io has unavailable first snapshot") + io_read_bytes = final_io["rbytes"] + io_write_bytes = final_io["wbytes"] + io_read_ios = final_io["rios"] + io_write_ios = final_io["wios"] + require( + io_read_bytes > 0 and io_read_ios > 0, + "cold scope has no attributable storage reads", + ) + require(io_read_bytes >= first_io["rbytes"], "cgroup read bytes regressed") + require(io_write_bytes >= first_io["wbytes"], "cgroup write bytes regressed") + require(io_read_ios >= first_io["rios"], "cgroup read I/Os regressed") + require(io_write_ios >= first_io["wios"], "cgroup write I/Os regressed") + io_first_touch_status = "attributable" + + values: list[str | int] = [ + SCHEMA, + "wasix", + "passed", + execution_sha, + sha256_file(args.residency_receipt), + args.carrier_root, + args.pgdata_root, + integer(totals.get("regular_path_count"), "regular_path_count"), + integer(totals.get("unique_file_count"), "unique_file_count"), + integer(totals.get("logical_bytes"), "logical_bytes"), + page_count, + integer(totals.get("resident_before_pages"), "resident_before_pages"), + resident_after, + proof_end, + args.spawn_monotonic_ns, + f"{boundary_ms:.6f}", + args.first_query_monotonic_ns, + f"{startup_ms:.6f}", + args.readiness_attempts, + sha256_file(args.first_query_snapshot), + sha256_file(args.final_snapshot), + cgroup_path, + cgroup_identity, + max_bytes, + high_bytes, + swap_bytes, + whole_memory_peak, + whole_swap_peak, + dirty_peak, + writeback_peak, + io_status, + io_controller, + io_missing_reason, + io_first_touch_status, + io_read_bytes, + io_write_bytes, + io_read_ios, + io_write_ios, + sample_count, + full_count, + sha256_file(args.resource_samples), + sha256_file(Path(__file__)), + ] + require(len(values) == len(HEADER), "internal cold sample schema mismatch") + args.output.parent.mkdir(parents=True, exist_ok=True) + pending = args.output.with_name(f".{args.output.name}.pending.{os.getpid()}") + with pending.open("x", encoding="utf-8", newline="") as stream: + writer = csv.writer(stream, delimiter="\t", lineterminator="\n") + writer.writerow(HEADER) + writer.writerow(values) + stream.flush() + os.fsync(stream.fileno()) + os.replace(pending, args.output) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (ValidationError, OSError, ValueError, json.JSONDecodeError) as error: + print(f"cold-ownership validation failed: {error}", file=sys.stderr) + raise SystemExit(1) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-memory-plateau.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-memory-plateau.py new file mode 100755 index 00000000..ccb9c71b --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-memory-plateau.py @@ -0,0 +1,605 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import csv +import hashlib +import math +import os +from pathlib import Path +import re +import statistics +import sys +from typing import Iterable + + +CHECKPOINT_SCHEMA = "oliphaunt.wasix-postmaster.lifecycle-memory-checkpoint.v1" +RESULT_SCHEMA = "oliphaunt.wasix-postmaster.lifecycle-memory-plateau.v1" +CLAIM_SCOPE = ( + "untimed-quiescent-reconnect-boundaries;baseline-and-terminal-runtime-fenced" +) +CHECKPOINT_FIELDS = ( + "schema_version", + "nonce", + "sequence", + "stage", + "completed_reconnects", + "requested_reconnects", + "checkpoint_every", + "quiescence_seconds", + "quiescence_start_ns", + "quiescence_end_ns", + "monotonic_before_ns", + "monotonic_after_ns", + "capture_elapsed_ns", + "server_pid", + "server_birth_identity", + "pss_kib", + "pss_anon_kib", + "anonymous_kib", + "heap_pss_kib", + "heap_private_kib", + "heap_mappings", + "status", +) +METRICS = ("pss_kib", "pss_anon_kib", "heap_pss_kib") +RESULT_FIELDS = ( + "schema_version", + "target", + "status", + "detail", + "claim_scope", + "input_path", + "input_sha256", + "runtime_plateau_path", + "runtime_plateau_sha256", + "runtime_evidence_sha256", + "runtime_freeze_receipt_sha256", + "validator_sha256", + "nonce", + "server_pid", + "server_birth_identity", + "checkpoint_count", + "requested_reconnects", + "checkpoint_every", + "min_quiescence_seconds", + "max_pss_growth_kib", + "max_pss_anon_growth_kib", + "max_heap_growth_kib", + "max_late_pss_slope_kib_per_1000_reconnects", + "max_late_pss_anon_slope_kib_per_1000_reconnects", + "max_late_heap_slope_kib_per_1000_reconnects", + "max_capture_elapsed_ns", + "tail_start_reconnects", + *( + field + for metric in METRICS + for field in ( + f"baseline_{metric}", + f"peak_{metric}", + f"final_{metric}", + f"full_peak_growth_{metric}", + f"final_growth_{metric}", + f"late_peak_growth_{metric}", + f"late_theil_sen_{metric}_per_1000_reconnects", + ) + ), +) +NONCE_RE = re.compile(r"^[0-9a-f]{32}$") +BIRTH_IDENTITY_RE = re.compile(r"^linux-starttime:[1-9][0-9]*$") +MIN_CHECKPOINTS = 5 +MAX_CHECKPOINTS = 257 + + +class EvidenceError(ValueError): + pass + + +def parse_nonnegative_int(value: str, field: str) -> int: + if re.fullmatch(r"0|[1-9][0-9]*", value) is None: + raise EvidenceError(f"{field} is not a canonical nonnegative integer") + return int(value) + + +def parse_positive_int(value: str, field: str) -> int: + parsed = parse_nonnegative_int(value, field) + if parsed == 0: + raise EvidenceError(f"{field} must be positive") + return parsed + + +def parse_nonnegative_number(value: str, field: str) -> float: + if re.fullmatch(r"(?:(?:0|[1-9][0-9]*)(?:\.[0-9]+)?|\.[0-9]+)", value) is None: + raise EvidenceError(f"{field} is not a canonical nonnegative number") + return float(value) + + +def read_regular_bytes(path: Path) -> bytes: + if path.is_symlink() or not path.is_file(): + raise EvidenceError(f"input is not a regular non-symlink file: {path}") + return path.read_bytes() + + +def expected_schedule(requested: int, every: int) -> list[int]: + if every >= requested: + raise EvidenceError("checkpoint interval must be smaller than reconnect count") + schedule = [0, *range(every, requested, every), requested] + if len(schedule) < MIN_CHECKPOINTS: + raise EvidenceError("checkpoint schedule needs at least five observations") + if len(schedule) > MAX_CHECKPOINTS: + raise EvidenceError("checkpoint schedule is too dense for nonintrusive sampling") + return schedule + + +def median(values: Iterable[float]) -> float: + return float(statistics.median(tuple(values))) + + +def theil_sen_per_1000(points: list[tuple[int, int]]) -> float: + slopes: list[float] = [] + for index, (left_x, left_y) in enumerate(points): + for right_x, right_y in points[index + 1 :]: + if right_x > left_x: + slopes.append((right_y - left_y) * 1000.0 / (right_x - left_x)) + return median(slopes) if slopes else 0.0 + + +def parse_checkpoints( + path: Path, + *, + nonce: str, + server_pid: int, + requested: int, + every: int, + min_quiescence_seconds: float, +) -> tuple[list[dict[str, int | float | str]], str]: + raw = read_regular_bytes(path) + try: + text = raw.decode("utf-8", errors="strict") + except UnicodeError as error: + raise EvidenceError(f"checkpoint evidence is not UTF-8: {error}") from error + reader = csv.DictReader(text.splitlines(), delimiter="\t", strict=True) + if tuple(reader.fieldnames or ()) != CHECKPOINT_FIELDS: + raise EvidenceError("checkpoint evidence has an unexpected ordered schema") + rows: list[dict[str, int | float | str]] = [] + schedule = expected_schedule(requested, every) + previous_after_ns = -1 + birth_identity = "" + for row_number, source in enumerate(reader, start=2): + if None in source or any(value is None for value in source.values()): + raise EvidenceError(f"row {row_number} has an invalid field count") + if source["schema_version"] != CHECKPOINT_SCHEMA: + raise EvidenceError(f"row {row_number} has an unexpected schema version") + if source["nonce"] != nonce: + raise EvidenceError(f"row {row_number} has a different lifecycle nonce") + sequence = parse_nonnegative_int(source["sequence"], "sequence") + if sequence != len(rows): + raise EvidenceError(f"row {row_number} has a non-contiguous sequence") + completed = parse_nonnegative_int( + source["completed_reconnects"], "completed_reconnects" + ) + if sequence >= len(schedule) or completed != schedule[sequence]: + raise EvidenceError(f"row {row_number} is outside the exact checkpoint schedule") + row_requested = parse_positive_int( + source["requested_reconnects"], "requested_reconnects" + ) + row_every = parse_positive_int(source["checkpoint_every"], "checkpoint_every") + if row_requested != requested or row_every != every: + raise EvidenceError(f"row {row_number} changes the reconnect policy") + expected_stage = ( + "baseline-fenced" + if sequence == 0 + else "final-fenced" + if completed == requested + else "wave-quiescent" + ) + if source["stage"] != expected_stage: + raise EvidenceError(f"row {row_number} has an invalid checkpoint stage") + quiescence = parse_nonnegative_number( + source["quiescence_seconds"], "quiescence_seconds" + ) + if quiescence < min_quiescence_seconds: + raise EvidenceError(f"row {row_number} has an insufficient quiescence window") + quiescence_start_ns = parse_positive_int( + source["quiescence_start_ns"], "quiescence_start_ns" + ) + quiescence_end_ns = parse_positive_int( + source["quiescence_end_ns"], "quiescence_end_ns" + ) + if quiescence_end_ns <= quiescence_start_ns: + raise EvidenceError(f"row {row_number} has invalid quiescence timing") + if quiescence_end_ns - quiescence_start_ns < quiescence * 1_000_000_000: + raise EvidenceError(f"row {row_number} did not observe its declared quiescence window") + before_ns = parse_positive_int(source["monotonic_before_ns"], "monotonic_before_ns") + after_ns = parse_positive_int(source["monotonic_after_ns"], "monotonic_after_ns") + elapsed_ns = parse_positive_int(source["capture_elapsed_ns"], "capture_elapsed_ns") + if after_ns < before_ns or after_ns - before_ns != elapsed_ns: + raise EvidenceError(f"row {row_number} has inconsistent capture timing") + if before_ns < quiescence_end_ns: + raise EvidenceError(f"row {row_number} overlaps capture with quiescence timing") + if previous_after_ns >= 0 and quiescence_start_ns <= previous_after_ns: + raise EvidenceError(f"row {row_number} starts quiescence before the preceding capture") + if before_ns <= previous_after_ns: + raise EvidenceError(f"row {row_number} is not after the preceding capture") + previous_after_ns = after_ns + row_pid = parse_positive_int(source["server_pid"], "server_pid") + if row_pid != server_pid: + raise EvidenceError(f"row {row_number} has a different server PID") + row_birth_identity = source["server_birth_identity"] + if BIRTH_IDENTITY_RE.fullmatch(row_birth_identity) is None: + raise EvidenceError(f"row {row_number} has an invalid Linux birth identity") + if not birth_identity: + birth_identity = row_birth_identity + elif row_birth_identity != birth_identity: + raise EvidenceError(f"row {row_number} changes the server birth identity") + metrics = { + field: parse_nonnegative_int(source[field], field) + for field in ( + "pss_kib", + "pss_anon_kib", + "anonymous_kib", + "heap_pss_kib", + "heap_private_kib", + "heap_mappings", + ) + } + if metrics["pss_anon_kib"] > metrics["pss_kib"]: + raise EvidenceError(f"row {row_number} has PSS_Anon greater than PSS") + if metrics["heap_pss_kib"] > metrics["pss_kib"]: + raise EvidenceError(f"row {row_number} has heap PSS greater than total PSS") + if source["status"] != "passed": + raise EvidenceError(f"row {row_number} was not captured successfully") + rows.append( + { + "sequence": sequence, + "completed_reconnects": completed, + "quiescence_start_ns": quiescence_start_ns, + "quiescence_end_ns": quiescence_end_ns, + "monotonic_before_ns": before_ns, + "monotonic_after_ns": after_ns, + "capture_elapsed_ns": elapsed_ns, + "server_birth_identity": row_birth_identity, + **metrics, + } + ) + if [int(row["completed_reconnects"]) for row in rows] != schedule: + raise EvidenceError("checkpoint evidence is incomplete or has extra rows") + return rows, hashlib.sha256(raw).hexdigest() + + +def parse_runtime_plateau( + path: Path, *, nonce: str, requested: int +) -> tuple[dict[str, int | str], str]: + raw = read_regular_bytes(path) + try: + text = raw.decode("utf-8", errors="strict") + except UnicodeError as error: + raise EvidenceError(f"runtime plateau is not UTF-8: {error}") from error + reader = csv.DictReader(text.splitlines(), delimiter="\t", strict=True) + required = { + "schema_version", + "target", + "status", + "nonce", + "evidence_sha256", + "freeze_receipt_sha256", + "reconnect_requested", + "reconnect_completed", + "reconnect_start_mono_ns", + "reconnect_end_mono_ns", + "readiness_fence_mono_ns", + "post_quiescence_fence_mono_ns", + } + fieldnames = tuple(reader.fieldnames or ()) + if len(fieldnames) != len(set(fieldnames)) or not required.issubset(fieldnames): + raise EvidenceError("runtime plateau is missing required receipt fields") + rows = list(reader) + if len(rows) != 1 or None in rows[0] or any(value is None for value in rows[0].values()): + raise EvidenceError("runtime plateau must contain exactly one well-formed row") + source = rows[0] + if ( + source["schema_version"] != "6" + or source["target"] != "wasix" + or source["status"] != "passed" + or source["nonce"] != nonce + ): + raise EvidenceError("runtime plateau did not pass for this WASIX lifecycle nonce") + for field in ("evidence_sha256", "freeze_receipt_sha256"): + if re.fullmatch(r"[0-9a-f]{64}", source[field]) is None: + raise EvidenceError(f"runtime plateau has an invalid {field}") + reconnect_requested = parse_positive_int( + source["reconnect_requested"], "reconnect_requested" + ) + reconnect_completed = parse_positive_int( + source["reconnect_completed"], "reconnect_completed" + ) + if reconnect_requested != requested or reconnect_completed != requested: + raise EvidenceError("runtime plateau does not bind the complete reconnect wave") + parsed: dict[str, int | str] = { + "evidence_sha256": source["evidence_sha256"], + "freeze_receipt_sha256": source["freeze_receipt_sha256"], + "reconnect_start_mono_ns": parse_positive_int( + source["reconnect_start_mono_ns"], "reconnect_start_mono_ns" + ), + "reconnect_end_mono_ns": parse_positive_int( + source["reconnect_end_mono_ns"], "reconnect_end_mono_ns" + ), + "readiness_fence_mono_ns": parse_positive_int( + source["readiness_fence_mono_ns"], "readiness_fence_mono_ns" + ), + "post_quiescence_fence_mono_ns": parse_positive_int( + source["post_quiescence_fence_mono_ns"], + "post_quiescence_fence_mono_ns", + ), + } + return parsed, hashlib.sha256(raw).hexdigest() + + +def validate( + *, + input_path: Path, + runtime_plateau_path: Path, + target: str, + nonce: str, + server_pid: int, + requested: int, + every: int, + min_quiescence_seconds: float, + max_growth: dict[str, int], + max_late_slope: dict[str, float], +) -> dict[str, str | int | float]: + runtime, runtime_plateau_sha256 = parse_runtime_plateau( + runtime_plateau_path, nonce=nonce, requested=requested + ) + rows, input_sha256 = parse_checkpoints( + input_path, + nonce=nonce, + server_pid=server_pid, + requested=requested, + every=every, + min_quiescence_seconds=min_quiescence_seconds, + ) + if int(rows[0]["monotonic_before_ns"]) < int(runtime["readiness_fence_mono_ns"]): + raise EvidenceError("baseline memory capture precedes the readiness runtime fence") + if int(rows[0]["monotonic_after_ns"]) > int(runtime["reconnect_start_mono_ns"]): + raise EvidenceError("baseline memory capture overlaps the reconnect wave") + if int(rows[-1]["quiescence_start_ns"]) < int(runtime["reconnect_end_mono_ns"]): + raise EvidenceError("terminal quiescence overlaps the reconnect wave") + if int(rows[-1]["monotonic_before_ns"]) < int( + runtime["post_quiescence_fence_mono_ns"] + ): + raise EvidenceError("terminal memory capture precedes the post-quiescence fence") + tail_start_index = next( + index + for index, row in enumerate(rows) + if int(row["completed_reconnects"]) * 2 >= requested + ) + tail = rows[tail_start_index:] + if len(tail) < 3: + raise EvidenceError("late-tail slope requires at least three checkpoints") + result: dict[str, str | int | float] = { + "schema_version": RESULT_SCHEMA, + "target": target, + "status": "passed", + "detail": "bounded-full-run-and-late-tail-quiescent-memory-growth", + "claim_scope": CLAIM_SCOPE, + "input_path": str(input_path), + "input_sha256": input_sha256, + "runtime_plateau_path": str(runtime_plateau_path), + "runtime_plateau_sha256": runtime_plateau_sha256, + "runtime_evidence_sha256": runtime["evidence_sha256"], + "runtime_freeze_receipt_sha256": runtime["freeze_receipt_sha256"], + "validator_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + "nonce": nonce, + "server_pid": server_pid, + "server_birth_identity": rows[0]["server_birth_identity"], + "checkpoint_count": len(rows), + "requested_reconnects": requested, + "checkpoint_every": every, + "min_quiescence_seconds": f"{min_quiescence_seconds:g}", + "max_pss_growth_kib": max_growth["pss_kib"], + "max_pss_anon_growth_kib": max_growth["pss_anon_kib"], + "max_heap_growth_kib": max_growth["heap_pss_kib"], + "max_late_pss_slope_kib_per_1000_reconnects": f"{max_late_slope['pss_kib']:g}", + "max_late_pss_anon_slope_kib_per_1000_reconnects": f"{max_late_slope['pss_anon_kib']:g}", + "max_late_heap_slope_kib_per_1000_reconnects": f"{max_late_slope['heap_pss_kib']:g}", + "max_capture_elapsed_ns": max(int(row["capture_elapsed_ns"]) for row in rows), + "tail_start_reconnects": tail[0]["completed_reconnects"], + } + failures: list[str] = [] + for metric in METRICS: + baseline = int(rows[0][metric]) + final = int(rows[-1][metric]) + peak = max(int(row[metric]) for row in rows) + tail_baseline = int(tail[0][metric]) + tail_peak = max(int(row[metric]) for row in tail) + full_peak_growth = max(0, peak - baseline) + final_growth = max(0, final - baseline) + late_peak_growth = max(0, tail_peak - tail_baseline) + slope = theil_sen_per_1000( + [ + (int(row["completed_reconnects"]), int(row[metric])) + for row in tail + ] + ) + result.update( + { + f"baseline_{metric}": baseline, + f"peak_{metric}": peak, + f"final_{metric}": final, + f"full_peak_growth_{metric}": full_peak_growth, + f"final_growth_{metric}": final_growth, + f"late_peak_growth_{metric}": late_peak_growth, + f"late_theil_sen_{metric}_per_1000_reconnects": f"{slope:.6f}", + } + ) + budget = max_growth[metric] + if max(full_peak_growth, final_growth, late_peak_growth) > budget: + failures.append( + f"{metric} growth exceeds {budget} KiB " + f"(peak={full_peak_growth},final={final_growth},late={late_peak_growth})" + ) + slope_budget = max_late_slope[metric] + if slope > slope_budget: + failures.append( + f"{metric} late Theil-Sen slope {slope:.6f} KiB/1000 " + f"exceeds {slope_budget:g}" + ) + if failures: + result["status"] = "failed" + result["detail"] = "; ".join(failures) + return result + + +def failed_result(args: argparse.Namespace, detail: str) -> dict[str, str | int]: + result: dict[str, str | int] = {field: "" for field in RESULT_FIELDS} + result.update( + schema_version=RESULT_SCHEMA, + target=args.target, + status="failed", + detail=detail.replace("\t", " ").replace("\r", " ").replace("\n", " "), + claim_scope=CLAIM_SCOPE, + input_path=str(args.input), + runtime_plateau_path=str(args.runtime_plateau), + validator_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + nonce=args.nonce, + server_pid=args.server_pid, + requested_reconnects=args.requested_reconnects, + checkpoint_every=args.checkpoint_every, + min_quiescence_seconds=args.min_quiescence_seconds, + max_pss_growth_kib=args.max_pss_growth_kib, + max_pss_anon_growth_kib=args.max_pss_anon_growth_kib, + max_heap_growth_kib=args.max_heap_growth_kib, + max_late_pss_slope_kib_per_1000_reconnects=( + args.max_late_pss_slope_kib_per_1000_reconnects + ), + max_late_pss_anon_slope_kib_per_1000_reconnects=( + args.max_late_pss_anon_slope_kib_per_1000_reconnects + ), + max_late_heap_slope_kib_per_1000_reconnects=( + args.max_late_heap_slope_kib_per_1000_reconnects + ), + ) + return result + + +def write_result(path: Path, result: dict[str, str | int | float]) -> None: + if path.exists() and (path.is_symlink() or not path.is_file()): + raise EvidenceError(f"output is not a regular non-symlink file: {path}") + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(f".{path.name}.tmp.{os.getpid()}") + try: + with temporary.open("w", encoding="utf-8", newline="") as stream: + writer = csv.DictWriter( + stream, + fieldnames=RESULT_FIELDS, + delimiter="\t", + lineterminator="\n", + extrasaction="raise", + ) + writer.writeheader() + writer.writerow({field: result.get(field, "") for field in RESULT_FIELDS}) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + finally: + temporary.unlink(missing_ok=True) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Validate quiescent WASIX reconnect memory checkpoints." + ) + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--runtime-plateau", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--target", default="wasix") + parser.add_argument("--nonce", required=True) + parser.add_argument("--server-pid", type=int, required=True) + parser.add_argument("--requested-reconnects", type=int, required=True) + parser.add_argument("--checkpoint-every", type=int, required=True) + parser.add_argument("--min-quiescence-seconds", type=float, required=True) + parser.add_argument("--max-pss-growth-kib", type=int, required=True) + parser.add_argument("--max-pss-anon-growth-kib", type=int, required=True) + parser.add_argument("--max-heap-growth-kib", type=int, required=True) + parser.add_argument( + "--max-late-pss-slope-kib-per-1000-reconnects", type=float, required=True + ) + parser.add_argument( + "--max-late-pss-anon-slope-kib-per-1000-reconnects", + type=float, + required=True, + ) + parser.add_argument( + "--max-late-heap-slope-kib-per-1000-reconnects", type=float, required=True + ) + args = parser.parse_args() + if args.target != "wasix": + parser.error("--target must be wasix") + if NONCE_RE.fullmatch(args.nonce) is None: + parser.error("--nonce must contain exactly 32 lowercase hexadecimal digits") + if args.server_pid <= 0 or args.requested_reconnects <= 0: + parser.error("server PID and reconnect count must be positive") + if args.checkpoint_every <= 0 or args.checkpoint_every >= args.requested_reconnects: + parser.error("checkpoint interval must be positive and smaller than reconnect count") + if not math.isfinite(args.min_quiescence_seconds) or args.min_quiescence_seconds <= 0: + parser.error("minimum quiescence must be positive") + if min( + args.max_pss_growth_kib, + args.max_pss_anon_growth_kib, + args.max_heap_growth_kib, + ) < 0: + parser.error("growth budgets must be nonnegative") + slope_budgets = ( + args.max_late_pss_slope_kib_per_1000_reconnects, + args.max_late_pss_anon_slope_kib_per_1000_reconnects, + args.max_late_heap_slope_kib_per_1000_reconnects, + ) + if any(not math.isfinite(value) or value < 0 for value in slope_budgets): + parser.error("late-tail slope budgets must be nonnegative") + return args + + +def main() -> int: + args = parse_args() + try: + result = validate( + input_path=args.input, + runtime_plateau_path=args.runtime_plateau, + target=args.target, + nonce=args.nonce, + server_pid=args.server_pid, + requested=args.requested_reconnects, + every=args.checkpoint_every, + min_quiescence_seconds=args.min_quiescence_seconds, + max_growth={ + "pss_kib": args.max_pss_growth_kib, + "pss_anon_kib": args.max_pss_anon_growth_kib, + "heap_pss_kib": args.max_heap_growth_kib, + }, + max_late_slope={ + "pss_kib": args.max_late_pss_slope_kib_per_1000_reconnects, + "pss_anon_kib": ( + args.max_late_pss_anon_slope_kib_per_1000_reconnects + ), + "heap_pss_kib": ( + args.max_late_heap_slope_kib_per_1000_reconnects + ), + }, + ) + except (EvidenceError, OSError, csv.Error) as error: + result = failed_result(args, str(error)) + try: + write_result(args.output, result) + except (EvidenceError, OSError) as error: + print(error, file=sys.stderr) + return 1 + if result["status"] != "passed": + print(result["detail"], file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-memory-plateau.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-memory-plateau.test.py new file mode 100755 index 00000000..026517a2 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-memory-plateau.test.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + + +SCRIPT = Path(__file__).with_name("validate-wasix-lifecycle-memory-plateau.py") +NONCE = "0123456789abcdef0123456789abcdef" +FIELDS = ( + "schema_version", + "nonce", + "sequence", + "stage", + "completed_reconnects", + "requested_reconnects", + "checkpoint_every", + "quiescence_seconds", + "quiescence_start_ns", + "quiescence_end_ns", + "monotonic_before_ns", + "monotonic_after_ns", + "capture_elapsed_ns", + "server_pid", + "server_birth_identity", + "pss_kib", + "pss_anon_kib", + "anonymous_kib", + "heap_pss_kib", + "heap_private_kib", + "heap_mappings", + "status", +) + + +class LifecycleMemoryPlateauTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.raw = self.root / "checkpoints.tsv" + self.output = self.root / "result.tsv" + self.runtime_plateau = self.root / "runtime-plateau.tsv" + self.runtime_plateau.write_text( + "schema_version\ttarget\tstatus\tnonce\tevidence_sha256\t" + "freeze_receipt_sha256\treconnect_requested\treconnect_completed\t" + "reconnect_start_mono_ns\treconnect_end_mono_ns\t" + "readiness_fence_mono_ns\tpost_quiescence_fence_mono_ns\n" + f"6\twasix\tpassed\t{NONCE}\t{'1' * 64}\t{'2' * 64}\t" + "100\t100\t3005000000\t13030000000\t2500000000\t15040500000\n", + encoding="utf-8", + ) + + def tearDown(self) -> None: + self.temporary.cleanup() + + def rows(self) -> list[dict[str, str | int]]: + rows: list[dict[str, str | int]] = [] + for sequence, completed in enumerate((0, 25, 50, 75, 100)): + rows.append( + { + "schema_version": ( + "oliphaunt.wasix-postmaster.lifecycle-memory-checkpoint.v1" + ), + "nonce": NONCE, + "sequence": sequence, + "stage": ( + "baseline-fenced" + if completed == 0 + else "final-fenced" + if completed == 100 + else "wave-quiescent" + ), + "completed_reconnects": completed, + "requested_reconnects": 100, + "checkpoint_every": 25, + "quiescence_seconds": 2, + "quiescence_start_ns": 1_000_000_000 + sequence * 3_010_000_000, + "quiescence_end_ns": 3_000_000_000 + sequence * 3_010_000_000, + "monotonic_before_ns": 3_001_000_000 + sequence * 3_010_000_000, + "monotonic_after_ns": 3_002_000_000 + sequence * 3_010_000_000, + "capture_elapsed_ns": 1_000_000, + "server_pid": 4242, + "server_birth_identity": "linux-starttime:123456", + "pss_kib": (100_000, 100_400, 100_300, 100_450, 100_350)[sequence], + "pss_anon_kib": (70_000, 70_200, 70_100, 70_250, 70_150)[sequence], + "anonymous_kib": (70_100, 70_300, 70_200, 70_350, 70_250)[sequence], + "heap_pss_kib": (10_000, 10_100, 10_050, 10_125, 10_075)[sequence], + "heap_private_kib": (10_000, 10_100, 10_050, 10_125, 10_075)[sequence], + "heap_mappings": 1, + "status": "passed", + } + ) + return rows + + def write_rows(self, rows: list[dict[str, str | int]]) -> None: + with self.raw.open("w", encoding="utf-8", newline="") as stream: + writer = csv.DictWriter( + stream, fieldnames=FIELDS, delimiter="\t", lineterminator="\n" + ) + writer.writeheader() + writer.writerows(rows) + + def run_validator(self, *extra: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ( + sys.executable, + str(SCRIPT), + "--input", + str(self.raw), + "--output", + str(self.output), + "--runtime-plateau", + str(self.runtime_plateau), + "--nonce", + NONCE, + "--server-pid", + "4242", + "--requested-reconnects", + "100", + "--checkpoint-every", + "25", + "--min-quiescence-seconds", + "1", + "--max-pss-growth-kib", + "512", + "--max-pss-anon-growth-kib", + "256", + "--max-heap-growth-kib", + "128", + "--max-late-pss-slope-kib-per-1000-reconnects", + "2048", + "--max-late-pss-anon-slope-kib-per-1000-reconnects", + "2048", + "--max-late-heap-slope-kib-per-1000-reconnects", + "1024", + *extra, + ), + check=False, + text=True, + capture_output=True, + ) + + def result(self) -> dict[str, str]: + with self.output.open(encoding="utf-8", newline="") as stream: + rows = list(csv.DictReader(stream, delimiter="\t", strict=True)) + self.assertEqual(len(rows), 1) + return rows[0] + + def test_accepts_exact_bounded_checkpoint_schedule(self) -> None: + self.write_rows(self.rows()) + completed = self.run_validator() + self.assertEqual(completed.returncode, 0, completed.stderr) + result = self.result() + self.assertEqual(result["status"], "passed") + self.assertEqual(result["checkpoint_count"], "5") + self.assertEqual(result["tail_start_reconnects"], "50") + self.assertEqual(result["full_peak_growth_pss_kib"], "450") + self.assertEqual(result["late_peak_growth_heap_pss_kib"], "75") + self.assertRegex(result["input_sha256"], r"^[0-9a-f]{64}$") + + def test_rejects_growth_even_when_terminal_sample_falls(self) -> None: + rows = self.rows() + rows[3]["pss_kib"] = 100_900 + rows[4]["pss_kib"] = 100_100 + self.write_rows(rows) + completed = self.run_validator() + self.assertEqual(completed.returncode, 1) + result = self.result() + self.assertEqual(result["status"], "failed") + self.assertIn("pss_kib growth exceeds", result["detail"]) + self.assertEqual(result["full_peak_growth_pss_kib"], "900") + + def test_rejects_small_but_monotonic_late_tail_leak(self) -> None: + rows = self.rows() + for index, value in zip((2, 3, 4), (100_300, 100_350, 100_400), strict=True): + rows[index]["pss_kib"] = value + self.write_rows(rows) + completed = self.run_validator( + "--max-late-pss-slope-kib-per-1000-reconnects", "1500" + ) + self.assertEqual(completed.returncode, 1) + result = self.result() + self.assertEqual(result["status"], "failed") + self.assertIn("late Theil-Sen slope", result["detail"]) + self.assertLessEqual(int(result["full_peak_growth_pss_kib"]), 512) + + def test_rejects_missing_intermediate_checkpoint(self) -> None: + rows = self.rows() + del rows[2] + for sequence, row in enumerate(rows): + row["sequence"] = sequence + self.write_rows(rows) + completed = self.run_validator() + self.assertEqual(completed.returncode, 1) + result = self.result() + self.assertEqual(result["status"], "failed") + self.assertIn("exact checkpoint schedule", result["detail"]) + + def test_rejects_server_pid_reuse(self) -> None: + rows = self.rows() + rows[3]["server_birth_identity"] = "linux-starttime:999999" + self.write_rows(rows) + completed = self.run_validator() + self.assertEqual(completed.returncode, 1) + result = self.result() + self.assertEqual(result["status"], "failed") + self.assertIn("changes the server birth identity", result["detail"]) + + def test_rejects_terminal_capture_before_bound_runtime_fence(self) -> None: + self.write_rows(self.rows()) + text = self.runtime_plateau.read_text(encoding="utf-8") + self.runtime_plateau.write_text( + text.replace("15040500000", "16000000000"), encoding="utf-8" + ) + completed = self.run_validator() + self.assertEqual(completed.returncode, 1) + result = self.result() + self.assertEqual(result["status"], "failed") + self.assertIn("precedes the post-quiescence fence", result["detail"]) + + def test_rejects_short_quiescence_window(self) -> None: + rows = self.rows() + rows[1]["quiescence_seconds"] = 0 + self.write_rows(rows) + completed = self.run_validator() + self.assertEqual(completed.returncode, 1) + result = self.result() + self.assertEqual(result["status"], "failed") + self.assertIn("insufficient quiescence", result["detail"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-plateau.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-plateau.py new file mode 100755 index 00000000..ed459f94 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-plateau.py @@ -0,0 +1,1607 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import csv +from dataclasses import dataclass +import hashlib +import os +from pathlib import Path +import re +import stat +import sys +from typing import Iterable + + +RUNTIME_CONTEXT = "wasix-runtime-context-v1" +RUNTIME_STATE = "wasix-runtime-state-v1" +RUNTIME_FENCE = "wasix-runtime-fence-v1" +HARNESS_PHASE = "wasix-runtime-phase-v1" +HARNESS_STABILIZATION = "wasix-runtime-stabilization-v1" +HARNESS_RECONNECT_CHURN = "wasix-runtime-reconnect-churn-v1" +PHASE_ORDER = ( + "cold-readiness", + "maintenance-stabilization", + "readiness", + "reconnect-churn", + "post-quiescence", + "complete", +) +FENCE_PHASE_ORDER = ("readiness", "post-quiescence") +WAIT_KINDS = ( + "wait-registry.epoll_wait.pending", + "wait-registry.futex_wait.pending", +) +CONTEXT_FIELDS = ("seq", "wait_kind", "observer_pid", "observer_tid") +STATE_FIELDS = ( + "seq", + "mono_ns", + "registered_processes", + "active_tasks", + "process_topology_nodes", + "process_child_edges", + "process_thread_entries", + "process_live_threads", + "process_pending_child_publications", + "process_execution_leases", + "process_quiescence_wakers", + "process_retiring_nodes", + "runtime_state_active", + "runtime_state_stale", + "runtime_state_slots", + "runtime_state_observer_registered", + "private_futexes", + "private_futex_waiters", + "private_futex_wakers", + "shared_futexes", + "shared_futex_waiters", + "shared_futex_wakers", + "epoll_states", + "epoll_subscriptions", + "epoll_ready_items", + "epoll_pending_subscriptions", + "epoll_enqueued_subscriptions", + "epoll_join_guards", + "epoll_close_registrations", + "shared_registry_active", + "shared_registry_stale", + "shared_registry_slots", + "shared_mappings", + "guest_fd_entries", +) +PHASE_FIELDS = ("nonce", "seq", "mono_ns", "phase", "observer_pid") +FENCE_FIELDS = ( + "nonce", + "seq", + "mono_ns", + "phase", + "observer_pid", + "observer_tid", + "request_seq", +) +STABILIZATION_FIELDS = ( + "nonce", + "method", + "before_writes", + "after_writes", + "before_write_bytes", + "after_write_bytes", + "before_stats_reset", + "after_stats_reset", + "target_lsn", + "observed_flush_lsn", + "wal_writer_delay_ms", + "start_mono_ns", + "end_mono_ns", + "status", + "observer_pid", +) +RECONNECT_CHURN_FIELDS = ( + "nonce", + "requested", + "completed", + "command_sha256", + "client_sha256", + "connection_sha256", + "start_mono_ns", + "end_mono_ns", + "status", + "observer_pid", +) +RECONNECT_COMMAND_CONTRACT = ( + b"oliphaunt.wasix-postmaster.lifecycle-reconnect.v1\0" + b"PGCONNECT_TIMEOUT=5\0psql\0-X\0-qAt\0-v\0ON_ERROR_STOP=1\0-c\0select 1\0" +) +RECONNECT_COMMAND_SHA256 = hashlib.sha256(RECONNECT_COMMAND_CONTRACT).hexdigest() +COUNT_FIELDS = STATE_FIELDS[2:] +OUTPUT_FIELDS = ( + "schema_version", + "target", + "status", + "detail", + "claim_scope", + "baseline_assumption", + "baseline_policy_id", + "baseline_policy_status", + "baseline_policy_sha256", + "baseline_binding_sha256", + "nonce", + "observer_pid", + "freeze_receipt_sha256", + "evidence_sha256", + "commit_ack_sha256", + "fence_end_offset", + "wait_kind", + "min_samples", + "min_span_ns", + "expected_interval_ms", + "max_sample_gap_ns", + "cold_readiness_phase_mono_ns", + "maintenance_stabilization_phase_mono_ns", + "readiness_phase_mono_ns", + "stabilization_method", + "stabilization_before_writes", + "stabilization_after_writes", + "stabilization_before_write_bytes", + "stabilization_after_write_bytes", + "stabilization_stats_reset", + "stabilization_target_lsn", + "stabilization_observed_flush_lsn", + "stabilization_wal_writer_delay_ms", + "stabilization_start_mono_ns", + "stabilization_end_mono_ns", + "stabilization_elapsed_ns", + "reconnect_requested", + "reconnect_completed", + "reconnect_command_sha256", + "reconnect_client_sha256", + "reconnect_connection_sha256", + "reconnect_start_mono_ns", + "reconnect_end_mono_ns", + "reconnect_elapsed_ns", + "readiness_fence_sequence", + "readiness_fence_mono_ns", + "post_quiescence_fence_sequence", + "post_quiescence_fence_mono_ns", + "readiness_samples", + "readiness_span_ns", + "readiness_max_gap_ns", + "post_quiescence_samples", + "post_quiescence_span_ns", + "post_quiescence_max_gap_ns", + "cold_readiness_samples", + "cold_readiness_span_ns", + "cold_readiness_max_gap_ns", + "cold_readiness_observer_pids", + "readiness_observer_pids", + "post_quiescence_observer_pids", + *( + output_field + for field in COUNT_FIELDS + for output_field in ( + f"cold_readiness_{field}", + f"readiness_{field}", + f"post_quiescence_{field}", + ) + ), +) +NONCE_RE = re.compile(r"^[0-9a-f]{32}$") +SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +U64_MAX = (1 << 64) - 1 +FREEZE_RECEIPT_FIELDS = ( + "schema_version", + "raw_log", + "raw_observed_size", + "commit_ack", + "commit_ack_sha256", + "fence_end_offset", + "frozen_log", + "frozen_size", + "sha256", + "nonce", + "observer_pid", + "fence_sequence", + "fence_mono_ns", + "complete_phase_sequence", + "complete_phase_mono_ns", +) +COMMIT_ACK_FIELDS = ( + "nonce", + "seq", + "mono_ns", + "phase", + "observer_pid", + "observer_tid", + "request_seq", + "fence_end_offset", +) +BASELINE_POLICY_FIELDS = ( + "schema_version", + "policy_id", + "policy_status", + "claim_scope", + "baseline_assumption", + "field", + "rule", + "minimum", + "maximum", +) +BASELINE_BINDING_FIELDS = ( + "schema_version", + "policy_id", + "policy_sha256", + "policy_status", + "claim_scope", + "baseline_assumption", + "postgres_major", + "runtime_footprint", + "runtime_footprint_sha256", + "durability_profile", + "durability_profile_sha256", + "postgres_profile_resolution_identity", + "runtime_mode", + "wasmer_bin_sha256", + "postgres_module_sha256", + "carrier_manifest_sha256", + "carrier_receipt_sha256", + "carrier_payload_inventory_sha256", +) +CLAIM_SCOPE = "relative-to-stabilized-baseline" +BASELINE_ASSUMPTION = ( + "readiness-is-stabilized-idle-postmaster-state;" + "absolute-pss-budget-governs-legitimate-baseline-size" +) + + +class EvidenceError(ValueError): + pass + + +@dataclass(frozen=True) +class RuntimeContext: + sequence: int + wait_kind: str + observer_pid: int + observer_tid: int + + +@dataclass(frozen=True) +class RuntimeSample: + sequence: int + mono_ns: int + wait_kind: str + observer_pid: int + observer_tid: int + phase: str | None + counts: tuple[int, ...] + + +@dataclass(frozen=True) +class StablePlateau: + samples: int + span_ns: int + max_gap_ns: int + first_mono_ns: int + last_mono_ns: int + observer_pids: tuple[int, ...] + counts: tuple[int, ...] + + +@dataclass(frozen=True) +class HarnessPhase: + sequence: int + mono_ns: int + phase: str + observer_pid: int + + +@dataclass(frozen=True) +class WalWriterStabilization: + method: str + before_writes: int + after_writes: int + before_write_bytes: int + after_write_bytes: int + stats_reset: int + target_lsn: str + observed_flush_lsn: str + wal_writer_delay_ms: int + start_mono_ns: int + end_mono_ns: int + observer_pid: int + + @property + def elapsed_ns(self) -> int: + return self.end_mono_ns - self.start_mono_ns + + +@dataclass(frozen=True) +class ReconnectChurn: + requested: int + completed: int + command_sha256: str + client_sha256: str + connection_sha256: str + start_mono_ns: int + end_mono_ns: int + observer_pid: int + + @property + def elapsed_ns(self) -> int: + return self.end_mono_ns - self.start_mono_ns + + +@dataclass(frozen=True) +class RuntimeFence: + phase: str + request_sequence: int + sample: RuntimeSample + + +@dataclass(frozen=True) +class LifecycleEvidence: + samples: list[RuntimeSample] + fences: dict[str, RuntimeFence] + phases: dict[str, HarnessPhase] + stabilization: WalWriterStabilization + reconnect_churn: ReconnectChurn + + +@dataclass(frozen=True) +class FreezeBinding: + receipt_sha256: str + evidence_sha256: str + commit_ack_sha256: str + fence_end_offset: int + fence_sequence: int + fence_mono_ns: int + + +@dataclass(frozen=True) +class BaselineConstraint: + field: str + rule: str + minimum: int + maximum: int + + +@dataclass(frozen=True) +class BaselinePolicy: + policy_id: str + policy_status: str + claim_scope: str + baseline_assumption: str + sha256: str + constraints: tuple[BaselineConstraint, ...] + + +@dataclass(frozen=True) +class BaselineBinding: + sha256: str + policy: BaselinePolicy + + +@dataclass(frozen=True) +class LifecycleValidationOutcome: + row: dict[str, str | int] + error: str | None + + @property + def passed(self) -> bool: + return self.error is None + + +def regular_file_identity(metadata: os.stat_result) -> tuple[int, ...]: + return ( + metadata.st_dev, + metadata.st_ino, + metadata.st_mode, + metadata.st_uid, + metadata.st_gid, + metadata.st_nlink, + metadata.st_size, + metadata.st_mtime_ns, + metadata.st_ctime_ns, + ) + + +def read_regular_file(path: Path) -> bytes: + before = os.lstat(path) + if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode): + raise EvidenceError(f"evidence input is not a regular non-symlink file: {path}") + fd = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)) + try: + opened = os.fstat(fd) + before_identity = regular_file_identity(before) + opened_identity = regular_file_identity(opened) + if not stat.S_ISREG(opened.st_mode) or opened_identity != before_identity: + raise EvidenceError(f"evidence input changed while opening: {path}") + + def read_all() -> bytes: + chunks: list[bytes] = [] + while True: + chunk = os.read(fd, 1024 * 1024) + if not chunk: + break + chunks.append(chunk) + return b"".join(chunks) + + payload = read_all() + middle = os.fstat(fd) + middle_identity = regular_file_identity(middle) + if middle_identity != opened_identity or len(payload) != opened.st_size: + raise EvidenceError(f"evidence input changed while reading: {path}") + + # Metadata catches ordinary in-place writes, including same-size ones. + # A second byte-for-byte read also closes filesystems whose timestamp + # granularity is too coarse to expose a concurrent rewrite. + os.lseek(fd, 0, os.SEEK_SET) + confirmed = read_all() + after = os.fstat(fd) + after_identity = regular_file_identity(after) + if after_identity != opened_identity or confirmed != payload: + raise EvidenceError(f"evidence input changed while confirming read: {path}") + current = os.lstat(path) + if stat.S_ISLNK(current.st_mode) or regular_file_identity(current) != opened_identity: + raise EvidenceError(f"evidence input pathname changed while reading: {path}") + return payload + finally: + os.close(fd) + + +def parse_ordered_record(line: str, prefix: str, fields: tuple[str, ...], line_no: int) -> dict[str, str]: + parts = line.rstrip("\n").split("\t") + if len(parts) != len(fields) + 1 or parts[0] != prefix: + raise EvidenceError(f"line {line_no}: malformed {prefix} record") + parsed: dict[str, str] = {} + for token, field in zip(parts[1:], fields, strict=True): + expected = f"{field}=" + if not token.startswith(expected) or token == expected: + raise EvidenceError(f"line {line_no}: expected ordered field {field}") + parsed[field] = token[len(expected):] + return parsed + + +def parse_uint(value: str, field: str, line_no: int, *, positive: bool = False) -> int: + if not value.isascii() or not value.isdecimal(): + raise EvidenceError(f"line {line_no}: {field} must be an unsigned decimal integer") + parsed = int(value) + if parsed > (1 << 64) - 1 or (positive and parsed == 0): + qualifier = "positive " if positive else "" + raise EvidenceError(f"line {line_no}: {field} must be a {qualifier}u64") + return parsed + + +def parse_pg_lsn(value: str, field: str, line_no: int) -> int: + if not re.fullmatch(r"[0-9A-F]+/[0-9A-F]+", value): + raise EvidenceError(f"line {line_no}: {field} must be a canonical PostgreSQL LSN") + high_text, low_text = value.split("/", 1) + high = int(high_text, 16) + low = int(low_text, 16) + if high > 0xFFFFFFFF or low > 0xFFFFFFFF: + raise EvidenceError(f"line {line_no}: {field} exceeds the PostgreSQL LSN range") + return (high << 32) | low + + +def parse_exact_tsv_record( + line: str, prefix: str, fields: tuple[str, ...], description: str +) -> dict[str, str]: + parts = line.split("\t") + if len(parts) != len(fields) + 1 or parts[0] != prefix: + raise EvidenceError(f"malformed {description} record") + values: dict[str, str] = {} + for token, field in zip(parts[1:], fields, strict=True): + expected = f"{field}=" + if not token.startswith(expected) or token == expected: + raise EvidenceError(f"{description} expected ordered field {field}") + values[field] = token[len(expected):] + return values + + +def parse_baseline_policy(path: Path) -> BaselinePolicy: + raw = read_regular_file(path) + if not raw.endswith(b"\n"): + raise EvidenceError("lifecycle baseline policy must be newline terminated") + text = raw.decode("utf-8", errors="strict") + lines = text[:-1].split("\n") + if not lines or tuple(lines[0].split("\t")) != BASELINE_POLICY_FIELDS: + raise EvidenceError("lifecycle baseline policy has an unexpected ordered schema") + if len(lines) != len(COUNT_FIELDS) + 1: + raise EvidenceError("lifecycle baseline policy must contain one row per state field") + + metadata: tuple[str, str, str, str] | None = None + constraints: list[BaselineConstraint] = [] + for line_no, line in enumerate(lines[1:], 2): + values = line.split("\t") + if len(values) != len(BASELINE_POLICY_FIELDS) or any(value == "" for value in values): + raise EvidenceError(f"baseline policy line {line_no} does not match its schema") + row = dict(zip(BASELINE_POLICY_FIELDS, values, strict=True)) + if row["schema_version"] != "oliphaunt.wasix-postmaster.lifecycle-baseline-policy.v1": + raise EvidenceError("lifecycle baseline policy schema version is unsupported") + row_metadata = ( + row["policy_id"], + row["policy_status"], + row["claim_scope"], + row["baseline_assumption"], + ) + if metadata is None: + metadata = row_metadata + elif row_metadata != metadata: + raise EvidenceError("lifecycle baseline policy metadata changes between fields") + expected_field = COUNT_FIELDS[len(constraints)] + if row["field"] != expected_field: + raise EvidenceError( + f"baseline policy line {line_no} expected ordered field {expected_field}" + ) + if row["rule"] not in ("exact", "relative-equal"): + raise EvidenceError(f"baseline policy line {line_no} has an unsupported rule") + minimum = parse_uint(row["minimum"], "minimum", line_no) + maximum = parse_uint(row["maximum"], "maximum", line_no) + if minimum > maximum: + raise EvidenceError(f"baseline policy line {line_no} has minimum above maximum") + if row["rule"] == "exact" and minimum != maximum: + raise EvidenceError(f"baseline policy line {line_no} exact bounds differ") + constraints.append( + BaselineConstraint(row["field"], row["rule"], minimum, maximum) + ) + assert metadata is not None + policy_id, policy_status, claim_scope, baseline_assumption = metadata + if policy_status not in ("exploratory-unbounded", "qualification-bounded"): + raise EvidenceError( + "lifecycle baseline policy status must be exploratory-unbounded or qualification-bounded" + ) + if claim_scope != CLAIM_SCOPE: + raise EvidenceError("lifecycle baseline policy must use the relative claim scope") + if baseline_assumption != BASELINE_ASSUMPTION: + raise EvidenceError( + "lifecycle baseline policy must state the stabilized-baseline assumption" + ) + if policy_status == "qualification-bounded": + if not re.fullmatch( + r"pg18-idle-postmaster-stabilized-qualified-v[1-9][0-9]*", policy_id + ): + raise EvidenceError( + "qualification baseline must use a distinct qualified policy ID" + ) + for constraint in constraints: + if constraint.rule != "exact" or constraint.minimum != constraint.maximum: + raise EvidenceError( + "qualification baseline must freeze the exact exploratory " + f"observation for {constraint.field}" + ) + elif not re.fullmatch( + r"pg18-idle-postmaster-stabilized-exploratory-v[1-9][0-9]*", policy_id + ): + raise EvidenceError("exploratory baseline must use an idle-postmaster policy ID") + + required_exact = { + "process_pending_child_publications": 0, + "process_quiescence_wakers": 0, + "process_retiring_nodes": 0, + "runtime_state_stale": 0, + "runtime_state_observer_registered": 1, + "shared_registry_stale": 0, + } + for constraint in constraints: + if constraint.field in required_exact and ( + constraint.rule != "exact" + or constraint.minimum != required_exact[constraint.field] + or constraint.maximum != required_exact[constraint.field] + ): + raise EvidenceError( + f"baseline policy weakens unconditional {constraint.field} ownership" + ) + return BaselinePolicy( + policy_id=policy_id, + policy_status=policy_status, + claim_scope=claim_scope, + baseline_assumption=baseline_assumption, + sha256=hashlib.sha256(raw).hexdigest(), + constraints=tuple(constraints), + ) + + +def parse_baseline_binding(path: Path, policy: BaselinePolicy) -> BaselineBinding: + raw = read_regular_file(path) + if not raw.endswith(b"\n") or raw.count(b"\n") != 2: + raise EvidenceError("baseline binding must contain exactly two newline-terminated rows") + header, record, _ = raw.decode("utf-8", errors="strict").split("\n") + if tuple(header.split("\t")) != BASELINE_BINDING_FIELDS: + raise EvidenceError("baseline binding has an unexpected ordered schema") + values = record.split("\t") + if len(values) != len(BASELINE_BINDING_FIELDS) or any(value == "" for value in values): + raise EvidenceError("baseline binding row does not match its schema") + row = dict(zip(BASELINE_BINDING_FIELDS, values, strict=True)) + for field, value in row.items(): + if any(ord(character) < 0x20 or ord(character) == 0x7F for character in value): + raise EvidenceError(f"baseline binding {field} contains a control character") + if row["schema_version"] != "oliphaunt.wasix-postmaster.lifecycle-baseline-binding.v1": + raise EvidenceError("baseline binding schema version is unsupported") + for field, expected in ( + ("policy_id", policy.policy_id), + ("policy_sha256", policy.sha256), + ("policy_status", policy.policy_status), + ("claim_scope", policy.claim_scope), + ("baseline_assumption", policy.baseline_assumption), + ): + if row[field] != expected: + raise EvidenceError(f"baseline binding {field} does not match its policy") + if row["postgres_major"] != "18": + raise EvidenceError("baseline binding is not for PostgreSQL major 18") + if row["runtime_footprint"] not in ("none", "embedded-concurrent"): + raise EvidenceError("baseline binding runtime footprint is unsupported") + if row["durability_profile"] not in ("none", "safe"): + raise EvidenceError("baseline binding durability profile is unsupported") + if row["runtime_mode"] not in ("compiler", "sealed-headless"): + raise EvidenceError("baseline binding runtime mode is unsupported") + required_hashes = ("wasmer_bin_sha256", "postgres_module_sha256") + optional_hashes = ( + "runtime_footprint_sha256", + "durability_profile_sha256", + "postgres_profile_resolution_identity", + "carrier_manifest_sha256", + "carrier_receipt_sha256", + "carrier_payload_inventory_sha256", + ) + for field in required_hashes: + if not re.fullmatch(r"[0-9a-f]{64}", row[field]): + raise EvidenceError(f"baseline binding {field} is not a SHA-256") + for field in optional_hashes: + if row[field] != "none" and not re.fullmatch(r"[0-9a-f]{64}", row[field]): + raise EvidenceError(f"baseline binding {field} is neither none nor a SHA-256") + for id_field, hash_field in ( + ("runtime_footprint", "runtime_footprint_sha256"), + ("durability_profile", "durability_profile_sha256"), + ): + if (row[id_field] == "none") != (row[hash_field] == "none"): + raise EvidenceError( + f"baseline binding {id_field} and {hash_field} presence differs" + ) + profiles_named = ( + row["runtime_footprint"] != "none" or row["durability_profile"] != "none" + ) + if profiles_named != (row["postgres_profile_resolution_identity"] != "none"): + raise EvidenceError( + "baseline binding profile IDs and resolution identity presence differs" + ) + carrier_fields = ( + row["carrier_manifest_sha256"], + row["carrier_receipt_sha256"], + row["carrier_payload_inventory_sha256"], + ) + if row["runtime_mode"] == "sealed-headless" and "none" in carrier_fields: + raise EvidenceError("sealed baseline binding is missing a carrier identity") + if row["runtime_mode"] == "compiler" and carrier_fields != ("none", "none", "none"): + raise EvidenceError("compiler baseline binding unexpectedly names a sealed carrier") + return BaselineBinding(sha256=hashlib.sha256(raw).hexdigest(), policy=policy) + + +def enforce_baseline_policy( + policy: BaselinePolicy, + readiness: StablePlateau, + final: StablePlateau, +) -> None: + for index, constraint in enumerate(policy.constraints): + readiness_value = readiness.counts[index] + final_value = final.counts[index] + if not constraint.minimum <= readiness_value <= constraint.maximum: + raise EvidenceError( + f"readiness {constraint.field}={readiness_value} is outside baseline policy " + f"[{constraint.minimum},{constraint.maximum}]" + ) + if not constraint.minimum <= final_value <= constraint.maximum: + raise EvidenceError( + f"post-quiescence {constraint.field}={final_value} is outside baseline policy " + f"[{constraint.minimum},{constraint.maximum}]" + ) + if constraint.rule == "exact" and readiness_value != constraint.minimum: + raise EvidenceError( + f"baseline policy requires {constraint.field}={constraint.minimum}" + ) + if constraint.rule == "relative-equal" and readiness_value != final_value: + raise EvidenceError( + f"baseline policy requires equal readiness/post-quiescence {constraint.field}" + ) + + +def validate_freeze_binding( + receipt_path: Path, + log_path: Path, + frozen: bytes, + nonce: str, + observer_pid: int, +) -> FreezeBinding: + receipt_raw = read_regular_file(receipt_path) + if not receipt_raw.endswith(b"\n") or receipt_raw.count(b"\n") != 2: + raise EvidenceError("freeze receipt must contain exactly two newline-terminated rows") + receipt_text = receipt_raw.decode("utf-8", errors="strict") + header, record, _ = receipt_text.split("\n") + if tuple(header.split("\t")) != FREEZE_RECEIPT_FIELDS: + raise EvidenceError("freeze receipt has an unexpected ordered schema") + values = record.split("\t") + if len(values) != len(FREEZE_RECEIPT_FIELDS): + raise EvidenceError("freeze receipt row does not match its schema") + row = dict(zip(FREEZE_RECEIPT_FIELDS, values, strict=True)) + if row["schema_version"] != "oliphaunt.wasix-postmaster.lifecycle-freeze.v2": + raise EvidenceError("freeze receipt schema version is unsupported") + if Path(row["frozen_log"]).resolve() != log_path.resolve(): + raise EvidenceError("freeze receipt names a different frozen lifecycle log") + if row["nonce"] != nonce: + raise EvidenceError("freeze receipt nonce does not match the lifecycle nonce") + receipt_pid = parse_uint(row["observer_pid"], "observer_pid", 2, positive=True) + if receipt_pid != observer_pid: + raise EvidenceError("freeze receipt observer PID does not match the postmaster") + + frozen_size = parse_uint(row["frozen_size"], "frozen_size", 2, positive=True) + if frozen_size != len(frozen): + raise EvidenceError("freeze receipt size does not match the frozen lifecycle log") + evidence_sha256 = hashlib.sha256(frozen).hexdigest() + if not re.fullmatch(r"[0-9a-f]{64}", row["sha256"]): + raise EvidenceError("freeze receipt lifecycle SHA-256 is malformed") + if row["sha256"] != evidence_sha256: + raise EvidenceError("freeze receipt SHA-256 does not match the frozen lifecycle log") + + ack_path = Path(row["commit_ack"]) + ack_raw = read_regular_file(ack_path) + ack_sha256 = hashlib.sha256(ack_raw).hexdigest() + if not re.fullmatch(r"[0-9a-f]{64}", row["commit_ack_sha256"]): + raise EvidenceError("freeze receipt committed-ACK SHA-256 is malformed") + if row["commit_ack_sha256"] != ack_sha256: + raise EvidenceError("freeze receipt SHA-256 does not match the committed ACK") + if not ack_raw.endswith(b"\n") or ack_raw.count(b"\n") != 1: + raise EvidenceError("committed ACK must contain exactly one newline-terminated record") + ack_text = ack_raw[:-1].decode("ascii", errors="strict") + ack = parse_exact_tsv_record( + ack_text, + "wasix-runtime-fence-commit-v1", + COMMIT_ACK_FIELDS, + "committed ACK", + ) + if ack["nonce"] != nonce or ack["phase"] != "post-quiescence": + raise EvidenceError("committed ACK does not identify the final lifecycle fence") + ack_pid = parse_uint(ack["observer_pid"], "observer_pid", 1, positive=True) + ack_tid = parse_uint(ack["observer_tid"], "observer_tid", 1, positive=True) + request_sequence = parse_uint(ack["request_seq"], "request_seq", 1, positive=True) + if ack_pid != observer_pid or ack_tid == 0 or request_sequence != 2: + raise EvidenceError("committed ACK has a foreign lifecycle identity") + + fence_sequence = parse_uint(row["fence_sequence"], "fence_sequence", 2, positive=True) + fence_mono_ns = parse_uint(row["fence_mono_ns"], "fence_mono_ns", 2, positive=True) + fence_end_offset = parse_uint( + row["fence_end_offset"], "fence_end_offset", 2, positive=True + ) + if ( + parse_uint(ack["seq"], "seq", 1, positive=True) != fence_sequence + or parse_uint(ack["mono_ns"], "mono_ns", 1, positive=True) != fence_mono_ns + or parse_uint(ack["fence_end_offset"], "fence_end_offset", 1, positive=True) + != fence_end_offset + ): + raise EvidenceError("freeze receipt and committed ACK fence coordinates differ") + if fence_end_offset >= len(frozen): + raise EvidenceError("freeze receipt fence offset does not precede the closing marker") + fence = ( + "wasix-runtime-fence-v1" + f"\tnonce={nonce}\tseq={fence_sequence}\tmono_ns={fence_mono_ns}" + f"\tphase=post-quiescence\tobserver_pid={observer_pid}" + f"\tobserver_tid={ack_tid}\trequest_seq=2\n" + ).encode("ascii") + if not frozen[:fence_end_offset].endswith(fence): + raise EvidenceError("freeze receipt offset does not end at its committed runtime fence") + complete_sequence = parse_uint( + row["complete_phase_sequence"], "complete_phase_sequence", 2, positive=True + ) + complete_mono_ns = parse_uint( + row["complete_phase_mono_ns"], "complete_phase_mono_ns", 2, positive=True + ) + complete = ( + "wasix-runtime-phase-v1" + f"\tnonce={nonce}\tseq={complete_sequence}\tmono_ns={complete_mono_ns}" + f"\tphase=complete\tobserver_pid={observer_pid}\n" + ).encode("ascii") + if frozen[fence_end_offset:] != complete: + raise EvidenceError("frozen lifecycle suffix is not the receipt-bound complete marker") + return FreezeBinding( + receipt_sha256=hashlib.sha256(receipt_raw).hexdigest(), + evidence_sha256=evidence_sha256, + commit_ack_sha256=ack_sha256, + fence_end_offset=fence_end_offset, + fence_sequence=fence_sequence, + fence_mono_ns=fence_mono_ns, + ) + + +def parse_evidence(lines: Iterable[str], nonce: str, observer_pid: int) -> LifecycleEvidence: + phase_index = -1 + active_phase: str | None = None + pending_context: RuntimeContext | None = None + last_runtime_sequence: int | None = None + last_runtime_mono_ns: int | None = None + last_phase_sequence = 0 + last_phase_mono_ns: int | None = None + samples: list[RuntimeSample] = [] + fences: dict[str, RuntimeFence] = {} + phases: dict[str, HarnessPhase] = {} + stabilization: WalWriterStabilization | None = None + reconnect_churn: ReconnectChurn | None = None + last_complete_sample: RuntimeSample | None = None + final_cutoff_reached = False + + for line_no, line in enumerate(lines, 1): + if line.startswith(HARNESS_PHASE + "\t"): + if pending_context is not None: + raise EvidenceError(f"line {line_no}: phase marker split a runtime record") + record = parse_ordered_record(line, HARNESS_PHASE, PHASE_FIELDS, line_no) + if record["nonce"] != nonce: + raise EvidenceError(f"line {line_no}: foreign lifecycle nonce") + sequence = parse_uint(record["seq"], "seq", line_no, positive=True) + mono_ns = parse_uint(record["mono_ns"], "mono_ns", line_no, positive=True) + marker_pid = parse_uint(record["observer_pid"], "observer_pid", line_no, positive=True) + if sequence != last_phase_sequence + 1: + raise EvidenceError(f"line {line_no}: nonconsecutive harness phase sequence") + if last_phase_mono_ns is not None and mono_ns <= last_phase_mono_ns: + raise EvidenceError(f"line {line_no}: nonmonotonic harness phase timestamp") + if marker_pid != observer_pid: + raise EvidenceError(f"line {line_no}: observer PID changed") + phase_index += 1 + if phase_index >= len(PHASE_ORDER) or record["phase"] != PHASE_ORDER[phase_index]: + raise EvidenceError(f"line {line_no}: unexpected lifecycle phase order") + phase = record["phase"] + if phase == "reconnect-churn" and "readiness" not in fences: + raise EvidenceError(f"line {line_no}: readiness phase has no writer fence") + if phase == "complete" and "post-quiescence" not in fences: + raise EvidenceError(f"line {line_no}: post-quiescence phase has no writer fence") + last_phase_sequence = sequence + last_phase_mono_ns = mono_ns + phases[phase] = HarnessPhase( + sequence=sequence, + mono_ns=mono_ns, + phase=phase, + observer_pid=marker_pid, + ) + active_phase = phase + last_complete_sample = None + if active_phase == "complete": + break + continue + + if line.startswith(HARNESS_STABILIZATION + "\t"): + if pending_context is not None: + raise EvidenceError( + f"line {line_no}: stabilization record split a runtime record" + ) + if active_phase != "maintenance-stabilization": + raise EvidenceError( + f"line {line_no}: stabilization record is outside its phase" + ) + if stabilization is not None: + raise EvidenceError(f"line {line_no}: duplicate stabilization record") + record = parse_ordered_record( + line, HARNESS_STABILIZATION, STABILIZATION_FIELDS, line_no + ) + if record["nonce"] != nonce: + raise EvidenceError(f"line {line_no}: foreign stabilization nonce") + if record["method"] != "pg_log_standby_snapshot": + raise EvidenceError(f"line {line_no}: unsupported stabilization method") + if record["status"] != "passed": + raise EvidenceError(f"line {line_no}: stabilization status is not passed") + before_writes = parse_uint(record["before_writes"], "before_writes", line_no) + after_writes = parse_uint(record["after_writes"], "after_writes", line_no) + before_write_bytes = parse_uint( + record["before_write_bytes"], "before_write_bytes", line_no + ) + after_write_bytes = parse_uint( + record["after_write_bytes"], "after_write_bytes", line_no + ) + if after_writes <= before_writes: + raise EvidenceError( + f"line {line_no}: WAL writer write count did not increase" + ) + if after_write_bytes <= before_write_bytes: + raise EvidenceError( + f"line {line_no}: WAL writer byte count did not increase" + ) + before_stats_reset = parse_uint( + record["before_stats_reset"], "before_stats_reset", line_no, positive=True + ) + after_stats_reset = parse_uint( + record["after_stats_reset"], "after_stats_reset", line_no, positive=True + ) + if before_stats_reset != after_stats_reset: + raise EvidenceError( + f"line {line_no}: pg_stat_io stats_reset changed during stabilization" + ) + target_lsn = parse_pg_lsn(record["target_lsn"], "target_lsn", line_no) + observed_flush_lsn = parse_pg_lsn( + record["observed_flush_lsn"], "observed_flush_lsn", line_no + ) + if target_lsn == 0: + raise EvidenceError(f"line {line_no}: target LSN is zero") + if observed_flush_lsn < target_lsn: + raise EvidenceError( + f"line {line_no}: observed flush LSN is behind the target LSN" + ) + start_mono_ns = parse_uint( + record["start_mono_ns"], "start_mono_ns", line_no, positive=True + ) + end_mono_ns = parse_uint( + record["end_mono_ns"], "end_mono_ns", line_no, positive=True + ) + if end_mono_ns <= start_mono_ns: + raise EvidenceError( + f"line {line_no}: stabilization timestamps are not increasing" + ) + stabilization_pid = parse_uint( + record["observer_pid"], "observer_pid", line_no, positive=True + ) + if stabilization_pid != observer_pid: + raise EvidenceError(f"line {line_no}: observer PID changed") + stabilization = WalWriterStabilization( + method=record["method"], + before_writes=before_writes, + after_writes=after_writes, + before_write_bytes=before_write_bytes, + after_write_bytes=after_write_bytes, + stats_reset=before_stats_reset, + target_lsn=record["target_lsn"], + observed_flush_lsn=record["observed_flush_lsn"], + wal_writer_delay_ms=parse_uint( + record["wal_writer_delay_ms"], + "wal_writer_delay_ms", + line_no, + positive=True, + ), + start_mono_ns=start_mono_ns, + end_mono_ns=end_mono_ns, + observer_pid=stabilization_pid, + ) + last_complete_sample = None + continue + + if line.startswith(HARNESS_RECONNECT_CHURN + "\t"): + if pending_context is not None: + raise EvidenceError( + f"line {line_no}: reconnect record split a runtime record" + ) + if active_phase != "reconnect-churn": + raise EvidenceError( + f"line {line_no}: reconnect record is outside its phase" + ) + if reconnect_churn is not None: + raise EvidenceError(f"line {line_no}: duplicate reconnect record") + record = parse_ordered_record( + line, HARNESS_RECONNECT_CHURN, RECONNECT_CHURN_FIELDS, line_no + ) + if record["nonce"] != nonce: + raise EvidenceError(f"line {line_no}: foreign reconnect nonce") + requested = parse_uint( + record["requested"], "requested", line_no, positive=True + ) + completed = parse_uint( + record["completed"], "completed", line_no, positive=True + ) + if completed != requested: + raise EvidenceError( + f"line {line_no}: reconnect completion count differs from requested" + ) + if record["command_sha256"] != RECONNECT_COMMAND_SHA256: + raise EvidenceError( + f"line {line_no}: reconnect command contract is not canonical" + ) + for field in ("client_sha256", "connection_sha256"): + if not SHA256_RE.fullmatch(record[field]): + raise EvidenceError( + f"line {line_no}: reconnect {field} is not a SHA-256" + ) + start_mono_ns = parse_uint( + record["start_mono_ns"], "start_mono_ns", line_no, positive=True + ) + end_mono_ns = parse_uint( + record["end_mono_ns"], "end_mono_ns", line_no, positive=True + ) + if end_mono_ns <= start_mono_ns: + raise EvidenceError( + f"line {line_no}: reconnect timestamps are not increasing" + ) + if record["status"] != "passed": + raise EvidenceError(f"line {line_no}: reconnect status is not passed") + reconnect_pid = parse_uint( + record["observer_pid"], "observer_pid", line_no, positive=True + ) + if reconnect_pid != observer_pid: + raise EvidenceError(f"line {line_no}: observer PID changed") + reconnect_churn = ReconnectChurn( + requested=requested, + completed=completed, + command_sha256=record["command_sha256"], + client_sha256=record["client_sha256"], + connection_sha256=record["connection_sha256"], + start_mono_ns=start_mono_ns, + end_mono_ns=end_mono_ns, + observer_pid=reconnect_pid, + ) + last_complete_sample = None + continue + + # The post-quiescence writer acknowledgement is the exact runtime + # cutoff. Runtime output may continue while the server remains alive, + # but it cannot extend or repair the acknowledged evidence window. + if final_cutoff_reached: + continue + + if line.startswith(RUNTIME_CONTEXT + "\t"): + last_complete_sample = None + if pending_context is not None: + raise EvidenceError(f"line {line_no}: runtime context missing its state record") + record = parse_ordered_record(line, RUNTIME_CONTEXT, CONTEXT_FIELDS, line_no) + sequence = parse_uint(record["seq"], "seq", line_no, positive=True) + expected_sequence = 1 if last_runtime_sequence is None else last_runtime_sequence + 1 + if sequence != expected_sequence: + raise EvidenceError( + f"line {line_no}: runtime sequence must start at 1 and remain consecutive" + ) + wait_kind = record["wait_kind"] + if wait_kind not in WAIT_KINDS: + raise EvidenceError(f"line {line_no}: unknown wait kind {wait_kind}") + pending_context = RuntimeContext( + sequence=sequence, + wait_kind=wait_kind, + observer_pid=parse_uint(record["observer_pid"], "observer_pid", line_no, positive=True), + observer_tid=parse_uint(record["observer_tid"], "observer_tid", line_no, positive=True), + ) + continue + + if line.startswith(RUNTIME_STATE + "\t"): + if pending_context is None: + raise EvidenceError(f"line {line_no}: runtime state has no context") + record = parse_ordered_record(line, RUNTIME_STATE, STATE_FIELDS, line_no) + sequence = parse_uint(record["seq"], "seq", line_no, positive=True) + mono_ns = parse_uint(record["mono_ns"], "mono_ns", line_no, positive=True) + if sequence != pending_context.sequence: + raise EvidenceError(f"line {line_no}: runtime context/state sequence mismatch") + expected_sequence = 1 if last_runtime_sequence is None else last_runtime_sequence + 1 + if sequence != expected_sequence: + raise EvidenceError( + f"line {line_no}: runtime sequence must start at 1 and remain consecutive" + ) + if last_runtime_mono_ns is not None and mono_ns <= last_runtime_mono_ns: + raise EvidenceError(f"line {line_no}: nonmonotonic runtime timestamp") + counts = tuple(parse_uint(record[field], field, line_no) for field in COUNT_FIELDS) + sample = RuntimeSample( + sequence=sequence, + mono_ns=mono_ns, + wait_kind=pending_context.wait_kind, + observer_pid=pending_context.observer_pid, + observer_tid=pending_context.observer_tid, + phase=active_phase, + counts=counts, + ) + samples.append(sample) + pending_context = None + last_runtime_sequence = sequence + last_runtime_mono_ns = mono_ns + last_complete_sample = sample + continue + + if line.startswith(RUNTIME_FENCE + "\t"): + if pending_context is not None: + raise EvidenceError(f"line {line_no}: writer fence split a runtime record") + if last_complete_sample is None: + raise EvidenceError( + f"line {line_no}: writer fence must immediately follow its context/state pair" + ) + record = parse_ordered_record(line, RUNTIME_FENCE, FENCE_FIELDS, line_no) + if record["nonce"] != nonce: + raise EvidenceError(f"line {line_no}: foreign writer-fence nonce") + phase = record["phase"] + request_sequence = parse_uint( + record["request_seq"], "request_seq", line_no, positive=True + ) + fence_index = len(fences) + if ( + fence_index >= len(FENCE_PHASE_ORDER) + or phase != FENCE_PHASE_ORDER[fence_index] + or request_sequence != fence_index + 1 + ): + raise EvidenceError(f"line {line_no}: unexpected writer-fence order") + if phase != active_phase: + raise EvidenceError(f"line {line_no}: writer fence does not close the active phase") + fence_sequence = parse_uint(record["seq"], "seq", line_no, positive=True) + fence_mono_ns = parse_uint(record["mono_ns"], "mono_ns", line_no, positive=True) + fence_pid = parse_uint(record["observer_pid"], "observer_pid", line_no, positive=True) + fence_tid = parse_uint(record["observer_tid"], "observer_tid", line_no, positive=True) + sample = last_complete_sample + if ( + fence_sequence != sample.sequence + or fence_mono_ns != sample.mono_ns + or fence_pid != sample.observer_pid + or fence_tid != sample.observer_tid + ): + raise EvidenceError( + f"line {line_no}: writer fence does not reference the immediately preceding sample" + ) + if fence_pid != observer_pid: + raise EvidenceError(f"line {line_no}: writer fence came from a foreign observer") + fences[phase] = RuntimeFence( + phase=phase, + request_sequence=request_sequence, + sample=sample, + ) + active_phase = None + last_complete_sample = None + if phase == "post-quiescence": + final_cutoff_reached = True + continue + + if line.startswith("wasix-runtime-"): + raise EvidenceError(f"line {line_no}: unknown or malformed lifecycle record") + last_complete_sample = None + + if pending_context is not None: + raise EvidenceError("truncated runtime context/state record") + if phase_index != len(PHASE_ORDER) - 1 or active_phase != "complete": + raise EvidenceError("incomplete lifecycle phase sequence") + if tuple(fences) != FENCE_PHASE_ORDER: + raise EvidenceError("incomplete writer-fence sequence") + if stabilization is None: + raise EvidenceError("missing WAL-writer stabilization record") + if reconnect_churn is None: + raise EvidenceError("missing reconnect churn record") + maintenance_phase = phases["maintenance-stabilization"] + readiness_phase = phases["readiness"] + if ( + stabilization.start_mono_ns < maintenance_phase.mono_ns + or stabilization.end_mono_ns > readiness_phase.mono_ns + ): + raise EvidenceError( + "WAL-writer stabilization timestamps are outside the maintenance phase" + ) + reconnect_phase = phases["reconnect-churn"] + post_quiescence_phase = phases["post-quiescence"] + if ( + reconnect_churn.start_mono_ns < reconnect_phase.mono_ns + or reconnect_churn.end_mono_ns > post_quiescence_phase.mono_ns + ): + raise EvidenceError("reconnect timestamps are outside the reconnect phase") + return LifecycleEvidence( + samples=samples, + fences=fences, + phases=phases, + stabilization=stabilization, + reconnect_churn=reconnect_churn, + ) + + +def stable_tail( + samples: list[RuntimeSample], min_samples: int, min_span_ns: int, max_gap_ns: int +) -> StablePlateau: + if not samples: + raise EvidenceError("phase has no samples") + final_counts = samples[-1].counts + start = len(samples) - 1 + while start > 0 and samples[start - 1].counts == final_counts: + start -= 1 + stable = samples[start:] + span_ns = stable[-1].mono_ns - stable[0].mono_ns + if len(stable) < min_samples: + raise EvidenceError(f"stable tail has {len(stable)} samples; need {min_samples}") + if span_ns < min_span_ns: + raise EvidenceError(f"stable tail spans {span_ns}ns; need {min_span_ns}ns") + gaps = [ + following.mono_ns - preceding.mono_ns + for preceding, following in zip(stable, stable[1:], strict=False) + ] + max_observed_gap_ns = max(gaps, default=0) + if max_observed_gap_ns > max_gap_ns: + raise EvidenceError( + f"stable tail has a {max_observed_gap_ns}ns sample gap; maximum is {max_gap_ns}ns" + ) + return StablePlateau( + samples=len(stable), + span_ns=span_ns, + max_gap_ns=max_observed_gap_ns, + first_mono_ns=stable[0].mono_ns, + last_mono_ns=stable[-1].mono_ns, + observer_pids=tuple(sorted({sample.observer_pid for sample in stable})), + counts=final_counts, + ) + + +def select_plateau( + evidence: LifecycleEvidence, + observer_pid: int, + min_samples: int, + min_span_ns: int, + max_gap_ns: int, + baseline_policy: BaselinePolicy, +) -> tuple[str, StablePlateau, StablePlateau, StablePlateau]: + readiness_fence = evidence.fences["readiness"] + final_fence = evidence.fences["post-quiescence"] + wait_kind = readiness_fence.sample.wait_kind + if final_fence.sample.wait_kind != wait_kind: + raise EvidenceError("readiness and post-quiescence fences used different wait kinds") + + phase_plateaus: dict[str, StablePlateau] = {} + for phase, fence in ( + ("cold-readiness", None), + ("readiness", readiness_fence), + ("post-quiescence", final_fence), + ): + phase_samples = [sample for sample in evidence.samples if sample.phase == phase] + # Every wait dump is a global control-plane/registry snapshot. Any + # registered idle PostgreSQL task may emit it; the state tuple itself + # proves that emitting task is registered. The postmaster alone owns + # the committed fence, while all same-kind observers strengthen the + # stable window instead of being misclassified as foreign processes. + candidate_samples = [ + sample for sample in phase_samples if sample.wait_kind == wait_kind + ] + if not candidate_samples: + raise EvidenceError(f"{phase} has no candidate samples") + if fence is not None and candidate_samples[-1].sequence != fence.sample.sequence: + raise EvidenceError(f"{phase} fence is not the terminal candidate sample") + plateau = stable_tail(candidate_samples, min_samples, min_span_ns, max_gap_ns) + for sample in phase_samples: + if sample.mono_ns < plateau.first_mono_ns: + continue + if sample.wait_kind != wait_kind: + raise EvidenceError( + f"{phase} stable tail contains a foreign wait kind" + ) + if sample.counts != plateau.counts: + raise EvidenceError( + f"{phase} has a contradictory {sample.wait_kind} sample " + f"from observer {sample.observer_pid} inside the fenced plateau" + ) + phase_plateaus[phase] = plateau + + cold = phase_plateaus["cold-readiness"] + readiness = phase_plateaus["readiness"] + final = phase_plateaus["post-quiescence"] + if readiness.counts != final.counts: + raise EvidenceError("readiness and post-quiescence tuples differ") + values = dict(zip(COUNT_FIELDS, readiness.counts, strict=True)) + if values["registered_processes"] == 0: + raise EvidenceError("idle-postmaster topology has no registered processes") + for field in ( + "active_tasks", + "process_topology_nodes", + "process_thread_entries", + "process_live_threads", + "runtime_state_active", + "runtime_state_slots", + ): + if values[field] != values["registered_processes"]: + raise EvidenceError( + "idle-postmaster semantic ownership requires " + f"{field}=registered_processes" + ) + if values["process_child_edges"] + 1 != values["process_topology_nodes"]: + raise EvidenceError("idle-postmaster process topology is not one rooted tree") + expected_execution_leases = values["active_tasks"] + values["process_child_edges"] + if values["process_execution_leases"] != expected_execution_leases: + raise EvidenceError( + "idle-postmaster semantic ownership requires " + "process_execution_leases=active_tasks+process_child_edges" + ) + if len(readiness.observer_pids) != values["registered_processes"]: + raise EvidenceError( + "readiness stable tail observer cardinality does not equal registered_processes" + ) + if len(final.observer_pids) != values["registered_processes"]: + raise EvidenceError( + "post-quiescence stable tail observer cardinality does not equal " + "registered_processes" + ) + if readiness.observer_pids != final.observer_pids: + raise EvidenceError( + "readiness and post-quiescence stable-tail observer PID sets differ" + ) + if observer_pid not in readiness.observer_pids: + raise EvidenceError("postmaster is absent from the terminal stable-tail observers") + if values["runtime_state_observer_registered"] != 1: + raise EvidenceError("runtime snapshot observer is not registered") + for field in ( + "process_pending_child_publications", + "process_quiescence_wakers", + "process_retiring_nodes", + ): + if values[field] != 0: + raise EvidenceError(f"idle-postmaster semantic ownership requires {field}=0") + runtime_active = readiness.counts[COUNT_FIELDS.index("runtime_state_active")] + runtime_stale = readiness.counts[COUNT_FIELDS.index("runtime_state_stale")] + runtime_slots = readiness.counts[COUNT_FIELDS.index("runtime_state_slots")] + if runtime_stale != 0: + raise EvidenceError("runtime-state registry stale count is nonzero") + if runtime_slots != runtime_active: + raise EvidenceError("runtime-state registry slots do not equal active entries") + for kind in ("private_futex", "shared_futex"): + waiters = readiness.counts[COUNT_FIELDS.index(f"{kind}_waiters")] + wakers = readiness.counts[COUNT_FIELDS.index(f"{kind}_wakers")] + if wakers > waiters: + raise EvidenceError(f"{kind} waker count exceeds waiter count") + epoll_subscriptions = readiness.counts[COUNT_FIELDS.index("epoll_subscriptions")] + epoll_close_registrations = readiness.counts[ + COUNT_FIELDS.index("epoll_close_registrations") + ] + if epoll_close_registrations > epoll_subscriptions: + raise EvidenceError("epoll close registrations exceed subscriptions") + active = readiness.counts[COUNT_FIELDS.index("shared_registry_active")] + stale = readiness.counts[COUNT_FIELDS.index("shared_registry_stale")] + slots = readiness.counts[COUNT_FIELDS.index("shared_registry_slots")] + if stale != 0: + raise EvidenceError("shared registry stale count is nonzero") + if slots != active: + raise EvidenceError("shared registry slots do not equal active entries") + enforce_baseline_policy(baseline_policy, readiness, final) + return wait_kind, cold, readiness, final + + +def output_row( + *, target: str, status: str, detail: str, nonce: str, observer_pid: int, + min_samples: int, min_span_ns: int, expected_interval_ms: int, + max_sample_gap_ns: int, wait_kind: str = "", + cold: StablePlateau | None = None, + readiness: StablePlateau | None = None, final: StablePlateau | None = None, + readiness_fence: RuntimeFence | None = None, + final_fence: RuntimeFence | None = None, + evidence: LifecycleEvidence | None = None, + freeze_binding: FreezeBinding | None = None, + baseline_binding: BaselineBinding | None = None, +) -> dict[str, str | int]: + row: dict[str, str | int] = {field: "" for field in OUTPUT_FIELDS} + row.update( + schema_version=6, + target=target, + status=status, + detail=detail.replace("\t", " ").replace("\r", " ").replace("\n", " "), + claim_scope=CLAIM_SCOPE, + baseline_assumption=BASELINE_ASSUMPTION, + nonce=nonce, + observer_pid=observer_pid, + wait_kind=wait_kind, + min_samples=min_samples, + min_span_ns=min_span_ns, + expected_interval_ms=expected_interval_ms, + max_sample_gap_ns=max_sample_gap_ns, + ) + if evidence is not None: + stabilization = evidence.stabilization + reconnect_churn = evidence.reconnect_churn + row.update( + cold_readiness_phase_mono_ns=evidence.phases["cold-readiness"].mono_ns, + maintenance_stabilization_phase_mono_ns=evidence.phases[ + "maintenance-stabilization" + ].mono_ns, + readiness_phase_mono_ns=evidence.phases["readiness"].mono_ns, + stabilization_method=stabilization.method, + stabilization_before_writes=stabilization.before_writes, + stabilization_after_writes=stabilization.after_writes, + stabilization_before_write_bytes=stabilization.before_write_bytes, + stabilization_after_write_bytes=stabilization.after_write_bytes, + stabilization_stats_reset=stabilization.stats_reset, + stabilization_target_lsn=stabilization.target_lsn, + stabilization_observed_flush_lsn=stabilization.observed_flush_lsn, + stabilization_wal_writer_delay_ms=stabilization.wal_writer_delay_ms, + stabilization_start_mono_ns=stabilization.start_mono_ns, + stabilization_end_mono_ns=stabilization.end_mono_ns, + stabilization_elapsed_ns=stabilization.elapsed_ns, + reconnect_requested=reconnect_churn.requested, + reconnect_completed=reconnect_churn.completed, + reconnect_command_sha256=reconnect_churn.command_sha256, + reconnect_client_sha256=reconnect_churn.client_sha256, + reconnect_connection_sha256=reconnect_churn.connection_sha256, + reconnect_start_mono_ns=reconnect_churn.start_mono_ns, + reconnect_end_mono_ns=reconnect_churn.end_mono_ns, + reconnect_elapsed_ns=reconnect_churn.elapsed_ns, + ) + if freeze_binding is not None: + row.update( + freeze_receipt_sha256=freeze_binding.receipt_sha256, + evidence_sha256=freeze_binding.evidence_sha256, + commit_ack_sha256=freeze_binding.commit_ack_sha256, + fence_end_offset=freeze_binding.fence_end_offset, + ) + if baseline_binding is not None: + row.update( + claim_scope=baseline_binding.policy.claim_scope, + baseline_assumption=baseline_binding.policy.baseline_assumption, + baseline_policy_id=baseline_binding.policy.policy_id, + baseline_policy_status=baseline_binding.policy.policy_status, + baseline_policy_sha256=baseline_binding.policy.sha256, + baseline_binding_sha256=baseline_binding.sha256, + ) + if ( + cold is not None + and readiness is not None + and final is not None + and readiness_fence is not None + and final_fence is not None + ): + row.update( + readiness_fence_sequence=readiness_fence.sample.sequence, + readiness_fence_mono_ns=readiness_fence.sample.mono_ns, + post_quiescence_fence_sequence=final_fence.sample.sequence, + post_quiescence_fence_mono_ns=final_fence.sample.mono_ns, + readiness_samples=readiness.samples, + readiness_span_ns=readiness.span_ns, + readiness_max_gap_ns=readiness.max_gap_ns, + post_quiescence_samples=final.samples, + post_quiescence_span_ns=final.span_ns, + post_quiescence_max_gap_ns=final.max_gap_ns, + cold_readiness_samples=cold.samples, + cold_readiness_span_ns=cold.span_ns, + cold_readiness_max_gap_ns=cold.max_gap_ns, + cold_readiness_observer_pids=",".join( + str(pid) for pid in cold.observer_pids + ), + readiness_observer_pids=",".join( + str(pid) for pid in readiness.observer_pids + ), + post_quiescence_observer_pids=",".join( + str(pid) for pid in final.observer_pids + ), + ) + for index, field in enumerate(COUNT_FIELDS): + row[f"cold_readiness_{field}"] = cold.counts[index] + row[f"readiness_{field}"] = readiness.counts[index] + row[f"post_quiescence_{field}"] = final.counts[index] + return row + + +def validate_lifecycle_bundle( + *, + log: Path, + freeze_receipt: Path, + baseline_policy_path: Path, + baseline_binding_path: Path, + target: str, + nonce: str, + observer_pid: int, + min_samples: int, + min_span_ns: int, + expected_interval_ms: int, + max_sample_gap_ns: int, +) -> LifecycleValidationOutcome: + freeze_binding: FreezeBinding | None = None + baseline_binding: BaselineBinding | None = None + evidence: LifecycleEvidence | None = None + try: + baseline_policy = parse_baseline_policy(baseline_policy_path) + baseline_binding = parse_baseline_binding( + baseline_binding_path, baseline_policy + ) + frozen = read_regular_file(log) + freeze_binding = validate_freeze_binding( + freeze_receipt, + log, + frozen, + nonce, + observer_pid, + ) + lines = frozen.decode("utf-8", errors="strict").splitlines(keepends=True) + evidence = parse_evidence(lines, nonce, observer_pid) + final_fence = evidence.fences["post-quiescence"].sample + if ( + final_fence.sequence != freeze_binding.fence_sequence + or final_fence.mono_ns != freeze_binding.fence_mono_ns + ): + raise EvidenceError("parsed final fence differs from the freeze receipt") + wait_kind, cold, readiness, final = select_plateau( + evidence, + observer_pid, + min_samples, + min_span_ns, + max_sample_gap_ns, + baseline_policy, + ) + row = output_row( + target=target, + status="passed", + detail=( + "idle-postmaster-semantic-ownership-and-stabilized-readiness-" + "post-quiescence-tuple-and-observer-identity-equality-with-" + "receipt-bound-reconnect-churn" + ), + nonce=nonce, + observer_pid=observer_pid, + min_samples=min_samples, + min_span_ns=min_span_ns, + expected_interval_ms=expected_interval_ms, + max_sample_gap_ns=max_sample_gap_ns, + wait_kind=wait_kind, + cold=cold, + readiness=readiness, + final=final, + readiness_fence=evidence.fences["readiness"], + final_fence=evidence.fences["post-quiescence"], + evidence=evidence, + freeze_binding=freeze_binding, + baseline_binding=baseline_binding, + ) + return LifecycleValidationOutcome(row=row, error=None) + except (EvidenceError, OSError, UnicodeError) as error: + row = output_row( + target=target, + status="failed", + detail=str(error), + nonce=nonce, + observer_pid=observer_pid, + min_samples=min_samples, + min_span_ns=min_span_ns, + expected_interval_ms=expected_interval_ms, + max_sample_gap_ns=max_sample_gap_ns, + evidence=evidence, + freeze_binding=freeze_binding, + baseline_binding=baseline_binding, + ) + return LifecycleValidationOutcome(row=row, error=str(error)) + + +def write_output(path: Path, row: dict[str, str | int]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + pending = path.with_name(path.name + ".pending") + with pending.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter(handle, delimiter="\t", fieldnames=OUTPUT_FIELDS, lineterminator="\n") + writer.writeheader() + writer.writerow(row) + pending.replace(path) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Validate WASIX lifecycle occupancy plateau evidence") + parser.add_argument("--log", required=True, type=Path) + parser.add_argument("--freeze-receipt", required=True, type=Path) + parser.add_argument("--baseline-policy", required=True, type=Path) + parser.add_argument("--baseline-binding", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--target", default="wasix") + parser.add_argument("--nonce", required=True) + parser.add_argument("--observer-pid", required=True, type=int) + parser.add_argument("--min-samples", default=3, type=int) + parser.add_argument("--min-span-ms", default=1000, type=int) + parser.add_argument("--expected-interval-ms", required=True, type=int) + args = parser.parse_args() + + if not NONCE_RE.fullmatch(args.nonce): + parser.error("--nonce must be exactly 32 lowercase hexadecimal characters") + if args.observer_pid <= 0: + parser.error("--observer-pid must be positive") + if args.min_samples < 3: + parser.error("--min-samples must be at least 3") + if args.min_span_ms < 1000: + parser.error("--min-span-ms must be at least 1000") + if args.expected_interval_ms <= 0: + parser.error("--expected-interval-ms must be positive") + if args.min_span_ms > ((1 << 64) - 1) // 1_000_000: + parser.error("--min-span-ms exceeds the supported u64 nanosecond range") + if args.expected_interval_ms > ((1 << 64) - 1) // 3_000_000: + parser.error("--expected-interval-ms exceeds the supported u64 nanosecond range") + min_span_ns = args.min_span_ms * 1_000_000 + max_sample_gap_ns = args.expected_interval_ms * 3_000_000 + + outcome = validate_lifecycle_bundle( + log=args.log, + freeze_receipt=args.freeze_receipt, + baseline_policy_path=args.baseline_policy, + baseline_binding_path=args.baseline_binding, + target=args.target, + nonce=args.nonce, + observer_pid=args.observer_pid, + min_samples=args.min_samples, + min_span_ns=min_span_ns, + expected_interval_ms=args.expected_interval_ms, + max_sample_gap_ns=max_sample_gap_ns, + ) + write_output(args.output, outcome.row) + if not outcome.passed: + print( + f"WASIX lifecycle plateau validation failed: {outcome.row['detail']}", + file=sys.stderr, + ) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-plateau.test.py b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-plateau.test.py new file mode 100755 index 00000000..fa2b8bdc --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/validate-wasix-lifecycle-plateau.test.py @@ -0,0 +1,897 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import hashlib +import importlib.util +import io +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + + +SCRIPT = Path(__file__).with_name("validate-wasix-lifecycle-plateau.py") +SPEC = importlib.util.spec_from_file_location( + "wasix_lifecycle_stable_read_test", SCRIPT +) +assert SPEC is not None and SPEC.loader is not None +VALIDATOR_MODULE = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = VALIDATOR_MODULE +SPEC.loader.exec_module(VALIDATOR_MODULE) +BASELINE_POLICY = ( + SCRIPT.parents[1] + / "profiles/lifecycle-baselines/relative-stabilized-idle-postmaster-candidate-v1.tsv" +) +NONCE = "0123456789abcdef0123456789abcdef" +OBSERVER_PID = 42 +WAIT_KIND = "wait-registry.epoll_wait.pending" +OTHER_WAIT_KIND = "wait-registry.futex_wait.pending" +COUNT_NAMES = ( + "registered_processes", + "active_tasks", + "process_topology_nodes", + "process_child_edges", + "process_thread_entries", + "process_live_threads", + "process_pending_child_publications", + "process_execution_leases", + "process_quiescence_wakers", + "process_retiring_nodes", + "runtime_state_active", + "runtime_state_stale", + "runtime_state_slots", + "runtime_state_observer_registered", + "private_futexes", + "private_futex_waiters", + "private_futex_wakers", + "shared_futexes", + "shared_futex_waiters", + "shared_futex_wakers", + "epoll_states", + "epoll_subscriptions", + "epoll_ready_items", + "epoll_pending_subscriptions", + "epoll_enqueued_subscriptions", + "epoll_join_guards", + "epoll_close_registrations", + "shared_registry_active", + "shared_registry_stale", + "shared_registry_slots", + "shared_mappings", + "guest_fd_entries", +) +BASE_COUNTS = { + "registered_processes": 6, + "active_tasks": 6, + "process_topology_nodes": 6, + "process_child_edges": 5, + "process_thread_entries": 6, + "process_live_threads": 6, + "process_pending_child_publications": 0, + "process_execution_leases": 11, + "process_quiescence_wakers": 0, + "process_retiring_nodes": 0, + "runtime_state_active": 6, + "runtime_state_stale": 0, + "runtime_state_slots": 6, + "runtime_state_observer_registered": 1, + "private_futexes": 2, + "private_futex_waiters": 3, + "private_futex_wakers": 2, + "shared_futexes": 4, + "shared_futex_waiters": 5, + "shared_futex_wakers": 4, + "epoll_states": 1, + "epoll_subscriptions": 6, + "epoll_ready_items": 0, + "epoll_pending_subscriptions": 2, + "epoll_enqueued_subscriptions": 1, + "epoll_join_guards": 3, + "epoll_close_registrations": 1, + "shared_registry_active": 2, + "shared_registry_stale": 0, + "shared_registry_slots": 2, + "shared_mappings": 3, + "guest_fd_entries": 9, +} +COUNTS = tuple(BASE_COUNTS[name] for name in COUNT_NAMES) +EXPECTED_INTERVAL_MS = 300 + + +def changed_counts(**overrides: int) -> tuple[int, ...]: + values = BASE_COUNTS | overrides + return tuple(values[name] for name in COUNT_NAMES) + + +COLD_COUNTS = changed_counts(guest_fd_entries=8) +OBSERVER_SET = (42, 43, 44, 45, 46, 47, 42) + + +def context( + sequence: int, + *, + pid: int = OBSERVER_PID, + tid: int = 1, + wait_kind: str = WAIT_KIND, +) -> str: + return ( + "wasix-runtime-context-v1" + f"\tseq={sequence}\twait_kind={wait_kind}\tobserver_pid={pid}\tobserver_tid={tid}\n" + ) + + +def state(sequence: int, mono_ns: int, counts: tuple[int, ...] = COUNTS) -> str: + fields = "".join( + f"\t{name}={value}" for name, value in zip(COUNT_NAMES, counts, strict=True) + ) + return f"wasix-runtime-state-v1\tseq={sequence}\tmono_ns={mono_ns}{fields}\n" + + +def fence( + sequence: int, + mono_ns: int, + name: str, + request_sequence: int, + *, + pid: int = OBSERVER_PID, + tid: int = 1, +) -> str: + return ( + "wasix-runtime-fence-v1" + f"\tnonce={NONCE}\tseq={sequence}\tmono_ns={mono_ns}\tphase={name}" + f"\tobserver_pid={pid}\tobserver_tid={tid}\trequest_seq={request_sequence}\n" + ) + + +def phase(sequence: int, mono_ns: int, name: str) -> str: + return ( + "wasix-runtime-phase-v1" + f"\tnonce={NONCE}\tseq={sequence}\tmono_ns={mono_ns}" + f"\tphase={name}\tobserver_pid={OBSERVER_PID}\n" + ) + + +def stabilization(**overrides: str | int) -> str: + values: dict[str, str | int] = { + "nonce": NONCE, + "method": "pg_log_standby_snapshot", + "before_writes": 10, + "after_writes": 12, + "before_write_bytes": 81_920, + "after_write_bytes": 147_456, + "before_stats_reset": 1_786_320_000_000_000, + "after_stats_reset": 1_786_320_000_000_000, + "target_lsn": "0/1000000", + "observed_flush_lsn": "0/1008000", + "wal_writer_delay_ms": 200, + "start_mono_ns": 25, + "end_mono_ns": 35, + "status": "passed", + "observer_pid": OBSERVER_PID, + } + values.update(overrides) + return "wasix-runtime-stabilization-v1" + "".join( + f"\t{field}={values[field]}" for field in ( + "nonce", + "method", + "before_writes", + "after_writes", + "before_write_bytes", + "after_write_bytes", + "before_stats_reset", + "after_stats_reset", + "target_lsn", + "observed_flush_lsn", + "wal_writer_delay_ms", + "start_mono_ns", + "end_mono_ns", + "status", + "observer_pid", + ) + ) + "\n" + + +def reconnect_churn(**overrides: str | int) -> str: + values: dict[str, str | int] = { + "nonce": NONCE, + "requested": 2000, + "completed": 2000, + "command_sha256": VALIDATOR_MODULE.RECONNECT_COMMAND_SHA256, + "client_sha256": "1" * 64, + "connection_sha256": "2" * 64, + "start_mono_ns": 61, + "end_mono_ns": 69, + "status": "passed", + "observer_pid": OBSERVER_PID, + } + values.update(overrides) + return "wasix-runtime-reconnect-churn-v1" + "".join( + f"\t{field}={values[field]}" for field in ( + "nonce", + "requested", + "completed", + "command_sha256", + "client_sha256", + "connection_sha256", + "start_mono_ns", + "end_mono_ns", + "status", + "observer_pid", + ) + ) + "\n" + + +def valid_log( + *, + cold_counts: tuple[int, ...] = COLD_COUNTS, + readiness_counts: tuple[int, ...] = COUNTS, + final_counts: tuple[int, ...] = COUNTS, + readiness_kind: str = WAIT_KIND, + final_kind: str = WAIT_KIND, + readiness_observers: tuple[int, ...] = OBSERVER_SET, + final_observers: tuple[int, ...] = OBSERVER_SET, + stabilization_record: str | None = None, + reconnect_record: str | None = None, + final_times: tuple[int, ...] = ( + 3_000_000_000, + 3_200_000_000, + 3_400_000_000, + 3_600_000_000, + 3_800_000_000, + 4_000_000_000, + 4_200_000_000, + ), +) -> str: + if len(readiness_observers) != 7 or len(final_observers) != len(final_times): + raise ValueError("test fixture observer/time cardinality mismatch") + lines = [phase(1, 10, "cold-readiness")] + runtime_sequence = 1 + for mono_ns in (100_000_000, 500_000_000, 900_000_000, 1_300_000_000): + lines.extend( + ( + context(runtime_sequence), + state(runtime_sequence, mono_ns, cold_counts), + ) + ) + runtime_sequence += 1 + lines.extend( + ( + phase(2, 20, "maintenance-stabilization"), + stabilization_record if stabilization_record is not None else stabilization(), + phase(3, 50, "readiness"), + # A readiness probe may still be retiring before the stable + # coverage window begins. It cannot contribute to or contradict + # the terminal stable tail. + context(runtime_sequence, pid=99), + state(runtime_sequence, 1_400_000_000, tuple(80 for _ in COUNT_NAMES)), + ) + ) + runtime_sequence += 1 + readiness_times = ( + 1_500_000_000, + 1_700_000_000, + 1_900_000_000, + 2_100_000_000, + 2_300_000_000, + 2_500_000_000, + 2_700_000_000, + ) + for index, (mono_ns, pid) in enumerate(zip(readiness_times, readiness_observers, strict=True)): + lines.extend( + ( + context(runtime_sequence, pid=pid, wait_kind=readiness_kind), + state(runtime_sequence, mono_ns, readiness_counts), + ) + ) + if index == len(readiness_times) - 1: + lines.append( + fence(runtime_sequence, mono_ns, "readiness", 1) + ) + runtime_sequence += 1 + lines.extend( + ( + phase(4, 60, "reconnect-churn"), + context(runtime_sequence, pid=99), + state( + runtime_sequence, + 2_800_000_000, + tuple(999 for _ in COUNT_NAMES), + ), + reconnect_record if reconnect_record is not None else reconnect_churn(), + phase(5, 70, "post-quiescence"), + ) + ) + runtime_sequence += 1 + for index, (mono_ns, pid) in enumerate(zip(final_times, final_observers, strict=True)): + lines.extend( + ( + context(runtime_sequence, pid=pid, wait_kind=final_kind), + state(runtime_sequence, mono_ns, final_counts), + ) + ) + if index == len(final_times) - 1: + lines.append( + fence(runtime_sequence, mono_ns, "post-quiescence", 2) + ) + runtime_sequence += 1 + lines.append(phase(6, 80, "complete")) + return "".join(lines) + + +def strict_policy_payload() -> bytes: + source = csv.DictReader( + BASELINE_POLICY.read_text(encoding="utf-8").splitlines(), delimiter="\t" + ) + stream = io.StringIO(newline="") + writer = csv.DictWriter( + stream, fieldnames=source.fieldnames, delimiter="\t", lineterminator="\n" + ) + writer.writeheader() + for row in source: + row["policy_id"] = "pg18-idle-postmaster-stabilized-qualified-v1" + row["policy_status"] = "qualification-bounded" + row["rule"] = "exact" + row["minimum"] = str(BASE_COUNTS[row["field"]]) + row["maximum"] = str(BASE_COUNTS[row["field"]]) + writer.writerow(row) + return stream.getvalue().encode("utf-8") + + +class LifecyclePlateauTests(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory() + self.root = Path(self.temp.name) + + def tearDown(self) -> None: + self.temp.cleanup() + + def run_validator( + self, + contents: str | bytes, + *, + tamper_receipt: bool = False, + tamper_ack: bool = False, + tamper_policy_after_binding: bool = False, + policy_payload_override: bytes | None = None, + ) -> tuple[subprocess.CompletedProcess[str], dict[str, str]]: + log = self.root / "runtime.log" + raw_log = self.root / "runtime.raw.log" + ack = self.root / "runtime-fence.ack" + receipt = self.root / "runtime.freeze.tsv" + baseline_binding = self.root / "baseline-binding.tsv" + baseline_policy = self.root / "baseline-policy.tsv" + output = self.root / "plateau.tsv" + if isinstance(contents, bytes): + log.write_bytes(contents) + else: + log.write_text(contents, encoding="utf-8") + frozen = log.read_bytes() + fence_pattern = re.compile( + rb"wasix-runtime-fence-v1\tnonce=" + NONCE.encode("ascii") + + rb"\tseq=([1-9][0-9]*)\tmono_ns=([1-9][0-9]*)" + + rb"\tphase=post-quiescence\tobserver_pid=" + + str(OBSERVER_PID).encode("ascii") + + rb"\tobserver_tid=([1-9][0-9]*)\trequest_seq=2\n" + ) + matches = list(fence_pattern.finditer(frozen)) + self.assertTrue(matches, "test fixture must retain a parseable final fence") + final_match = matches[-1] + fence_sequence, fence_mono_ns, observer_tid = ( + value.decode("ascii") for value in final_match.groups() + ) + fence_end_offset = final_match.end() + ack_payload = ( + "wasix-runtime-fence-commit-v1" + f"\tnonce={NONCE}\tseq={fence_sequence}\tmono_ns={fence_mono_ns}" + f"\tphase=post-quiescence\tobserver_pid={OBSERVER_PID}" + f"\tobserver_tid={observer_tid}\trequest_seq=2" + f"\tfence_end_offset={fence_end_offset}\n" + ).encode("ascii") + ack.write_bytes(ack_payload) + raw_log.write_bytes(frozen[:fence_end_offset]) + receipt_payload = ( + "schema_version\traw_log\traw_observed_size\tcommit_ack" + "\tcommit_ack_sha256\tfence_end_offset\tfrozen_log\tfrozen_size" + "\tsha256\tnonce\tobserver_pid\tfence_sequence\tfence_mono_ns" + "\tcomplete_phase_sequence\tcomplete_phase_mono_ns\n" + "oliphaunt.wasix-postmaster.lifecycle-freeze.v2" + f"\t{raw_log}\t{fence_end_offset}\t{ack}" + f"\t{hashlib.sha256(ack_payload).hexdigest()}\t{fence_end_offset}" + f"\t{log}\t{len(frozen)}\t{hashlib.sha256(frozen).hexdigest()}" + f"\t{NONCE}\t{OBSERVER_PID}\t{fence_sequence}\t{fence_mono_ns}" + "\t6\t80\n" + ) + receipt.write_text(receipt_payload, encoding="utf-8") + policy_payload = policy_payload_override or BASELINE_POLICY.read_bytes() + baseline_policy.write_bytes(policy_payload) + policy_sha256 = hashlib.sha256(policy_payload).hexdigest() + policy_reader = csv.DictReader( + policy_payload.decode("utf-8").splitlines(), delimiter="\t" + ) + policy_metadata = next(policy_reader) + baseline_binding.write_text( + "schema_version\tpolicy_id\tpolicy_sha256\tpolicy_status\tclaim_scope" + "\tbaseline_assumption\tpostgres_major\truntime_footprint" + "\truntime_footprint_sha256\tdurability_profile" + "\tdurability_profile_sha256\tpostgres_profile_resolution_identity" + "\truntime_mode\twasmer_bin_sha256\tpostgres_module_sha256" + "\tcarrier_manifest_sha256\tcarrier_receipt_sha256" + "\tcarrier_payload_inventory_sha256\n" + "oliphaunt.wasix-postmaster.lifecycle-baseline-binding.v1" + f"\t{policy_metadata['policy_id']}\t{policy_sha256}" + f"\t{policy_metadata['policy_status']}\t{policy_metadata['claim_scope']}" + f"\t{policy_metadata['baseline_assumption']}" + "\t18\tnone\tnone\tnone\tnone\tnone\tcompiler" + f"\t{'1' * 64}\t{'2' * 64}\tnone\tnone\tnone\n", + encoding="utf-8", + ) + if tamper_receipt: + receipt.write_text( + receipt_payload.replace(hashlib.sha256(frozen).hexdigest(), "0" * 64), + encoding="utf-8", + ) + if tamper_ack: + ack.write_bytes(ack_payload.replace(b"request_seq=2", b"request_seq=3")) + if tamper_policy_after_binding: + tampered = policy_payload.replace( + b"\tpg18-idle-postmaster-stabilized-exploratory-v1\t", + b"\tpg18-idle-postmaster-stabilized-exploratory-v2\t", + ) + self.assertNotEqual(tampered, policy_payload) + baseline_policy.write_bytes(tampered) + result = subprocess.run( + [ + str(SCRIPT), + "--log", str(log), + "--freeze-receipt", str(receipt), + "--baseline-policy", str(baseline_policy), + "--baseline-binding", str(baseline_binding), + "--output", str(output), + "--nonce", NONCE, + "--observer-pid", str(OBSERVER_PID), + "--min-samples", "3", + "--min-span-ms", "1000", + "--expected-interval-ms", str(EXPECTED_INTERVAL_MS), + ], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + with output.open(encoding="utf-8", newline="") as handle: + row = next(csv.DictReader(handle, delimiter="\t")) + return result, row + + def assert_fails(self, contents: str, detail: str = "") -> dict[str, str]: + result, row = self.run_validator(contents) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertEqual(row["status"], "failed") + if detail: + self.assertIn(detail, row["detail"]) + return row + + def test_regular_file_reader_rejects_same_size_mutation(self) -> None: + path = self.root / "stable-input.tsv" + original = b"a" * 4096 + replacement = b"b" * len(original) + path.write_bytes(original) + real_read = os.read + mutated = False + + def mutate_after_first_read(fd: int, size: int) -> bytes: + nonlocal mutated + chunk = real_read(fd, size) + if chunk and not mutated: + mutated = True + path.write_bytes(replacement) + return chunk + + with mock.patch.object( + VALIDATOR_MODULE.os, "read", side_effect=mutate_after_first_read + ): + with self.assertRaisesRegex( + VALIDATOR_MODULE.EvidenceError, + r"changed while (reading|confirming read)", + ): + VALIDATOR_MODULE.read_regular_file(path) + + def test_exact_fenced_plateau_passes(self) -> None: + result, row = self.run_validator(valid_log()) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(row["schema_version"], "6") + self.assertEqual(row["status"], "passed") + self.assertEqual(row["claim_scope"], "relative-to-stabilized-baseline") + self.assertEqual(row["baseline_policy_status"], "exploratory-unbounded") + self.assertEqual(row["baseline_policy_sha256"], hashlib.sha256(BASELINE_POLICY.read_bytes()).hexdigest()) + self.assertIn("readiness-is-stabilized-idle-postmaster-state", row["baseline_assumption"]) + self.assertEqual(row["evidence_sha256"], hashlib.sha256(valid_log().encode()).hexdigest()) + self.assertEqual(row["wait_kind"], WAIT_KIND) + self.assertEqual(row["cold_readiness_samples"], "4") + self.assertEqual(row["readiness_samples"], "7") + self.assertEqual(row["post_quiescence_samples"], "7") + self.assertEqual(row["readiness_fence_sequence"], "12") + self.assertEqual(row["post_quiescence_fence_sequence"], "20") + self.assertEqual(row["stabilization_method"], "pg_log_standby_snapshot") + self.assertEqual(row["stabilization_target_lsn"], "0/1000000") + self.assertEqual(row["stabilization_elapsed_ns"], "10") + self.assertEqual(row["reconnect_requested"], "2000") + self.assertEqual(row["reconnect_completed"], "2000") + self.assertEqual( + row["reconnect_command_sha256"], + VALIDATOR_MODULE.RECONNECT_COMMAND_SHA256, + ) + self.assertEqual(row["reconnect_elapsed_ns"], "8") + self.assertEqual(row["cold_readiness_guest_fd_entries"], "8") + self.assertEqual(row["readiness_observer_pids"], "42,43,44,45,46,47") + self.assertEqual(row["post_quiescence_observer_pids"], "42,43,44,45,46,47") + self.assertEqual(row["readiness_registered_processes"], "6") + self.assertEqual(row["readiness_private_futex_waiters"], "3") + self.assertEqual(row["post_quiescence_epoll_subscriptions"], "6") + self.assertEqual(row["post_quiescence_guest_fd_entries"], "9") + + def test_count_drift_fails_closed(self) -> None: + self.assert_fails( + valid_log(final_counts=changed_counts(registered_processes=2)), "tuples differ" + ) + + def test_wal_writer_event_stabilization_fails_closed(self) -> None: + cases = ( + (stabilization(after_writes=10), "write count did not increase"), + (stabilization(after_write_bytes=81_920), "byte count did not increase"), + ( + stabilization(after_stats_reset=1_786_320_001_000_000), + "stats_reset changed", + ), + (stabilization(target_lsn="0/1009000"), "behind the target LSN"), + (stabilization(target_lsn="0/0"), "target LSN is zero"), + (stabilization(end_mono_ns=25), "timestamps are not increasing"), + (stabilization(start_mono_ns=5), "outside the maintenance phase"), + (stabilization(status="not-applicable"), "status is not passed"), + ) + for record, detail in cases: + with self.subTest(detail=detail): + self.assert_fails( + valid_log(stabilization_record=record), + detail, + ) + + def test_missing_or_duplicate_stabilization_record_fails_closed(self) -> None: + self.assert_fails( + valid_log(stabilization_record=""), + "missing WAL-writer stabilization record", + ) + self.assert_fails( + valid_log(stabilization_record=stabilization() + stabilization()), + "duplicate stabilization record", + ) + + def test_reconnect_churn_is_receipt_bound_and_fails_closed(self) -> None: + cases = ( + ("", "missing reconnect churn record"), + ( + reconnect_churn(completed=1999), + "completion count differs from requested", + ), + ( + reconnect_churn(command_sha256="0" * 64), + "command contract is not canonical", + ), + (reconnect_churn(status="failed"), "status is not passed"), + ( + reconnect_churn(start_mono_ns=59), + "outside the reconnect phase", + ), + ( + reconnect_churn() + reconnect_churn(), + "duplicate reconnect record", + ), + ) + for record, detail in cases: + with self.subTest(detail=detail): + self.assert_fails(valid_log(reconnect_record=record), detail) + + def test_stale_or_unbalanced_registry_fails_closed(self) -> None: + self.assert_fails( + valid_log( + readiness_counts=changed_counts(runtime_state_stale=1), + final_counts=changed_counts(runtime_state_stale=1), + ), + "runtime-state registry stale count", + ) + self.assert_fails( + valid_log( + readiness_counts=changed_counts(shared_registry_stale=1, shared_registry_slots=3), + final_counts=changed_counts(shared_registry_stale=1, shared_registry_slots=3), + ), + "stale count", + ) + + def test_observer_and_inner_occupancy_invariants_fail_closed(self) -> None: + self.assert_fails( + valid_log( + readiness_counts=changed_counts(runtime_state_observer_registered=0), + final_counts=changed_counts(runtime_state_observer_registered=0), + ), + "runtime snapshot observer is not registered", + ) + self.assert_fails( + valid_log( + readiness_counts=changed_counts(private_futex_wakers=4), + final_counts=changed_counts(private_futex_wakers=4), + ), + "private_futex waker count exceeds waiter count", + ) + self.assert_fails( + valid_log( + readiness_counts=changed_counts(shared_futex_wakers=6), + final_counts=changed_counts(shared_futex_wakers=6), + ), + "shared_futex waker count exceeds waiter count", + ) + self.assert_fails( + valid_log( + readiness_counts=changed_counts(epoll_close_registrations=7), + final_counts=changed_counts(epoll_close_registrations=7), + ), + "epoll close registrations exceed subscriptions", + ) + + def test_each_inner_waiter_and_epoll_count_drift_fails_closed(self) -> None: + inner_fields = ( + "private_futexes", + "private_futex_waiters", + "private_futex_wakers", + "shared_futexes", + "shared_futex_waiters", + "shared_futex_wakers", + "epoll_states", + "epoll_subscriptions", + "epoll_ready_items", + "epoll_pending_subscriptions", + "epoll_enqueued_subscriptions", + "epoll_join_guards", + "epoll_close_registrations", + ) + for field in inner_fields: + with self.subTest(field=field): + self.assert_fails( + valid_log( + final_counts=changed_counts(**{field: BASE_COUNTS[field] + 1}) + ), + "tuples differ", + ) + + def test_idle_postmaster_relational_ownership_fails_closed(self) -> None: + for field in ( + "active_tasks", + "process_topology_nodes", + "process_thread_entries", + "process_live_threads", + "runtime_state_active", + "runtime_state_slots", + ): + with self.subTest(field=field): + inconsistent = changed_counts(**{field: BASE_COUNTS[field] + 1}) + self.assert_fails( + valid_log( + readiness_counts=inconsistent, + final_counts=inconsistent, + ), + f"requires {field}=registered_processes", + ) + invalid_leases = changed_counts(process_execution_leases=10) + self.assert_fails( + valid_log( + readiness_counts=invalid_leases, + final_counts=invalid_leases, + ), + "process_execution_leases=active_tasks+process_child_edges", + ) + no_process = changed_counts(registered_processes=0) + self.assert_fails( + valid_log(readiness_counts=no_process, final_counts=no_process), + "has no registered processes", + ) + malformed_tree = changed_counts(process_child_edges=4) + self.assert_fails( + valid_log(readiness_counts=malformed_tree, final_counts=malformed_tree), + "not one rooted tree", + ) + for field in ( + "process_pending_child_publications", + "process_quiescence_wakers", + "process_retiring_nodes", + ): + with self.subTest(field=field): + elevated = changed_counts(**{field: 1}) + self.assert_fails( + valid_log(readiness_counts=elevated, final_counts=elevated), + f"requires {field}=0", + ) + + def test_each_process_topology_count_drift_fails_closed(self) -> None: + for field in ( + "process_topology_nodes", + "process_child_edges", + "process_thread_entries", + "process_live_threads", + "process_pending_child_publications", + "process_execution_leases", + "process_quiescence_wakers", + "process_retiring_nodes", + ): + with self.subTest(field=field): + self.assert_fails( + valid_log( + final_counts=changed_counts(**{field: BASE_COUNTS[field] + 1}) + ), + "tuples differ", + ) + + def test_freeze_receipt_and_committed_ack_are_verified(self) -> None: + result, row = self.run_validator(valid_log(), tamper_receipt=True) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("SHA-256 does not match the frozen", row["detail"]) + result, row = self.run_validator(valid_log(), tamper_ack=True) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("SHA-256 does not match the committed ACK", row["detail"]) + + def test_pre_run_hashed_baseline_policy_binding_fails_on_mutation(self) -> None: + result, row = self.run_validator( + valid_log(), tamper_policy_after_binding=True + ) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("policy_id does not match its policy", row["detail"]) + + def test_exact_qualification_policy_passes(self) -> None: + result, row = self.run_validator( + valid_log(), policy_payload_override=strict_policy_payload() + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(row["baseline_policy_status"], "qualification-bounded") + + def test_relabelled_broad_policy_cannot_be_promoted(self) -> None: + exploratory = BASELINE_POLICY.read_bytes() + relabelled = exploratory.replace( + b"\tpg18-idle-postmaster-stabilized-exploratory-v1\texploratory-unbounded\t", + b"\tpg18-idle-postmaster-stabilized-qualified-v1\tqualification-bounded\t", + ) + self.assertNotEqual(relabelled, exploratory) + result, row = self.run_validator( + valid_log(), policy_payload_override=relabelled + ) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("must freeze the exact exploratory observation", row["detail"]) + + def test_near_u64_policy_cannot_masquerade_as_bounded(self) -> None: + broad = strict_policy_payload().replace( + b"\tprivate_futexes\texact\t2\t2\n", + b"\tprivate_futexes\trelative-equal\t0\t18446744073709551614\n", + ) + self.assertNotEqual(broad, strict_policy_payload()) + result, row = self.run_validator(valid_log(), policy_payload_override=broad) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("must freeze the exact exploratory observation", row["detail"]) + + def test_different_fence_wait_kinds_fail_closed(self) -> None: + self.assert_fails(valid_log(final_kind=OTHER_WAIT_KIND), "different wait kinds") + + def test_short_or_gapped_coverage_fails_closed(self) -> None: + self.assert_fails( + valid_log( + final_times=( + 3_000_000_000, + 3_100_000_000, + 3_200_000_000, + 3_300_000_000, + 3_400_000_000, + 3_500_000_000, + 3_600_000_000, + ) + ), + "need 1000000000ns", + ) + self.assert_fails( + valid_log( + final_times=( + 3_000_000_000, + 3_200_000_000, + 3_400_000_000, + 3_600_000_000, + 3_800_000_000, + 4_000_000_000, + 5_000_000_000, + ) + ), + "sample gap", + ) + + def test_runtime_sequence_must_start_at_one_and_remain_gapless_to_fence(self) -> None: + self.assert_fails( + valid_log().replace( + "wasix-runtime-context-v1\tseq=1\t", + "wasix-runtime-context-v1\tseq=12\t", + 1, + ), + "start at 1", + ) + self.assert_fails( + valid_log().replace("wasix-runtime-context-v1\tseq=4", "wasix-runtime-context-v1\tseq=40", 1), + "consecutive", + ) + + def test_fence_must_be_immediate_and_reference_the_terminal_sample(self) -> None: + log = valid_log().replace( + "wasix-runtime-fence-v1\tnonce=", + "unrelated-line\nwasix-runtime-fence-v1\tnonce=", + 1, + ) + self.assert_fails(log, "immediately follow") + self.assert_fails( + valid_log().replace( + "\tseq=12\tmono_ns=2700000000\tphase=readiness", + "\tseq=11\tmono_ns=2700000000\tphase=readiness", + 1, + ), + "does not reference", + ) + + def test_missing_writer_fence_cannot_be_repaired_by_phase_markers(self) -> None: + log = valid_log().replace( + fence(12, 2_700_000_000, "readiness", 1), "", 1 + ) + self.assert_fails(log, "no writer fence") + + def test_each_registered_observer_strengthens_global_plateau(self) -> None: + result, row = self.run_validator(valid_log()) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(row["readiness_observer_pids"], "42,43,44,45,46,47") + + def test_foreign_observer_before_terminal_tail_is_allowed_to_retire(self) -> None: + result, row = self.run_validator(valid_log()) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(row["status"], "passed") + + def test_terminal_observer_coverage_and_identity_are_exact(self) -> None: + self.assert_fails( + valid_log(final_observers=(42, 43, 44, 45, 46, 42, 42)), + "observer cardinality", + ) + self.assert_fails( + valid_log(final_observers=(42, 43, 44, 45, 46, 48, 42)), + "observer PID sets differ", + ) + + def test_foreign_wait_kind_inside_terminal_tail_fails_even_when_counts_match(self) -> None: + log = valid_log().replace( + "wasix-runtime-context-v1\tseq=16\twait_kind=" + WAIT_KIND, + "wasix-runtime-context-v1\tseq=16\twait_kind=" + OTHER_WAIT_KIND, + 1, + ) + self.assert_fails(log, "foreign wait kind") + + def test_other_wait_kind_cannot_hide_contradictory_global_state(self) -> None: + original = context(8, pid=44) + state(8, 1_900_000_000, COUNTS) + contradictory = context(8, pid=44, wait_kind=OTHER_WAIT_KIND) + state( + 8, 1_900_000_000, tuple(7 for _ in COUNT_NAMES) + ) + self.assert_fails( + valid_log().replace(original, contradictory, 1), "foreign wait kind" + ) + + def test_final_fence_is_a_cutoff_for_append_races(self) -> None: + contents = valid_log().encode("utf-8") + b"\xff\xfe" + result, row = self.run_validator(contents) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertEqual(row["status"], "failed") + self.assertIn("receipt-bound complete marker", row["detail"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/verify-immutable-sealed-carrier.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/verify-immutable-sealed-carrier.sh new file mode 100755 index 00000000..4109a835 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/verify-immutable-sealed-carrier.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$project_root/lib/common.sh" +source "$project_root/lib/sealed-carrier.sh" +source "$project_root/lib/qualification-identities.sh" + +usage() { + cat <<'USAGE' +Usage: verify-immutable-sealed-carrier.sh --sealed-carrier DIR --receipt FILE [--fast] + +Without --fast, perform one complete cryptographic carrier verification and +bind it to the immutable deployment receipt. With --fast, read only the small +receipt and verify the receipt-bound inode identities, modes, and +i flags. +The fast form is valid only between full campaign-boundary verifications. +USAGE +} + +carrier="" +receipt="" +fast=0 +while [ "$#" -gt 0 ]; do + case "$1" in + --sealed-carrier) + shift + [ "$#" -gt 0 ] && [ -z "$carrier" ] || { usage >&2; exit 2; } + carrier="$1" + ;; + --receipt) + shift + [ "$#" -gt 0 ] && [ -z "$receipt" ] || { usage >&2; exit 2; } + receipt="$1" + ;; + --fast) fast=1 ;; + -h|--help) usage; exit 0 ;; + *) usage >&2; exit 2 ;; + esac + shift +done +[ -n "$carrier" ] && [ -n "$receipt" ] || { usage >&2; exit 2; } +[ -d "$carrier" ] && [ ! -L "$carrier" ] || { + printf 'sealed carrier must be a non-symlink directory: %s\n' "$carrier" >&2 + exit 2 +} +carrier="$(cd "$carrier" && pwd -P)" +receipt_parent="$(dirname "$receipt")" +[ -d "$receipt_parent" ] && [ ! -L "$receipt_parent" ] || { + printf 'receipt parent must be a non-symlink directory: %s\n' "$receipt_parent" >&2 + exit 2 +} +receipt="$(cd "$receipt_parent" && pwd -P)/$(basename "$receipt")" + +arguments=(--verify-fast --carrier "$carrier" --receipt "$receipt") +if [ "$fast" -eq 0 ]; then + fresh_capture_qualification_carrier_identity "$carrier" || { + printf 'sealed carrier verification failed: %s\n' "$carrier" >&2 + exit 1 + } + arguments+=( + --manifest-sha256 "$FRESH_QUALIFICATION_CARRIER_MANIFEST_SHA256" + --wasmer-build-receipt-sha256 "$FRESH_QUALIFICATION_CARRIER_RECEIPT_SHA256" + --payload-inventory-sha256 "$FRESH_QUALIFICATION_CARRIER_PAYLOAD_SHA256" + --headless-sha256 "$FRESH_QUALIFICATION_CARRIER_HEADLESS_SHA256" + ) +fi +python3 "$project_root/lib/immutable-carrier.py" "${arguments[@]}" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/verify-sealed-headless-carrier.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/verify-sealed-headless-carrier.sh new file mode 100755 index 00000000..cd3ad053 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/verify-sealed-headless-carrier.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$project_root/lib/common.sh" +source "$project_root/lib/sealed-carrier.sh" + +if [ "$#" -ne 1 ] || [ -z "$1" ]; then + printf 'Usage: verify-sealed-headless-carrier.sh CARRIER_DIR\n' >&2 + exit 2 +fi + +fresh_verify_sealed_headless_carrier "$1" +printf 'verified sealed headless WASIX PostgreSQL carrier: %s\n' \ + "$(cd "$1" && pwd -P)" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/bin/wasix-make.sh b/src/runtimes/liboliphaunt/wasix-postmaster/bin/wasix-make.sh new file mode 100755 index 00000000..3271c44d --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/bin/wasix-make.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/common.sh" + +fresh_ensure_dirs +fresh_require_command git + +jobs="${JOBS:-$(fresh_jobs)}" +docker_bin="$(fresh_docker_bin)" +fresh_resolve_wasix_core_profile + +"$FRESH_ROOT/bin/apply-wasix-core-overlay.sh" >/dev/null + +if [ ! -f "$WASIX_BUILD_DIR/config.status" ]; then + "$FRESH_ROOT/bin/build-wasix-core.sh" --configure-only +fi + +if ! "$docker_bin" info >/dev/null 2>&1; then + echo "blocked: Docker daemon is not reachable" >&2 + exit 2 +fi +fresh_ensure_docker_image >/dev/null + +make_lock_dir="$FRESH_WORK_ROOT/.wasix-make.lock" +make_lock_waits=0 +until mkdir "$make_lock_dir" 2>/dev/null; do + make_lock_waits=$((make_lock_waits + 1)) + if [ "$make_lock_waits" -gt 600 ]; then + echo "timed out waiting for WASIX make lock: $make_lock_dir" >&2 + exit 2 + fi + sleep 0.2 +done +trap 'rmdir "$make_lock_dir" 2>/dev/null || true' EXIT + +if [ "$#" -eq 0 ]; then + set -- -j "$jobs" +fi + +quoted_args=() +for arg in "$@"; do + quoted_args+=("$(printf '%q' "$arg")") +done +make_args="${quoted_args[*]}" + +log="$REPORT_DIR/wasix-make.log" +{ + printf '\n## %s\n\n' "$(fresh_timestamp)" + printf 'make -C %q %s\n\n' "$WASIX_BUILD_DIR" "$make_args" +} >>"$log" + +docker_env=() +if [ -n "${WASIXCC_SYSROOT_PREFIX:-}" ]; then + docker_env+=(-e "WASIXCC_SYSROOT_PREFIX=$(fresh_docker_path_for "$WASIXCC_SYSROOT_PREFIX")") +fi +if [ -n "${WASIXCC_SYSROOT:-}" ]; then + docker_env+=(-e "WASIXCC_SYSROOT=$(fresh_docker_path_for "$WASIXCC_SYSROOT")") +fi + +"$docker_bin" run --rm \ + -v "$REPO_ROOT:/work" \ + -w /work \ + -e BUILD_DIR="${WASIX_BUILD_DIR#$REPO_ROOT/}" \ + -e WASIXCC_RUN_WASM_OPT="$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT" \ + -e WASIXCC_WASM_OPT_FLAGS="$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT_FLAGS" \ + -e WASIXCC_WASM_OPT_SUPPRESS_DEFAULT="$FRESH_WASIX_CORE_EFFECTIVE_WASM_OPT_SUPPRESS_DEFAULT" \ + "${docker_env[@]}" \ + "$FRESH_WASIX_DOCKER_IMAGE" \ + bash -lc " + set -euo pipefail + source ./src/runtimes/liboliphaunt/wasix/assets/build/docker_wasix_env.sh + if [ ! -e \"\$BUILD_DIR/src/include/utils/errcodes.h\" ]; then + make -C \"\$BUILD_DIR/src/backend\" generated-headers + fi + make -C \"\$BUILD_DIR\" $make_args + " 2>&1 | tee -a "$log" diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/docs/architecture.md b/src/runtimes/liboliphaunt/wasix-postmaster/docs/architecture.md new file mode 100644 index 00000000..aab0a003 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/docs/architecture.md @@ -0,0 +1,1005 @@ +# WASIX Postmaster Architecture and Promotion Gates + +## Product boundary and status + +`liboliphaunt-wasix-postmaster` is a third runtime lane. It is independent of +`pg_durable`, and it does not replace or modify the single-user WASIX runtime. +Its purpose is narrower and harder: preserve PostgreSQL's postmaster process +model closely enough to retain real backend isolation, shared-memory +coordination, sockets, extension loading, and PostgreSQL error recovery. + +This directory is currently replayable research, not a release product. Linux +x86_64 is the only host with retained evidence in +[replay-status.md](replay-status.md), and that evidence does not constitute a +release-platform qualification. Portability is an architectural requirement, +not a claim that the current patches already support every host. + +## Selected process and memory model + +The production candidate is PostgreSQL's `EXEC_BACKEND` seam. It keeps process +semantics at the runtime boundary and avoids making PostgreSQL aware of Wasmer +stores, instances, or compiler continuations. + +1. The postmaster accepts a connection and serializes PostgreSQL's normal + `BackendParameters` handoff. +2. WASIX spawn/exec creates a logical child and starts the same PostgreSQL + module with its backend entry arguments. +3. Wasmer creates a fresh store, module instance, private linear memory, guest + stack, tables, and process-local runtime state for the backend. +4. The backend restores the serialized handoff and reopens the named, + file-backed PostgreSQL shared-memory object. +5. The runtime maps that same backing at PostgreSQL's recorded guest virtual + address with `MAP_SHARED | MAP_FIXED` semantics. +6. Process-shared futexes coordinate over the shared backing; sockets, epoll, + signals, wait, files, and dynamic loading remain host-runtime services. + +The current carrier topology is one host Wasmer process and one instance group +per PostgreSQL postmaster tree. “Process” below means a logical guest process +unless explicitly qualified as an OS process. The shared-futex registry is an +in-process registry inherited by those logical children; it is not an +OS-process-shared futex implementation. A carrier that places backends in +separate host processes is a different topology and must provide and qualify a +real cross-OS-process wait/wake mechanism. + +Only mappings explicitly registered as shared PostgreSQL mappings are replayed. +Parent-private heap pages, guest stacks, tables, file-wrapper state, and Wasmer +object graphs are not copied into the child. This is the important performance +and correctness invariant: a new backend receives a clean private instance and +the minimum shared state required by PostgreSQL. + +The fixed address is not an optimization. PostgreSQL stores pointers inside +shared memory, so relocating the mapping changes their meaning. A collision, +unsupported exact mapping, wrong backing identity, or failed futex association +must abort backend creation. The runtime must never silently substitute a +private mapping or copy-on-write snapshot. + +The replay now implements the collision rule: a fresh exec child installs a +bounded, aligned allocator reservation before exact `MAP_FIXED` reattachment, +pre-grows the memory when required, rolls back failed transitions, and rejects +moving-memory configurations. Promotion still requires repeated sealed +PostgreSQL malloc/mmap/DSM churn around maximum shared mapping sizes; the +implemented protocol is not itself cross-platform qualification. + +## Production supervisor boundary + +The generic Wasmer CLI is useful for replay and diagnostics, but it is not the +production product boundary. A supported carrier should provide a small +liboliphaunt supervisor built directly on pinned Wasmer libraries, with one +native host process per PostgreSQL cluster. It owns the verified loader, one +logical WASIX process table, the shared mapping/futex registry, signals and +timers, resource policy, lifecycle cleanup, and structured telemetry. Backend +isolation is fresh Wasm-instance isolation inside that host process; it is not +OS-process crash, address-space, or resource-accounting isolation. + +The portable payload is a signed guest-set closure rather than only +`postgres.wasm`: every PostgreSQL helper, dynamic side module/extension, share +tree, and default configuration belongs to the closure. One immutable ABI +preflight computes the negotiated feature set before any side effect and makes +the same result visible to every module. Each shipped AOT object binds its +module digest to the closure digest, carrier/compiler recipe, target/CPU policy, +memory/table configuration, Wasm features and middleware, and dynamic-link +plan. + +This supervisor is also the natural cross-platform seam. PostgreSQL and the +canonical guest set stay host-independent; Linux, macOS, and Windows carriers +implement the same versioned mapping, process, wait, signal, timer, filesystem, +and loader contracts through their native adapters. The generic CLI and its +writable cache remain research tools and are never an automatic production +fallback. + +Cold embedded accounting is a separate loader/measurement contract. Full +carrier verification and initdb may execute before the measured postmaster, +but the Linux qualification lane must then synchronize and individually evict +the exact immutable-carrier and fresh-PGDATA regular files, prove zero resident +pages with `mincore(2)`, and launch without another read of either root. First +faults, storage I/O, charged file pages, dirty/writeback state, and +scope-lifetime memory peak then belong to the fresh server cgroup. See +[cold-ownership-qualification.md](cold-ownership-qualification.md); warm runs +without that receipt cannot establish cold cgroup ownership. + +## Why copied fork is historical only + +The imported experiment contains substantial copied-memory `proc_fork` prior +art: LLVM continuation capture, instance-group cloning, dynamic-linker state +detachment, and shared-mapping replay. Focused probes proved useful pieces, +including indirect calls and child-side `dlopen` after a copied fork. + +It is not the selected backend mechanism. A complete backend copies or rebuilds +too much private runtime state, makes compiler frame reconstruction part of +process correctness, and reached roughly 160 MiB child RSS before trapping in +one historical checkpoint. It remains available only as an explicit research +probe (`WASIX_CORE_CHILD_BACKEND=copied-fork`). It must not be an automatic +fallback when EXEC_BACKEND capability checks fail. + +## Host contract + +The selected design depends on a coherent set of host semantics, not merely on +the ability to instantiate WebAssembly: + +- an allocator-integrated guest virtual-address layout that reserves every + exact-address shared-mapping window in the postmaster and every exec child, + with explicit size, 64 KiB Wasm-page, and host-page alignment rules; +- exact-address, file-backed shared mappings with cross-process visibility and + no later heap/mmap allocation overlap; +- stable shared-backing identities, reference-counted futex-registry lifetime, + deterministic removal on unmap/last close, and protection against stale + inode reuse during mapping churn; +- crash-safe named shared-object ownership, stale-object reclamation, and + unlink semantics that allow a cluster to restart after a hard host failure; +- real sequentially consistent fences ordering ordinary shared loads/stores + across separately instantiated memories that alias one host backing; +- futex wait/wake keyed to the shared backing, including timeout behavior; +- fresh child instance creation, exec, child adoption and cleanup; +- blocking wait, `WNOHANG`, EINTR, SIGCHLD, and guest signal delivery; +- process-signal delivery that interrupts an indefinitely blocked private + self-pipe/epoll wait, including PostgreSQL latch wakeups; +- autonomous POSIX one-shot and interval timers with nanosecond-correct host + scheduling that can interrupt such blocked waits; +- level-triggered epoll readiness for listening sockets under connection + bursts, plus correct nonblocking and close-on-exec flags; +- filesystem sync and open-flag behavior sufficient for PostgreSQL WAL; +- WASIX dynamic loading for PostgreSQL extension side modules; +- finite runtime-owned stack limits and explicit core/open-file limits; and +- WebAssembly EH-compatible setjmp/longjmp behavior. + +These capabilities compose. Passing mmap, futex, spawn, or epoll in isolation +does not establish postmaster support; the strict blocker suite and PostgreSQL +integration gates are both required. + +## Versioned Oliphaunt host ABI + +Most current replay patches still add behavior to the broad `wasix_32v1` +surface. The range-writeback bridge is the first deliberate exception: +wasix-libc imports `fd_sync_range` from `oliphaunt_postmaster_v1`, and the +runtime exposes no compatibility alias under `wasix_32v1`. This establishes the +namespace and exact core-Wasm ABI shape, but it does not by itself negotiate +availability. An otherwise valid Wasmer build can still instantiate a module +that omits a different PostgreSQL semantic. Promotion therefore still requires +the explicit feature contract and fail-fast preflight below. + +The target contract is: + +- The canonical guest carries an + `oliphaunt.postmaster.requirements.v1` custom section. It declares an ABI + major/minor, required and optional feature bits, the expected WASIX ABI, and + the PostgreSQL/shared-memory layout version. +- Non-standard imports live under a versioned core-Wasm import namespace such + as `oliphaunt_postmaster_v1`; they are not disguised as portable WASIX + behavior. Standard WASIX calls can remain under `wasix_32v1`. +- The carrier validates the requirements section and its own signed feature + manifest before instantiating any PostgreSQL process. A diagnostic records + the guest hash, carrier hash, ABI version, and every missing feature. +- A major version changes when an existing call or bit changes meaning. Minor + versions are additive. Unknown required bits, a major-version mismatch, or a + missing minimum minor version are hard errors. +- Preflight computes one immutable `supported ∩ requested` result. Unknown + optional bits are ignored but reported, and the same result is exposed to + every helper/side module through a stable query surface. Optional imports + have guarded calls or stable `ENOTSUP` stubs; module load order cannot change + negotiation. +- Validation occurs before opening a database directory, binding a listener, + or deserializing an AOT artifact. There is no best-effort mode and no copied + fork/private-mmap fallback. + +The first feature set should distinguish at least: + +| Feature | Required semantic | +| --- | --- | +| `exec_backend_fresh_instance` | Child has new private instance state and only declared shared mappings are replayed. | +| `reserved_shared_va_layout` | Every replayed shared range is reserved in each fresh child's allocator before `MAP_FIXED`, with bounded layout and no later allocation collision. | +| `fixed_shared_file_mapping` | Exact-address mappings share one coherent file backing and write back correctly. | +| `shared_mapping_registry_lifecycle` | Mapping identity and futex registries are reference-counted, reclaimed on final unmap/close, and cannot alias stale inode reuse. | +| `shared_object_lifecycle` | Cluster-scoped shared objects have a live-owner lease, safe stale reclamation/unlink, and hard-crash restart behavior. | +| `cross_instance_memory_order` | `atomic.fence` provides a real seq-cst barrier for ordinary PostgreSQL shared fields across aliased instance memories. | +| `shared_futex` | Wait/wake crosses child instances over that backing. | +| `wait_sigchld` | Blocking wait, `WNOHANG`, EINTR, default disposition, and reaping obey the tested contract. | +| `blocked_signal_wake` | A process signal reaches its handler and wakes a backend blocked indefinitely in private self-pipe/epoll state. | +| `posix_one_shot_timers` | `setitimer` honors `it_value` and `it_interval`, uses correct time units, fires without unrelated signal polling, and interrupts blocked waits. | +| `level_triggered_socket_epoll` | Listener readiness persists while accept backlog remains. | +| `range_writeback` | Linux HostFS delegates exact `sync_file_range` offset, length, and flag semantics on PostgreSQL's read-only pre-sync descriptors under the advisory `FD_ADVISE` right, without substituting a stronger sync; unsupported adapters fail explicitly. | +| `dynamic_linking` | Main and side modules use the supported EH/PIC model. | +| `rlimit_contract` | Stack, core, and open-file values have the semantics below. | +| `wasm_eh_sjlj` | PostgreSQL error recovery can cross the supported Wasm EH paths. | + +The sysroot builder already records, binds, and validates source, patch, +toolchain-image, archive, header, and variant hashes in +`oliphaunt.wasix-libc-sysroots.v1` manifests. The runtime builder now also emits +an `oliphaunt.wasix-postmaster.wasmer-build.v2` receipt. It binds all Wasmer +and gitlink pins, both prepared source states, tracked patches, `Cargo.lock`, +the exact sysroot manifests, host ABI, Rust/LLVM provenance, artifact ABI, and +runtime ABI. It records the LLVM AOT producer and compiler-free headless +executor as separate binary digests with separate feature sets. Producer +validation recomputes every available build input immediately before atomic +publication; runtime selection validates the canonical schema, expected pins +and patches, host ABI/features, and selected binary hash without depending on +disposable compiler worktrees. + +The embedded lane adds a third native artifact without changing that producer +receipt: `oliphaunt-wasix-postmaster-executor` is built in an isolated Cargo +target with only `product-executor`. Its canonical +`postmaster-executor-build.v3` receipt binds the exact parent Wasmer receipt, +runtime ABI, package/feature set, sealed runtime policy, narrow CLI contract, +host toolchain, executor bytes, and the exact bounded-linear-memory tool and +profile identity. The runtime build recipe includes the exact +`embedded-postmaster-v1` task-budget profile, and the runtime-policy identity +names its 96-task ceiling. This separates native code +reachability from guest process semantics: fresh EXEC_BACKEND instances, +private backend state, shared-mapping replay, durability, AOT, and memory-image +contracts are unchanged. + +### Receipt-bound task and worker budget + +The product executor uses one immutable budget at both admission boundaries. +It sets `Capabilities.threading.max_threads=96` before `WasiRunner` constructs +the process tree, so `WasiControlPlane`'s exact compare-and-swap counter rejects +task 97 before its memory or instance is created. The same 96 is +`TokioTaskManagerConfig.max_threads`; its blocking pool retains one core worker +and retires every excess worker after 1000 ms. The two Tokio reactor workers +are a separate fixed executor service and are not guest backend slots. The +generic/full-headless control keeps Wasmer's default blocking-worker policy. + +The profile ceiling is above PostgreSQL 18's complete tracked child-slot shape, +not merely the four-client benchmark shape. For the embedded profile, +`InitializeMaxBackends()` gives 32: 8 connections, 4 autovacuum worker slots, +8 background workers, 10 WAL senders, and 2 special workers. PostgreSQL's +`MaxLivePostmasterChildren()` allocation then adds 18 authentication-overlap +slots, 32 compile-time I/O-worker slots, and 8 other fixed PM-child roles, for +90 tracked children. One root postmaster plus five overload/helper slots gives +the receipt-bound budget of 96. `io_method=sync` means the 32 I/O slots cannot +become active in this profile, but retaining them in the derivation makes the +admission proof conservative. + +Control-plane task count is deliberately not execution-lease count. Each +guest process main thread owns one `TaskCountGuard`; execution leases are +lifecycle ownership tokens around the same callback. The qualified idle shape +therefore has six active tasks and six live guest threads even though it has +eleven execution leases. Multiplying the budget by lease count would create +host workers that no guest thread can use. Conversely, unauthenticated +dead-end children are intentionally unbounded in upstream PostgreSQL and +cannot be promised unbounded residency by an embedded host. The five reserve +slots absorb bounded transitions; sustained excess ingress fails as resource +admission rather than growing host threads without limit. + +The profile bytes participate in `fresh_runtime_build_recipe_sha256`; that +digest is in both the Wasmer and postmaster-executor receipts and therefore in +the verified carrier closure. Changing the GUC capacity, formula, budget, +blocking core, or retirement interval rotates the runtime ABI and invalidates +old executors/carriers. This is a candidate containment policy until overload, +recovery, replication, and maximum-role qualification pass on the rebuilt +carrier. + +These are useful fail-closed build-provenance layers, but neither is runtime +feature negotiation or a sealed release carrier. The receipt is unsigned and +does not bind the PostgreSQL guest/install tree, native runtime libraries, or +AOT cache. The guest custom section, complete migration and feature negotiation +for non-standard imports, pre-instantiation host check, and signed full-bundle +verifier remain promotion gaps. The versioned `fd_sync_range` import is a +grounded first slice, not evidence that this broader contract is complete. + +## Canonical guest and sealed host carriers + +There should be one canonical PostgreSQL guest-set closure for a release, +built from exact PostgreSQL, wasix-libc, extension, toolchain, patch, share-tree, +and configuration inputs. Its canonical manifest, module/file bytes, and +digests are the portable source of truth; detached signatures, provenance, and +qualification attestations reference that immutable closure. Host differences +belong in sealed carriers: + +- a pinned, patched Wasmer runtime and required native libraries; +- a host adapter implementing the versioned feature set; +- a manifest containing host OS/architecture, runtime and ABI versions, + feature bits, source and patch digests, build configuration, and hashes for + every shipped file; and +- signatures and provenance sufficient to reject mixed or locally mutated + runtime/libc/guest combinations. + +Each carrier is qualified independently. Sharing canonical Wasm does not permit +a Linux result to qualify macOS or Windows. + +### Historical five-module sealed headless research carrier + +The Linux x86_64 research lane implements the execution shape below the +release trust boundary. Its fixed five-module inventory is historical: the +guest proof closure now contains 27 runtime-loadable side modules, so no new +general-purpose carrier is admitted until both inventories come from +`runtime/policies/sealed-side-modules.v1.tsv`: + +- `build-sealed-headless-carrier.sh` accepts only an already validated v2 + receipt and its exact precompiled LLVM AOT bucket. It does not compile or + accept native-CPU code implicitly. +- The atomically published closure contains only one native executor plus + `initdb`, `postgres`, + `libpq.so.5.18`, `dict_snowball.so`, and `plpgsql.so`, their five AOT + artifacts, deterministic memory images and receipts for both executables, + the PostgreSQL support tree, with the selected executor installed at + `bin/wasmer-headless`. Product carriers contain + `postmaster-executor.receipt`; its exact presence selects the product role. + Full-headless controls omit the sidecar and remain bound directly to the v2 + Wasmer receipt. No carrier contains both executors. It contains no symlinks + or special files and has a complete hash/size inventory. +- The product verifier streams that inventory from opened regular files and + requires an exact directory/file set. It rejects unsafe or unsorted paths, + duplicate manifest/receipt keys, missing or unexpected entries, symlinks, + special files, unlisted empty directories, and files whose identity changes + while they are read. The builder, standalone verifier, and benchmark + preflight apply the same policy. +- The strict format-6 `oliphaunt.wasix-postmaster.sealed-aot.v5` manifest binds + the raw modules, AOT artifacts, executable memory image metadata, source and + patch identities, compiler/capture recipe, artifact/runtime ABI, generic CPU + policy, target and host ABI, LLVM producer, and headless executor. Producer + and executor identities remain distinct. It also binds `core-profile` and + `guest-build-recipe-sha256`; the canonical guest receipt includes the + effective flags and an installed-closure identity recomputed from both the + source prefix and staged carrier. A deny-unknown `file-cache-policy` object + additionally binds the requested adaptive algorithm ID, the sole approved + compiled config ID and SHA-256, and the observe-only portable fallback. + Executable selection makes `initdb` unconditionally observe-only while + allowing only `postgres` to request the approved adaptive policy. +- The headless loader opens regular non-symlink files, validates the same + mapped bytes it deserializes, checks the embedded raw-module digest, and + preloads an in-memory module registry. Exact sealed paths shadow the mutable + guest filesystem; other paths retain ordinary filesystem replacement + semantics. +- The builder uses an image-free format-4 `sealed-aot.v3` manifest only as a + capture capability. It performs two captures in independent headless stores + for each executable and requires byte-identical images and receipts. The + provisional manifest and capture workspaces are removed before inventory. +- The builder validates final `postgres --version` and a real disposable + `initdb` bootstrap through the image-bearing manifest, runs the exact inventory + verifier, fsyncs the payload, and renames the complete directory into place. +- Its default publication name contains both the runtime ABI prefix and the + full final `payload.files` SHA-256. Content identity is computed after + staging, so two PostgreSQL build profiles sharing one executor ABI cannot + collide at an ABI-only carrier path. + +This closes the warm-exec raw-Wasm read/hash/compile path without collapsing +fresh backend instances. It is a digest-verified local research carrier. The +payload inventory and strict loader are not a substitute for release signing, +transparent provenance, dependency closure, selection-time policy, or +per-platform qualification. + +### Linux direct immutable deployment + +The ext-family embedded candidate adds a deployment boundary after carrier +construction. A privileged, capability-bounded step verifies the complete +payload closure, publishes an external root-owned immutable receipt, and marks +every carrier file and directory with `FS_IMMUTABLE_FL`. Execution is a +separate unprivileged phase. Its pre-run policy binds the receipt SHA-256, +device and inode plus carrier closure identity, and Wasmer must report +`direct-immutable-inode` with zero loader writes and sync calls for each AOT +and selected executable memory image. Reflink and streamed compatibility modes +cannot satisfy the candidate qualification. + +Full content hashing occurs once at each qualification campaign boundary. +Between those boundaries, the external immutable receipt supplies the frozen +content identities while a fast verifier checks every receipt-bound inode's +device, inode, type, size, mode, uid/gid, and immutable flag without rereading AOT or +memory-image contents. This prevents integrity bookkeeping from preheating the +cold lane or adding roughly one carrier read per measured sample. +Carrier ownership is preserved and receipt-bound; only the external receipt is +required to be root-owned. The authority boundary is +`CAP_LINUX_IMMUTABLE`, not a destructive recursive ownership conversion. + +The generic loader also recognizes direct mappings on a read-only filesystem, +which is the intended SquashFS/EROFS portability seam; that mode is not +silently treated as ext immutable evidence. The full transition order, crash +recovery journal, unprivileged qualification commands, and remaining +ancestor-path race assumption are specified in +[immutable-carrier-deployment.md](immutable-carrier-deployment.md). + +The tracked `pin-runtime-artifacts.sh` utility is intentionally below this +boundary. It copies a matched Wasmer/receipt pair, PostgreSQL install, and cache +for local benchmark replay, but its file index is not a signed selection-time +trust root. A product bundle needs a canonical +`oliphaunt.wasix-postmaster.bundle.v1` manifest whose verifier derives paths +from the bundle root and binds the Wasmer receipt, canonical guest and install +trees, native dependencies, sysroot/ABI variant, compiler and stack settings, +qualification evidence, AOT identity, host ABI/CPU requirements, and every +shipped file before executing or sourcing anything from that bundle. + +### AOT cache rules + +AOT output is a derived, host-specific accelerator, never the distributable +semantic source of truth. A cache key must include at least: + +- guest-set closure digest and exact module digest; +- sealed carrier, AOT-producer binary/recipe, and runtime-executor binary + digests as separate identities; +- Wasmer serialization/artifact ABI version; +- exact Rust target triple/environment, minimum platform ABI, native page + granule, and baseline plus actually used CPU features; +- compiler backend/recipe and every code-generation/optimization flag; +- Wasm features, memory/table tunables, middleware, and dynamic-link plan; and +- Oliphaunt host ABI version and negotiated feature set. + +The carrier must distinguish two artifact classes. Shipped AOT is an immutable, +signed carrier file. A locally derived cache is mutable performance state and +is never a trust root. Wasmer serialized modules contain host-native executable +code; the current +[`Module` API](https://docs.rs/wasmer/latest/wasmer/struct.Module.html#method.deserialize) +also documents deserialization as unsafe because it loads executable code. The +pinned API's safe-named file deserializer delegates to its unchecked +deserializer, so the production carrier must own the loader boundary instead +of trusting the Wasmer CLI auto-cache. + +The implemented research loader opens without following links, verifies size +and hash over the exact opened mapping, checks the target, host ABI, artifact +ABI, runtime ABI, CPU policy, producer recipe, executor identity, and raw-module +digest, then deserializes those same immutable bytes without a path-reopen race. +Release verification must add signature and provenance validation around this +boundary. A compiler-bearing carrier may discard an invalid local entry and +compile the canonical guest, but it must authenticate any artifact reused +across runs. A headless carrier cannot recompile: it must ship matching AOT and +fail closed when it is absent or invalid. Cache entries are never copied +between host triples, runtime builds, or CPU feature sets, and cache presence +must not change database behavior. + +The sealed raw-module-digest to AOT mapping belongs to the manifest-owned module +registry. It must not be moved into a generic `engine-headless-*` cache bucket: +that namespace is derived from the consumer, while AOT provenance belongs to +the compiler-bearing producer and its exact recipe. Keeping those identities +separate also permits a smaller executor without falsely claiming that it +produced the native code. + +### Detached AOT runtime image + +The sealed loader uses a dedicated detached deserialization path. Wasmer first +validates the complete serialized archive, allocates and links executable +sections, and materializes only the module metadata, owned data initializers, +CPU-feature bits, and frame information needed at runtime. It then drops the +serialized archive and its file mapping. The resulting module executes +normally but cannot be serialized again; attempting to do so is a hard error. + +Detachment removes redundant host-side retention after executable code and +runtime metadata have been published. The sealed memory-image path separately +replaces the verified full 64-KiB pages below the guest stack with private +file-backed mappings after module start. It does not deduplicate WASIX process +state, tables, stacks, PostgreSQL backend state, the initialized partial page +adjacent to the stack, or any page later dirtied by the guest. Those remaining +classes require the instance/PSS ladder rather than another semantic shortcut. + +The loader receipt does not infer reclamation from a successful hint. On Linux +it takes non-faulting `mincore(2)` checkpoints through a temporary `PROT_NONE` +descriptor mapping after the exact hash/inspection, immediately after detached +deserialization has released the archive, and immediately after +`POSIX_FADV_DONTNEED`. The receipt persists hint call/success/errno data and +resident pages/bytes independently. This makes a kernel-retained archive page +visible without reading it again, delaying launch, dropping process-wide +caches, or applying advice to the anonymous executable allocations that the +detached engine has already published. Preinitialized images use the same +source-file boundary, while separately recording the `MADV_DONTNEED` result on +their short-lived verification mapping; the later private image mapping and +its dirty COW pages are not the target of this loader audit. + +Compatibility activation has two possible cache owners. A streamed copy reads +the original carrier inode and hashes a private snapshot; a reflink hashes a +private COW inode without a userspace source read. The snapshot object therefore +retains the exact original source descriptor until activation completes. It +records and advises original-source and activation-snapshot residency +separately, then releases both descriptors. Direct immutable activation has no +second inode and marks snapshot-only advice not applicable. This distinction is +required even though promotion rejects compatibility modes: otherwise a +successful private-snapshot DONTNEED could be misreported as reclaiming the +original streamed carrier pages. + +This is already a correctness issue in the replay, not just release hardening. +The pinned LLVM compiler's original `deterministic_id()` included only the +optimization level, so default and `--disable-non-volatile-memops` runs reused +the same native artifact. The local patch versions that identity and binds NaN +canonicalization, volatile-memory semantics, read-only table policy, the full +O3/indirect-call toggles, and PIC mode, with a focused negative identity test. +The product-owned key above remains broader and authoritative. + +## Platform support boundary + +| Host | Current status | What is still required | +| --- | --- | --- | +| `linux-x64-gnu` | Runtime and release-O3 core proofs are current; the historical five-module carrier, private-image mapping, cgroup runs, and throughput characterization were exercised on one host. The complete 27-side-module carrier is blocked; not a release claim. | Derive the full carrier closure from policy; then repeat CI, signing/provenance, timer, crash/recovery, churn, regression, ABI-negotiation, latency, and memory gates. | +| `linux-arm64-gnu` | Unqualified | Build a native carrier and run the complete blocker, memory-order, latch/timer, integration, recovery, extension, memory, and performance ladders. | +| `macos-arm64` | A Unix private-image mapping implementation exists, but the carrier is unqualified | Prove allocator-safe exact coherent shared mappings, private-image ownership/replacement and footprint, cross-instance barriers, futex equivalent, process/signal/kqueue-or-poll/timer adapters, sync behavior, and dynamic linking. | +| `windows-x64-msvc` | Unsupported/blocked | The private-image mapping backend and reserved fixed-address coherent shared-region backend are not implemented; placeholder replacement, cross-instance ordering, process, wait/signal/timer, readiness, filesystem, and DLL carrier work all require implementation and qualification. | +| macOS x64, Windows arm64, musl, mobile, BSD | Future/out of catalog | Add repository release plumbing and independently implement and qualify a native carrier before resolution metadata may expose it. | +| Browser/worker runtimes | Full postmaster excluded | Browser hosts do not provide the required listener, process/wait, durable filesystem, and exact coherent shared-mapping contract. The separate single-user product remains the browser path. | + +“All platforms” therefore means the same canonical guest plus separately +implemented and sealed native host carriers. It does not mean emulating missing +host semantics in PostgreSQL or advertising Wasmer's general platform matrix as +postmaster support. + +Every native carrier is additionally bound to an exact Rust target/ABI, +minimum OS/kernel/libc, native page and mapping granule, filesystem/locking +semantics, and an explicit baseline/optional CPU-feature policy. Matching the +host name alone is insufficient for carrier selection. + +The native mapping adapters cannot be a single conditional wrapper around +`mmap`. Linux's +[`MAP_FIXED_NOREPLACE`](https://man7.org/linux/man-pages/man2/munmap.2.html) +can reserve without clobbering but does not itself replace an owned reservation; +the adapter must prove ownership of a bounded placeholder before an exact +replacement. Windows provides explicit placeholder reservation and exact +replacement through +[`VirtualAlloc2`](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc2) +and +[`MapViewOfFile3`](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-mapviewoffile3), +with 64 KiB allocation-granule constraints outside placeholder replacement. +The current Unix source path is present for macOS but unqualified. If its +`mmap` ownership/replacement semantics cannot meet the contract, the macOS +carrier needs a Mach VM implementation grounded in +[`mach_vm_map`](https://developer.apple.com/documentation/kernel/1402481-mach_vm_map) +and separately proved replacement/ownership behavior. These are candidate +primitives, not support evidence. + +## Immutable post-start linear-memory image + +The implemented research path is a module-start image, not a snapshot of a +running PostgreSQL process and not a static reconstruction of data segments. +The current PostgreSQL Wasm modules use passive segments, BSS fills, an atomic +initialization protocol, data drops, and LLVM global-TLS relocation work inside +`__wasm_init_memory`. Reconstructing only segment bytes would omit semantic +effects, while marking memory initialized would incorrectly skip per-instance +global state. + +The carrier therefore executes ordinary module start in each independent +capture store and snapshots only the complete 64-KiB pages below `stack-low`. +It repeats the capture from a second independent store and requires the image +and receipt bytes to match. The final identity binds at least: + +- canonical module, AOT, and guest-source/closure digests; +- runtime and artifact ABI, memory type, initial/maximum pages, fixed 64-KiB + mapping alignment, and Wasm feature set; +- the post-module-start/pre-link-relocation phase, image digest and mapped + length, module memory base, dylink size/alignment, initial/maximum pages, + shared-memory bit, mapping alignment, and stack-low boundary; +- target, host ABI, CPU policy, producer recipe, and executor identity; and +- the capture receipt itself in the complete carrier payload inventory. + +Instance creation still constructs a fresh store, WASIX process, globals, +tables, stacks, descriptors, clocks, signals, locks, and extension state, and +then runs normal module start. Immediately afterward, before dynamic module +loading, relocations, or constructors, the runtime validates the module and +layout receipt, compares the live prefix byte-for-byte, and replaces only those +identical pages with the immutable image as a writable private mapping. This +keeps global TLS relocation and data-drop semantics per instance while making +clean initialized pages reclaimable/shareable. A private write faults a COW +page visible only to that instance; `memory.grow` and PostgreSQL's separately +declared coherent shared mappings retain their normal behavior. + +The image may contain deterministic linear-memory TLS initializer bytes because +ordinary start has just reproduced them, but it never contains guest-entry +stack state, mutable globals, tables, descriptors, timers, locks, WASIX process +objects, or a running postmaster/backend snapshot. The final sealed carrier +fails closed on an identity, layout, granule, size, digest, or byte-comparison +mismatch before side-module code can run. It does not set the LLVM initialization +guard or skip `__wasm_init_memory`. + +### Native image adapters + +The shared template bytes and invariants are cross-platform; the mapping +mechanism is not: + +| Host adapter | Candidate implementation | Required proof | +| --- | --- | --- | +| Linux | Implemented: seal the verified image against mutation, then remap it `MAP_PRIVATE` at an allocator-owned reservation. `MAP_FIXED` is used only after exact ownership/range checks; PostgreSQL shared windows remain separate coherent file mappings. | Focused seal/identity, partial-page, private/shared remap, COW-isolation, lifetime, grow, and sparse-copy tests pass. Release still needs repeated pressure/reclaim and huge-page policy evidence. | +| macOS | The Unix mapper implementation exists, but no macOS evidence has been collected. Its intended contract is a verified vnode/private-copy mapping into a carrier-owned reservation. | Native page granule, copy semantics, reservation replacement, codesign/hardened-runtime/JIT interactions, physical-footprint accounting, and shared-window coherence. | +| Windows | Unsupported. The required placeholder/section mapper is not implemented. The intended design uses a read-only section with write-copy protection and a separate read/write section for declared PostgreSQL shared ranges. | Implement and prove 64 KiB allocation granularity, placeholder splitting/replacement, section identity, COW isolation, CFG/unwind/protection compatibility, working-set/commit behavior, and shared-window coherence. | + +The Linux row describes implemented local research; the macOS and Windows rows +remain design requirements, not support claims. A Linux result does not +establish that either other adapter is correct. + +### Image semantic and performance gates + +Before an adapter can be promoted, automated tests must: + +1. Compare ordinary and template instantiation byte-for-byte through startup, + `memory.grow`, extension load, error unwind, signals, timers, and shutdown. +2. Start at least two instances, mutate canaries on every template page, and + prove private writes never cross instances while declared PostgreSQL shared + ranges remain coherent and their futex/latch wakes cross instances. +3. Randomize initial/maximum memories, initializer offsets, sparse holes, + guard boundaries, shared-window collisions, granules, and repeated + map/unmap/reuse; fail closed on every wrong digest, ABI, target, CPU, size, + protection, or page-layout input. +4. Exercise crash/restart, backend churn, dynamic loading, deep PostgreSQL + error paths, and at least 100 fresh-postmaster liveness repetitions on each + claimed carrier. +5. Measure cold start, warm backend launch, page faults, dirty/private pages, + PSS or the native equivalent, cgroup/job pressure, and tail latency at + N=0,1,2,4,8,16 live and reaped backends. Clean template pages must be shared + or reclaimable, and the marginal dirty-page slope must meet an explicit + embedded budget. + +Promotion is per target/ABI/OS floor. Resolver metadata may expose an adapter +only after its full correctness, tamper, pressure, performance, and lifecycle +matrix passes; there is no aggregate “Wasmer supports this OS” shortcut. + +## Child wait and signal reliability + +The experiment exposed two independent races and one runtime lifecycle gap: + +1. A direct `proc_join` can return WASI errno 27 (`INTR`) after clearing its + in/out PID selector. PostgreSQL's direct join shims now retry EINTR and + reconstruct the tagged PID before every attempt. +2. SIGCHLD can arrive before the application calls `sigaction`. wasix-libc now + materializes its built-in default handler before the first indirect callback, + avoiding an uninitialized function-table entry while preserving default and + ignored dispositions. +3. The patched Wasmer join path implements true nonblocking “any child” lookup, + processes signals during blocking waits, and removes an adopted child only + once an exit status is available. + +These fixes are one contract. Removing the PostgreSQL retry because the Wasmer +wait path improved would reintroduce an ordinary POSIX EINTR bug. Likewise, +handling EINTR alone does not make early default SIGCHLD dispatch safe. + +As of the 2026-08-08 upstream review, current Wasmer main still does not contain +this complete wait/reap/spawn-cleanup behavior, and current wasix-libc retains a +different `waitpid(..., WNOHANG)` status-decoding defect. The patched runtime, +and eventually every sealed carrier, must therefore retain and probe the local +patches until equivalent upstream +changes are pinned and verified. + +Immutable upstream snapshots supporting that boundary are Wasmer's +[`proc_join`](https://github.com/wasmerio/wasmer/blob/1276e8462a66a2a218ae1d1600de5fa6afd0be4a/lib/wasix/src/syscalls/wasix/proc_join.rs#L116-L205) +and +[`proc_spawn3`](https://github.com/wasmerio/wasmer/blob/1276e8462a66a2a218ae1d1600de5fa6afd0be4a/lib/wasix/src/syscalls/wasix/proc_spawn3.rs#L125-L186), +plus wasix-libc's +[`waitpid`](https://github.com/wasix-org/wasix-libc/blob/35224ad8f837e35a9c76d9474c455156a2330a7f/libc-top-half/musl/src/process/waitpid.c#L24-L61). +Likewise, stock wasix-libc +[`mmap`](https://github.com/wasix-org/wasix-libc/blob/35224ad8f837e35a9c76d9474c455156a2330a7f/libc-bottom-half/mman/mman.c#L322-L447) +does not provide the fixed coherent shared-page contract used here. Wasmer's +[general runtime platform matrix](https://docs.wasmer.io/runtime/features/) +must not be read as a PostgreSQL postmaster support matrix. + +## Cross-instance latch ordering and timer wakeups + +The backend-wave gate exposed a stronger failure than a generic lock timeout. +With one persistent diagnostic connection, one PostgreSQL backend reported +`Lock/extend` for 600 samples while its relation `ExclusiveLock` was +`granted=true` and `pg_blocking_pids` was empty. Three peers queued behind it, +and unrelated backends continued updating through the same postmaster. The +heavyweight lock manager had therefore completed `GrantLock`; the granted +backend remained asleep in the `ProcSleep`/`WaitLatch` path after `SetLatch`. + +The historical safe-O2 PostgreSQL module contains `atomic.fence` for +`pg_memory_barrier()`. Wasmer's repaired LLVM backend emits an LLVM +sequentially consistent fence when that operator reaches it. This carrier +deliberately aliases file-backed pages across distinct Wasm instances, so the +operator remains required even though the WebAssembly threads specification +defines +[`atomic.fence`](https://webassembly.github.io/threads/core/syntax/instructions.html) +as a synchronization primitive. + +The current release-O3 pipeline has a two-stage concurrency contract. The +linked module contains 1,111 `atomic.fence` operators. Exact export-closure DCE +removes 116 fences in dead functions, leaving 995 in the sealed module. +`SetLatch`, `ResetLatch`, and `WaitEventSetWait` retain exactly 2, 1, and 1 +fences respectively, together with their packed atomic load/RMW operators; +all 4,739 function-table entries are unchanged. Both inventories are checked +and source-signature-bound. Unmeasured profiles fail closed instead of +borrowing these release-O3 totals. + +The product-scoped repair is guarded by +`PG_WASIX_ATOMIC_LATCH_STATE`. It packs SET and SLEEPING into one lock-free SC +atomic word, preserves the upstream `Latch` layout with a reserved word and +static offset checks, and makes wake responsibility an atomic clear-to-set +transition. Existing fences remain a secondary contract. Each optimized +carrier must prove the packed atomic load/RMW operators and fences in its final +module, then pass the focused latch and repeated fresh-postmaster ladders. + +Timers remain an independent known gap. The pinned wasix-libc `setitimer` +implementation derives the delay from `it_interval` rather than `it_value` and +requests repetition even for PostgreSQL's one-shot timers. The pinned Wasmer +interval path interprets a WASI nanosecond timestamp as milliseconds, records +but does not consume the repeat flag, and polls timers only while processing +another signal. It therefore cannot autonomously wake a backend blocked +forever in epoll. `deadlock_timeout`, `statement_timeout`, and related +PostgreSQL recovery/diagnostic timers cannot be considered implemented. The +focused 100 ms one-shot diagnostic reached its 2,001 ms bound with zero alarms; +promotion requires it to interrupt a blocked wait exactly once with correct +units, followed by interval, disarm, replacement, race, and PostgreSQL timeout +coverage. + +## Shared-object and registry lifecycle + +The patched mapping/futex registry is keyed by backing device/inode (or the +native Windows volume/file identity) within one host runtime process, but its +table owns only a weak reference plus an allocation-generation token. Every +live mapping and in-flight waiter owns the registry strongly; the registry in +turn owns the exact same `Arc` already used for the mapping. That pins +the backing identity without opening another host descriptor. The last strong +drop removes only its exact generation, so an old destructor cannot erase a +replacement after identity reuse. A deterministic round-robin sweep examines +at most 16 weak slots per lookup as a poisoned-lock/abandoned-slot backstop, +and telemetry separates active, stale, and total slots. + +Focused ownership, replacement, split-mapping, pruning, and fork-state tests +plus the full WASIX library suite cover the in-process invariants. Promotion +still requires repeated real PostgreSQL DSM map/unmap, forked futex wait/wake, +host-FD, and registry-slot plateau evidence; synthetic ownership tests alone +do not prove the complete guest syscall chain under churn. The implemented +untimed lifecycle lane closes each measured window with a runtime-writer +flush/fence record followed by a separate committed ACK containing its exact +synced-log end offset. It validates one global sequence beginning at one, +freezes only that committed byte prefix, re-reads the ACK to close replacement +races, and binds ACK/frozen hashes and the offset into a receipt that the +validator checks independently. Its terminal stable tail rejects foreign +observers, foreign wait kinds, stale runtime/shared registry entries, slot +imbalance, count drift, and sample gaps. The legitimate idle PostgreSQL shape is +six registered processes and six runtime states in one rooted topology with +five child edges. It has eleven execution leases: six active tasks plus five +parent continuations suspended across the EXEC_BACKEND `vfork`/`exec` edges. +The exploratory validator requires those relational invariants and exact +readiness-to-post-reconnect equality; it also requires no pending child +publication, quiescence waker, retiring node, or stale entry. + +Cold readiness is not the stable baseline. PostgreSQL 18's background writer +logs a running-transactions snapshot on its first approximately 15-second +maintenance interval. That record publishes an asynchronous WAL LSN and wakes +the WAL writer, which opens the current segment on first write and deliberately +retains its VFD while the segment remains current. The verbose trace proves +that the persistent aggregate guest-FD transition from 70 to 71 belongs to PID +6 opening `pg_wal/000000010000000000000001`; PID 7's `global/1262` descriptor +was already present in the earliest inventory and did not cause the delta. + +The lifecycle lane advances this timer-controlled lazy state with an event +barrier rather than a guessed delay or an FD-name exception. During a dedicated +maintenance phase it samples the `pg_stat_io` row for +`backend_type='walwriter'`, `object='wal'`, `context='normal'`, invokes +`pg_log_standby_snapshot()`, and records the returned LSN. It then polls using +fresh statistics snapshots until both `writes` and `write_bytes` increase and +`pg_current_wal_flush_lsn()` reaches that LSN, rejecting a changed +`stats_reset`. Readiness measurement begins only afterward. This exercises the +same `LogStandbySnapshot` -> asynchronous-LSN -> WAL-writer path as the periodic +event without relying on timing, filesystem paths, or runtime-internal FD +knowledge. It requires `wal_level >= replica` and is unavailable in recovery; +those preconditions fail closed instead of silently restoring the cold +baseline. + +Every compact field is present in a versioned, pre-run-hashed baseline policy +bound to the resolved PostgreSQL profile, runtime, guest module, and carrier +identities. The checked-in policy is deliberately `exploratory-unbounded` with +`claim_scope=relative-to-fresh-baseline`: its equality gate proves reconnects +did not accumulate state relative to readiness, not that every nonzero +readiness resource is intrinsically leak-free or small. Promotion requires a +separate clean PG18 run and a distinct qualified policy whose bounds are exact +for every observed field; changing only the exploratory status or leaving a +near-u64 range is rejected. Absolute PSS/cgroup budgets govern legitimate +baseline size. +Wait-dump instrumentation is prohibited in timed throughput and latency +qualification because it changes the parked-wait hot path. + +Performance composition is also fail-closed. The checked-in composite policy +records unresolved same-host-session, memory-workload-shape, and +memory-sampling contracts. Its CLI can only publish an immutable failed, +non-release receipt and never a component manifest. Enabling a positive path +requires one timestamp-free host/boot identity shared by all four lanes plus a +pre-run memory receipt binding the complete 100k-row/100k-iteration workload, +controlled checkpoints, full resource evidence, and the 0.1-second sampling +interval. A caller-supplied campaign nonce is not a substitute for host facts. + +Hard-crash restart is also unresolved. The PostgreSQL WASIX overlay currently +treats an existing named main-shared-memory object as live because its +`PGSharedMemoryIsInUse` path has no reliable lock/lease primitive. A terminated +runtime can therefore leave an object that prevents restart until external +cleanup. The carrier contract must provide collision-resistant cluster +identity, a live-owner lease, safe stale reclamation and unlink semantics, and +crash/restart qualification on each filesystem/platform. + +## Resource-limit semantics + +The patched runtime implements `proc_rlimit_get`, consumed by wasix-libc +`getrlimit`: + +- `RLIMIT_STACK`: current and maximum are the runtime-owned finite guest stack + policy (4 MiB in the recorded blocker run), allowing PostgreSQL's + `max_stack_depth` guard to fire before host-stack exhaustion; +- `RLIMIT_CORE`: current and maximum are zero because the guest runtime does + not produce POSIX core files; +- `RLIMIT_NOFILE`: current and maximum are `RLIM_INFINITY`; actual descriptor + allocation can still fail, but PostgreSQL is not given a false host soft + limit; and +- unsupported resources return `EINVAL` rather than fabricated values. + +The contract currently covers reads, not mutable `setrlimit` semantics. Any +future finite descriptor policy or guest core support requires a versioned +feature/ABI change and new probes. + +## Performance and memory model + +The target is native single-backend-like steady-state behavior while retaining +real postmaster concurrency. Measurements must separate cold AOT production, +postmaster startup, warm backend launch, persistent-connection query latency, +connect/select/disconnect latency, fan-out bulk throughput, tail latency, +WAL/fsync-heavy work, and extension/error paths. The concurrent harness's bulk +fields measure batch wall time and derive logical row operations per second; +they do **not** measure request or transaction throughput, backend launch +latency, or a per-query p95. Its opt-in native-libpq lane separately records raw +`CLOCK_MONOTONIC` persistent-query samples and complete `PQconnectdb` through +`SELECT 1` and `PQfinish` reconnect/backend-launch samples. The lane exists and +fails closed, but statistically useful paired WASIX/native evidence remains a +promotion requirement. + +The production memory budget has five independently measured parts: + +1. fixed supervisor/runtime and verified module cost; +2. PostgreSQL's file-backed shared mappings, counted once by physical identity; +3. marginal private memory for each live logical process; +4. transient backend-launch and workload peaks; and +5. kernel, page-table, socket, and file-cache charge owned by the carrier's + resource group. + +Aggregate host RSS alone cannot supply those values. Native PostgreSQL's +process-tree RSS counts shared mappings once per OS process. The WASIX carrier +maps the same PostgreSQL shared-memory file at a different address in every +guest instance, so even its one host process can count the same physical file +page at several aliases. VSZ is dominated by a 4 GiB static memory bound plus a +2 GiB guard per instance and is largely zero-resident. Linux qualification uses +PSS/private mapping data together with a dedicated cgroup's physical charge; +other hosts require their native physical/private/commit equivalents and +shared-backing identity. See [rss-memory-model.md](rss-memory-model.md) for the +measurement contract and full attribution. + +The historical five-module carrier with runtime ABI +`995f6a9bf69ce6ff154533369eb4f9d6c45d9dfca13fdc213e0f6be8ae405217` +and payload inventory +`8e907e600fa9d7197c2ae98ddece5cb3093e4e7e3caf8f27f325a24955c120a7` +with manifest +`cea8c0933fa01f6646184c1f97c2156300e50bcf8a5d1d2e38fbb4ed2bb11fec` +produced the last retained budgeted Linux x86_64 result. On the one measured host, +`final-995f-embedded-c4-lower-pressure-v1` passed the predeclared +`embedded-c4-lower-pressure-v1` four-client budget: + +| Readiness PSS | Peak fan-out PSS / anonymous PSS / page tables | Whole-run cgroup peak and limits | Required-phase pressure | Bulk rates read/mixed/update/insert | +| ---: | --- | --- | --- | --- | +| 66.862 MiB | 132.708 / 77.473 / 1.164 MiB, within 160 / 96 / 2 MiB | 224.707 MiB; 256/224/0 MiB hard/high/swap | 2,528 high events; PSI some/full 0.003289; no max/OOM/OOM-kill/swap | 352,846.832 / 359,874.044 / 42,854.082 / 140,449.438 ops/s | + +The row is a passed memory-performance run for one exact carrier, Linux host, +and workload shape. The profile remains `candidate-unqualified` until the other +composition lanes pass; it is not a release, all-workload, latency, or +cross-platform claim. + +The original compiler-bearing carrier used 226.940 MiB readiness PSS. The +successive retained evidence runs below show the architecture's historical +optimization trajectory. The values are named run observations, not a claim +that each delta belongs exclusively to the feature in its label. + +| Evidence run | Readiness or quiescent-readiness PSS | Boundary exercised | +| --- | ---: | --- | +| `embedded-pss-baseline-c1-i100k` | 226.940 MiB | Historical compiler-bearing baseline. | +| `sealed-detached-paired-c1-i100k-r2` | 150.062 MiB | Compiler-free sealed registry and detached AOT. | +| `cow-image-fixed-c1-i100k` | 129.929 MiB | Corrected immutable post-start private image. | +| `cow-arc-import-c1-i100k` | 113.901 MiB | Shared immutable linker/import ownership. | +| `cow-lazy-artifact-c1-i100k` | 79.101 MiB | Exact eager exports with lazy AOT activation of the remainder. | + +The prior safe-O2 carrier then passed the following explicit cgroup gates with +swap disabled. They remain historical comparison points rather than the final +`995f6a9b…` carrier's budget result: + +| Run | Hard/high limit | Result | Relevant physical evidence | +| --- | --- | --- | --- | +| `cap-final-wasix-c1-100k-192m` | 192/160 MiB | All four 100k workloads passed; no max/OOM/swap event | 77.995 MiB quiescent readiness PSS; fan-out peaks 98.640/121.291/149.189/154.604 MiB for read/mixed/update/insert. | +| `cap-content-addressed-final-c1-read-96m` | 96/88 MiB | Indexed read passed through the final content-addressed carrier; no max/OOM/swap event | 77.761 MiB readiness, 95.171 MiB fan-out PSS, 88.629 MiB cgroup peak, and 220 high events. | +| `cap-final-wasix-c4-100k-192m-pressure` | 192/176 MiB | All four workloads passed; no max/OOM/swap event | Reclaim pressure reduced write throughput; this is a survival tier, not the preferred performance budget. | +| `cap-final-wasix-c4-100k-256m` | 256/224 MiB | All four workloads passed; no max/OOM/swap event | Fan-out PSS 107.576/195.131/219.062/219.653 MiB; lower-pressure sizing characterization only because no PSS/high/PSI budgets were predeclared. | + +The 192 MiB four-client run's mixed-write, update, and insert rates were about +13%, 25%, and 19% below the 256 MiB run respectively, consistent with active +reclaim rather than a free memory reduction. `cap-final-wasix-c4-read1m-maps` +measured 78.068 MiB quiescent readiness, 108.274 MiB during four active read +clients, and 93.674 MiB after quiescence: about 7.5 MiB PSS per concurrently +active read backend over that readiness point. One workload and one host do +not define the general marginal-backend budget. + +Mapping inspection explains why raw RSS remains much larger. A live diagnostic +with 14–15 guest processes counted roughly 650 MiB of RSS across aliases of +one approximately 142 MiB PostgreSQL shared-memory backing; proportional +accounting charged approximately one physical copy. The immutable initializer +images themselves shared effectively, with roughly 230 KiB private per guest +and about 5 MiB aggregate image PSS in that snapshot. The current resident +optimization targets are instead host allocator/arena retention, approximately +17 MiB of anonymous executable code, and dense per-instance runtime metadata +plus active guest-private tails. + +A declared embedded PostgreSQL profile (`io_method=sync`, +`shared_buffers=32MB`) removes three AIO-worker instances and materially lowers +PSS. The last retained budgeted run is the 256/224 MiB result above. Earlier +same-profile 192/176 and 160/144 MiB runs remain pressure evidence: the former +completed all four workloads at 66.515 MiB readiness and at most 131.709 MiB +fan-out PSS but recorded 4,650 high events; the latter recorded 11,804 high +events and only 13,703 update ops/s. Removing `memory.high` at a 160 MiB hard +limit did not create headroom: it completed with 10,168 `memory.max` events. +The profile still requires broader correctness, durability, and latency +qualification and is never a hidden substitute for fixing carrier overhead. + +The release-O3 PostgreSQL guest is also characterization only. At one client +and one million iterations, its WASIX/native bulk-throughput ratios were +0.754/0.801/0.779/0.829 for read/mixed/update/insert. A fresh four-client +one-million-iteration mixed-write run did not complete within 300 seconds; +sampling showed clients serialized on PostgreSQL's relation-extension lock. A +different contaminated sequence emitted PostgreSQL `IO in wrong state: 0` from +the AIO state machine. O3 is therefore unpromoted despite its single-client +gain. A balanced one-block 100k diagnostic also passed all throughput gates at +0.746/0.788/0.833/0.874, but failed the batch-residual gates for mixed-write, +update, and insert. Its overall classification remains diagnostic; the +residual is not per-query or launch latency. + +The embedded carrier roadmap is therefore: + +1. build the separately identified bounded owner-census diagnostic carrier and + reconcile its exact range/cardinality records with PID-birth-safe `smaps` + and cgroup evidence, without manufacturing a total across unlike accounting + systems; +2. define and qualify the supported embedded PostgreSQL profile, rerunning its + full correctness, bulk-throughput, memory-pressure, durability, and latency + matrix rather than treating GUC changes as runtime savings; +3. qualify the strict Linux runtime-relocated regular-disk `CodeMemory` bridge, + then replace it with immutable PIC executable text in a verified + cross-platform disk-backed mapping with only a small writable + GOT/relocation state; +4. compact immutable module templates and per-instance funcref/table/import + metadata, with counters proving which bytes scale with live backends; +5. enforce logical-process admission from a host resource budget and expose + worker, queue, arena, stack-pool, module, and dirty-page telemetry; +6. qualify the implemented reserved-address protocol and shared-object/futex + lifecycle under real PostgreSQL churn, then qualify the Unix private-image adapter and implement a + Windows placeholder adapter before any additional host claim; and +7. add release signing/provenance and feature negotiation, then exercise and + gate the true-libpq persistent/reconnect lane independently on every + advertised host. + +The detailed cache-lifecycle, relocated-code bridge, and owner-census contracts +are in +[embedded-memory-architecture.md](embedded-memory-architecture.md). + +These optimizations preserve fresh stores, private process state, exact +file-backed PostgreSQL shared memory, signals/timers, extension loading, and +error recovery. Sharing one mutable instance between backends, copied-memory +fork fallback, weakening WAL/fence/fsync semantics, unsafe path-only executable +caching, shrinking guards solely to improve VSZ, or using allocator trimming as +the primary fix are explicitly outside the architecture. The primary PostgreSQL +path is `vfork` to fresh `exec`; image-aware copied-fork transfer would optimize +a different path and is deferred. Shrinking the sparse 4 GiB bound/2 GiB guard +or 32 MiB stack reservation solely to improve RSS/VSZ is not an acceptable +shortcut: those mappings are mostly nonresident, and any change must prove +bounds-check, collision, deep-stack, and tail-latency effects. + +## Promotion gates + +This becomes a release product only after all of the following are automated: + +1. Every patch stack replays from exact source pins and produces sealed, + reproducible guest and host-carrier identities. +2. ABI/feature negotiation fails closed before side effects, and negative tests + prove mismatch, missing-feature, tamper, and stale-cache rejection. +3. Strict blocker probes pass on every claimed host carrier, including + allocator churn around fixed mappings, cross-instance message ordering, + latch/signal wake from blocked waits, one-shot/interval timers, and mapping + registry reuse/cleanup. +4. `initdb`, postmaster startup, concurrent libpq connections, clean shutdown, + crash recovery, extension loading, and a representative PostgreSQL + regression suite pass without PostgreSQL-side semantic fakes. +5. Repeated start/stop, multi-wave connection churn, and sustained indexed + updates show no hangs, orphaned logical children, repeated epoll warnings, + stale shared mappings, leaked wait registrations, or leaked handles. The + observed 4x100k update stall and the reproduced mixed-write + `Lock/extend`/retained-backend failure must remain covered by a deterministic + runtime-level reproducer and at least 100 clean repetitions per claimed + compiler/carrier. +6. Warm single-backend latency/throughput, fixed PSS, backend-launch peak, and + marginal clean/dirty backend memory have explicit budgets and pass against + the native oracle on identical hardware. Initializer-enabled carriers pass + the N=0,1,2,4,8,16 isolation and pressure ladder above. +7. Runtime patches are split by capability, minimized against pinned upstream, + and either upstreamed or carried with named ownership and focused tests. +8. Release artifacts, SDK resolution, notices, provenance, and support metadata + exist for every advertised platform. +9. AOT and initializer tamper, symlink/TOCTOU, stale-cache, wrong-target, + wrong-CPU, wrong-runtime-ABI, wrong-page-layout, and missing-artifact tests + prove the compiler-bearing and headless carrier policies fail closed. +10. Hard-kill/stale-shared-object recovery, DSM/futex registry churn, + upgrade/restart against existing `PGDATA`, and WAL power-loss semantics are + qualified on every advertised filesystem/platform. +11. Resolver negative tests prove browser/single-user packages cannot select a + postmaster carrier and postmaster packages cannot silently fall back to a + browser/single-user runtime. diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/docs/attested-start-fastpath.md b/src/runtimes/liboliphaunt/wasix-postmaster/docs/attested-start-fastpath.md new file mode 100644 index 00000000..3ac48232 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/docs/attested-start-fastpath.md @@ -0,0 +1,197 @@ +# Attested fresh-backend instantiation + +## Decision + +The implemented v5 source keeps ordinary WebAssembly instantiation and module +start for every EXEC_BACKEND. An +`oliphaunt.wasix-postmaster.memory-image.v2` receipt eliminates only repeated +post-start byte comparison, and only after both of these independent checks +succeed: + +1. the pinned carrier-build analyzer proves that the exact raw module's + transitive start + closure belongs to a restricted deterministic-effects policy; and +2. the first fresh instance in each executor activation runs ordinary start and + compares the entire mapped prefix with the immutable captured image. + +The proof uses schema +`oliphaunt.wasix-postmaster.deterministic-start-proof.v1`; the final manifest +binds its digest and the analyzer's exact output digest. The first comparison +result, success or failure, is stored in the image object's single-flight +`OnceLock`. Later fresh instances still run module start but reuse that exact +attestation result. A private, non-`Copy`, non-`Clone` witness is constructed +only after ordinary instance construction and is consumed by image application, +so callers cannot mint a successful-start claim. V1 images continue to compare +every instance. An attested image fails closed if the runtime supplies reused, +copied, or otherwise non-fresh memory. + +This preserves the product's defining architecture: the postmaster launches a +real, fresh EXEC_BACKEND PostgreSQL process. It is not a backend pool, store +snapshot, fork emulation, or guest-visible lifecycle shortcut. + +## Measured motivation + +The pre-attestation 256 MiB reconnect diagnostic attributed these medians per +fresh instance: + +| Component | Mean per instance | +| --- | ---: | +| `wasi_env.instantiate.linker.new` | 6.291 ms | +| `main_instance.new` | 2.539 ms | +| post-start compare/remap | 2.460 ms | +| data relocations | 0.775 ms | +| import construction | 0.258 ms | +| memory construction | 0.121 ms | +| symbol resolution | 0.019 ms | + +That historical implementation read and compared 2,621,440 bytes for every +postgres backend. Removing the repeated comparison projected about 2.46 ms from +each warm backend launch while retaining ordinary start. The number is a +pre-attestation diagnostic estimate, not a v5 measurement or qualification +result. + +## Exact PostgreSQL module evidence + +The following values are bound to the historical analyzed carrier, not stable +product constants. For module +`277e02c8263ca93ce5db23149540c46edcf27c73e48e4a145c762e2b8750b958`: + +- the module start is function 147, exported as `__wasm_init_memory`; +- its only call is local function 148, + `__wasm_apply_global_tls_relocs`; +- it calls no imported function and performs no table operation; +- it initializes three passive data segments totaling 2,415,803 bytes, + performs one zero fill, and uses LLVM's one-time shared-memory atomic guard; +- it writes local globals 5, 172, 689, and 690 from the receipt-bound + `env.__memory_base`; and +- it drops passive data segments 1 and 2 while retaining TLS segment 0. + +The proof digest for the analyzed start closure is +`3558b5cd25769fb8b5e3aa07be691f47d2af047f15dcbdf4ed402b8991ac9a29`. +The analyzer also accepts the exact initdb module under the same policy, with +proof digest +`c382782eaac57cbc24a0d9e9358798419492e3bd603878f1bf0f556123a9b914`. + +The analyzer uses `wasmparser`, validates the whole module first, and rejects +unknown operators. Its accepted closure is deliberately narrow: integer-only +constant/local/global relocation arithmetic, direct local calls, the LLVM +atomic initialization guard, passive `memory.init`, one `memory.fill`, and +the expected data drops. Imported calls, table operations, memory loads, +indirect calls, memory growth, floating point, SIMD, relaxed SIMD, reference +effects, exceptions, and an unknown control-flow shape all fail the proof. + +The proof digest is SHA-256 over the policy identifier, a delimiter, the raw +module digest in lowercase ASCII, then each sorted transitive function index, +body length, and exact raw body bytes. The carrier manifest already binds the +receipt and raw module digests; the runtime additionally checks the embedded +module hash, start index, start export, runtime ABI, and memory layout. + +## Implemented build and activation protocol + +The carrier builder performs the following transaction before final +manifest publication: + +1. validate and analyze the exact raw executable; +2. capture two independent ordinary-start images, as today; +3. require byte-identical images and receipts; +4. emit memory-image schema + `oliphaunt.wasix-postmaster.memory-image.v2`, embedding the analyzer proof; +5. validate the proof's module digest against the executable and AOT module + hash; +6. bind the v2 receipt fields and image digest into the final manifest; and +7. exercise initdb plus real EXEC_BACKEND launches from the immutable carrier. + +At activation the loader validates all v2 proof constants before it opens the +fast path. The first fresh instance performs the ordinary full comparison. +`OnceLock>>` gives concurrent backend launches one +linearizable validation. A mismatch, I/O error, non-fresh memory, malformed +proof, module/start mismatch, or layout mismatch remains an error and is never +downgraded to v1 behavior silently. + +The runtime appends one terminal +`oliphaunt.wasix-postmaster.attested-start-runtime-summary.v1` JSONL record for +each loaded `(native executor pid, module SHA-256)` activation. It does so only +after the root status, every execution lease, every pending child publication, +and every process published in the same control-plane epoch are quiescent. The +barrier joins the snapshot and then rechecks the epoch exclusively, so a late +descendant cannot be omitted merely because the root status was already +published. Evidence finalization remains inside the originally admitted +`run_wasm` future. + +`oliphaunt.wasix-postmaster.sealed-loader-audit-validation.v3` binds each +terminal summary to the v2 memory metadata in the final manifest and requires, +for every activation: + +```text +ordinary_start_completed_instances = fresh_zeroed_instances + nonfresh_instances +validation_attempts = ordinary_start_completed_instances +full_compare_attempts = full_compare_successes + full_compare_failures = 1 +validation_attempts = full_compare_attempts + reuse_successes + reuse_failures +reuse_successes = ordinary_start_completed_instances - 1 +compared_bytes = mapped_size * full_compare_successes +skipped_bytes = mapped_size * reuse_successes +remap_successes + remap_failures = full_compare_successes + reuse_successes +remap_successes = ordinary_start_completed_instances +``` + +It additionally requires `nonfresh_instances`, `full_compare_failures`, +`reuse_failures`, `remap_failures`, and `counter_overflow` to be zero. There +must be exactly one summary after, and no orphan summary without, its matching +preinitialized-memory loader row. The loader mapping rows remain +`oliphaunt.wasix-postmaster.sealed-loader-receipt.v2`; summary v1 and mapping v2 +coexist deliberately in the same bounded append-only JSONL stream. + +## Why ordinary start is not skipped + +The current image is only the 64 KiB-aligned prefix below `stack_low`. For +postgres it ends at 2,621,440, while `stack_low` is 2,665,472. The LLVM atomic +initialization guard is in that un-mapped final partial page. Memory bytes are +also not the complete Wasm-visible post-start state: start mutates globals and +passive-data drop state. Blindly mapping the image and omitting start would +therefore be observably wrong even for this exact module. + +A future direct post-start instance template is a separate optimization. It is +not eligible for integration until its versioned template captures and restores +all of the following: + +- the complete initialized memory state, including the final partial page and + the atomic guard, without overwriting live stack bytes; +- every local and imported mutable global affected by initialization, including + TLS bases and relocation globals; +- local/imported table state and active/passive element state; +- passive data drop state; +- successful-start/trap outcome and the absence (or exact replay) of imported + side effects; +- memory/table growth and reference/exn state; and +- the same fresh-memory, module, runtime ABI, feature, and linker-layout + identity used by capture. + +That design also needs a standard-vs-template differential harness comparing +memory, globals, tables, dropped segments, exports, TLS, traps, and subsequent +PostgreSQL behavior over concurrent EXEC_BACKEND launches. Until then, the v2 +comparison cache is the safe optimization boundary. + +## Qualification gates + +Before promotion, build a new immutable carrier and require: + +- focused proof-schema, fail-closed, single-flight success/failure, and reused + memory tests; +- analyzer negative fixtures for imported calls, memory loads, indirect calls, + table effects, unknown atomics, changed drop state, and call-graph cycles; +- two-capture identity plus first-instance forced mismatch tests; +- initdb bootstrap and real postmaster/EXEC_BACKEND lifecycle tests; +- 256 MiB reconnect p95/p99 and single-backend paired throughput; +- 256 MiB full-map readiness/fanout RSS/PSS/anonymous memory; +- 1 GiB throughput and latency regression guards; and +- audit evidence that exactly one mapped-prefix comparison occurred per + executor/module activation while ordinary start occurred once per fresh + instance, all equations above conserve exactly, every expected `(pid,module)` + has one terminal summary, and nonfresh/failure/overflow counts are zero; +- proof that terminal summaries are emitted only after complete process-tree + execution and child-publication quiescence; and +- for an acting adaptive-cache qualification, separate + `file-cache.adaptive-linux.v5` terminal evidence with zero sampler, clock, + advice, pin, and revoke errors plus zero live descriptor capabilities and + exact entry/byte conservation. Those cache counters are not fields in the + attested-start summary and must not be inferred from it. diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/docs/checkpoint-recycle-qualification.md b/src/runtimes/liboliphaunt/wasix-postmaster/docs/checkpoint-recycle-qualification.md new file mode 100644 index 00000000..e19de416 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/docs/checkpoint-recycle-qualification.md @@ -0,0 +1,158 @@ +# Checkpoint, WAL recycle, and clean postmaster recycle qualification + +This lane asks whether the fresh-backend WASIX postmaster remains viable when +durable write traffic, periodic checkpoints, WAL retention, and a clean +postmaster recycle are included. It does not replace the single-backend +architecture and it does not promote a product. Every result is explicitly +`candidate-unqualified-*-non-release`. + +## Why this is a separate lane + +The steady query benchmarks showed that process RSS alone is the wrong control +surface for an embedded database. Under write load, the cgroup also charges +PostgreSQL data and WAL page cache, dirty/writeback pages, page tables, and +kernel memory. In the observed high-water samples, charged file memory was a +larger pressure source than anonymous runtime memory. Reducing Wasmer stack +reservation or retaining a warm worker cannot bound that file-backed component. + +The qualification therefore controls and reports two different quantities: + +- summed process PSS, PSS-anon, private pages, and page tables; and +- scope-wide `memory.current`, `memory.peak`, swap, selected `memory.stat`, + `memory.events`, and memory PSI counters. + +The standalone WASIX server runs in a fresh cgroup-v2 scope with +`MemoryMax=256M`, `MemoryHigh=224M`, and `MemorySwapMax=0`. A second fresh scope +is created after clean shutdown. The epoch receipt binds each sample stream to +its distinct cgroup path, filesystem identity, creation timestamp, and limits. +Cumulative counters are counted from the fresh-scope zero boundary, not from +the first process sample, so startup pressure is not subtracted away. Rates use +the sum of observed epoch durations, excluding the process-free recycle gap; +the second startup boundary is also gated independently. + +## Workload semantics + +The probe is a compiled libpq client linked to the exact native-oracle libpq. +It opens four persistent connections and offers 15 transactions per second per +client using absolute `CLOCK_MONOTONIC` deadlines. Each transaction performs +exactly 48 updates, 16 inserts, and 8 reads in one PostgreSQL transaction. The +updated and inserted payloads are 512 bytes so the lane exercises WAL and +writeback instead of measuring a read-mostly toy workload. + +The evidence validator reconstructs the exact per-client schedule from the +configured interval and stagger. It verifies every scheduled timestamp, +deadline lateness, sequence, result shape, realtime/monotonic duration, and +flush receipt. Late offers are skipped by the probe rather than caught up in a +burst, and any skip fails qualification. Achieved TPS is calculated over the +observed monotonic window rather than the configured duration. + +The checkpoint policy is intentionally explicit: + +- `checkpoint_timeout=30s` and `checkpoint_completion_target=0.9`; +- `max_wal_size=256MB` and `min_wal_size=64MB`; +- WAL recycling enabled, archiving and WAL retention consumers disabled; +- checkpoint logging plus WAL and I/O timing enabled; and +- the existing embedded runtime and durability profiles remain in force. + +The paired lane starts a new cluster and postmaster for every position in an +ABBA/BAAB block. Adjacent positions form native/WASIX pairs, which bounds drift +without sharing a warm runtime. PostgreSQL settings are captured and hashed; +each pair must have the same command-line settings. Diagnostic and promotional +modes gate both absolute WASIX latency and paired native/WASIX ratios. Smoke +mode still gates structure, durability, offered-load completion, WAL volume, +and checkpoint overlap, but only observes the latency thresholds. + +## WAL and recycle proof + +The standalone lane adds an explicit high-volume transaction after the first +steady epoch. It requires a logged WAL-triggered checkpoint, then takes six +host-side snapshots of regular 24-hex WAL segment files: + +1. `before-steady` +2. `after-steady` +3. `after-volume` +4. `plateau-1` +5. `plateau-2` +6. `plateau-3` + +The snapshot validator requires that exact order, ordinal reset and contiguous +ordering inside each snapshot, unique names and filesystem identities within a +snapshot, a logged positive recycle count, and the same `(device, inode)` under +a different segment name in two adjacent snapshots. Thus a duplicate row cannot be +misreported as recycling. The last three snapshots must form a bounded byte +and file-count plateau, and the latest `pg_wal` footprint must remain at or +below 512 MiB. `max_wal_size` is a checkpoint trigger, not a strict disk cap; +the separate observed plateau is the relevant embedded-footprint evidence. + +## Clean postmaster recycle proof + +The first WASIX postmaster receives PostgreSQL's smart-shutdown signal. Forced +termination is not an allowed success path. Qualification waits for the owned +process group, cgroup, TCP port, and private `/dev/shm` directory to become +empty and requires PostgreSQL's clean-shutdown markers. + +The same PGDATA is then opened by a new Wasmer/postmaster process in a new +cgroup scope. The new birth identity and cgroup identity must differ. Startup +must report a previously shut-down database and must not report crash recovery +or redo. Logical row counts, numeric aggregates, and two independent 64-bit +content aggregates are captured before shutdown and compared byte-for-byte +immediately after restart. Only then does the second fixed-offer epoch run. + +Both the native controls and WASIX clusters are initialized with PostgreSQL +data checksums. Each running postmaster must independently report +`data_checksums=on`. After the second smart shutdown, the exact pinned native +PostgreSQL 18 `pg_checksums` binary performs an offline `--check` over the +WASIX-produced PGDATA; its binary identity, exit status, complete log, and log +hash are retained. The logical-state comparison and physical page-checksum +scan are separate gates, so agreement at the SQL layer cannot conceal page +corruption. + +This preserves the core idea: every backend and every recycled postmaster is a +fresh execution context. No warm-worker reuse, hidden connection pool, relaxed +durability, dropped transaction work, or catch-up batching is used to obtain +the numbers. + +## Running the lane + +First inspect the bounded plan without requiring built artifacts: + +```bash +src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-checkpoint-recycle.sh \ + --mode smoke --print-plan +``` + +Then supply an already verified compiler-free sealed carrier: + +```bash +src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-checkpoint-recycle.sh \ + --sealed-carrier /absolute/path/to/sealed-carrier \ + --mode smoke \ + --label checkpoint-smoke +``` + +Smoke uses one four-position block and 40-second epochs. Diagnostic uses three +blocks and 240-second epochs. Promotional uses at least ten blocks, but remains +non-release evidence until its candidate thresholds are reviewed and promoted +through the repository's normal policy process. Raw transaction, checkpoint, +full `pg_stat_io`/WAL/checkpointer, memory, WAL snapshot, shutdown, identity, +and gate receipts remain alongside the summary. + +## Reading a failure + +- A high PSS-anon result points toward runtime heap, compiled-code metadata, or + guest-private allocation work. +- A high cgroup result with bounded PSS but high file/dirty/writeback values + points toward PostgreSQL buffer/WAL pacing and host page-cache reclaim. +- `memory.events high` or PSI failure with no OOM means the limit is inducing + reclaim stalls before correctness fails. +- Checkpoint-overlap tail failure with acceptable non-overlap latency points to + writeback scheduling, not general query dispatch. +- WAL plateau or inode-reuse failure means retention/recycling has not been + proven even if the final directory happens to be small. +- A clean-recycle failure must be treated as lifecycle or durability work; it + cannot be papered over by deleting PGDATA or accepting crash recovery. + +These distinctions keep optimization tied to the measured root cause. They +also leave room for robust follow-on work—denser runtime metadata, reclaim-aware +checkpoint admission, or a bounded host-backed storage cache—without changing +PostgreSQL transaction semantics or fresh-backend isolation. diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/docs/cold-ownership-qualification.md b/src/runtimes/liboliphaunt/wasix-postmaster/docs/cold-ownership-qualification.md new file mode 100644 index 00000000..10164861 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/docs/cold-ownership-qualification.md @@ -0,0 +1,105 @@ +# Cold page-cache ownership qualification + +## What this lane closes + +Running the sealed-carrier verifier and `initdb` before a measured postmaster +is correct for integrity, but it warms carrier and PGDATA pages in the caller's +cgroup. A later server-only cgroup can then fault already-resident pages without +owning the storage I/O or the original page-cache charge. Such a run is useful +warm-state evidence, but it is not cold embedded memory evidence. + +The cold-ownership lane keeps full verification and initdb outside the timed +server, then establishes a new, explicit boundary: + +1. Enumerate every directory and regular path below the immutable carrier and + the newly initialized PGDATA. Symlinks and special entries fail closed. +2. Open with `O_NOFOLLOW`, content-hash every unique inode, recheck exact file + identity/size/timestamps, and `fdatasync` each file. +3. Record the resident-page count with `mincore(2)`, apply + `POSIX_FADV_DONTNEED` to each exact file, and run `mincore(2)` again. +4. Accept the boundary only when every unique regular-file page is nonresident. + The JSON receipt binds the two root content/metadata digests, exact + path/file/page/byte counts, host page size, UTC and monotonic timestamps, + tool identity, and the benchmark execution/carrier identities. +5. Perform no carrier or PGDATA verification, hash, stat, or content read after + proof completion. The benchmark takes one monotonic timestamp and launches + the process group; `systemd-run --user --scope` places the postmaster tree in + its fresh cgroup-v2 scope. + +This is targeted eviction, not a machine-wide benchmark trick. The lane never +writes `/proc/sys/vm/drop_caches`, never invokes allocator purges, and does not +disturb unrelated page cache. `mincore` is the authority: advisory eviction +without the zero-page observation is rejected. + +## Evidence contract + +`--cold-ownership` is a startup-only WASIX lane. It requires Linux, a fully +verified ext-family immutable sealed carrier and its exact external deployment +receipt, one WASIX target, full smaps/resource +sampling, and explicit `MemoryMax`, `MemoryHigh`, and `MemorySwapMax` controls. +It always enables the zero-write loader policy and requires every AOT and +executable memory-image row to report `direct-immutable-inode`; a reflink, +streamed copy, or generic read-only-filesystem row fails this candidate lane. +Loader mapping rows use +`oliphaunt.wasix-postmaster.sealed-loader-receipt.v2`. After complete +process-tree execution and child-publication quiescence, each loaded +`(pid,module)` must also have exactly one terminal +`oliphaunt.wasix-postmaster.attested-start-runtime-summary.v1` row. The lane +accepts only +`oliphaunt.wasix-postmaster.sealed-loader-audit-validation.v3`, which binds both +row kinds to the final format-6 manifest and proves one full comparison, +`starts - 1` successful reuses, one successful remap per start, exact +mapped-size byte accounting, and zero nonfresh/failure/overflow counts. +The per-run validator requires: + +- exactly two disjoint roots (`carrier` and `pgdata`) and no inode shared + across them; +- carrier mode invariants (`0555` directories; `0444`/`0555` files); +- zero resident pages at the proof boundary and a boundary-to-spawn gap no + greater than one second; +- a successful real libpq `SELECT 1`, with spawn-to-query completion measured + by `CLOCK_MONOTONIC`; +- first-query and pre-stop snapshots from the same exact cgroup identity; +- exact cgroup limits, positive scope-owned file charge, and no swap beyond + the declared limit; +- an explicit I/O-observation contract: when `io.stat` is present it is parsed + strictly and positive attributed reads plus non-regressing counters are + required; when the delegated cgroup lacks the I/O controller or `io.stat`, + status/reason are bound as unavailable and all I/O totals remain null/blank; + that run makes no storage-I/O first-touch claim; +- at least one race-free full process-tree smaps plus cgroup sample; +- scope-lifetime `memory.peak` and `swap.peak`, instantaneous + `memory.stat` file-dirty/writeback peaks, pressure totals, and, only when the + controller is observable, `io.stat` read/write byte and operation counts. + +The first query is the readiness probe itself. Native `psql` runs outside the +server scope, so client memory is not charged to the embedded server budget. +PGDATA is fresh per block. The repeated qualifier rejects reused PGDATA, +carrier/execution identity drift, changed limits, a missing sample, or any +failed per-run validation, then reports nearest-rank p50/p95 startup latency. + +## Campaign + +After building the final sealed carrier and native client install, run: + +```sh +bash src/runtimes/liboliphaunt/wasix-postmaster/bin/qualify-wasix-cold-ownership.sh \ + --sealed-carrier target/oliphaunt-wasix-postmaster/carriers/ \ + --immutable-carrier-receipt /var/lib/oliphaunt/wasix-postmaster/.json \ + --blocks 10 \ + --runtime-footprint embedded-concurrent \ + --durability safe \ + --memory-max 256M \ + --memory-high 224M \ + --swap-max 0 \ + --resource-interval 0.05 \ + --label cold-ownership-c4-final-v1 +``` + +Add `--max-p95-ms ` only when a performance ceiling has been declared +before measurement. Without it, latency is report-only; all ownership, +integrity, isolation, and memory-resource gates remain mandatory. I/O gates +remain mandatory when `io.stat` is available; an unavailable delegated I/O +controller is reported rather than converted to zero or an I/O first-touch +claim. The output is candidate, non-release Linux evidence and makes no WASIX +claim for platforms without cgroup v2, `mincore`, and POSIX file advice. diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/docs/concurrent-root-cause.md b/src/runtimes/liboliphaunt/wasix-postmaster/docs/concurrent-root-cause.md new file mode 100644 index 00000000..ffc7eb26 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/docs/concurrent-root-cause.md @@ -0,0 +1,309 @@ +# WASIX PostgreSQL concurrent root-cause notes + +> Historical measurement log imported from commit +> `aa4211f4485327cb7493863ea3380acf27c5f951`. It is evidence from the original +> macOS arm64 run, not a current cross-platform support claim. + +Generated during the 2026-05-12 20-connection investigation. + +2026-05-15 follow-up: the current PG18 `release-o3` exec-backend server path is +functional but still not native-parity. In `codex-perfgated-releaseo3-20x1000`, +WASIX reached `0.599-0.646x` native throughput across the four 20-client +workloads and kept fanout RSS around `293-307 MiB`. Linker subspans in +`codex-perfstats-linker-subspan-read-20x1000` show the short-fanout cost is +mostly per-backend dynamic-main `Instance::new`, not fd read/write or table +setup. + +A copied-fork child-backend experiment was added behind +`WASIX_CORE_CHILD_BACKEND=copied-fork`. It builds and passes single-process +initdb, but it fails server readiness with `RuntimeError: unreachable`. The +parseable perf-stats diagnostic +`codex-copiedfork-readiness-diag-atomicperf` shows it also copies about +`160 MiB` per forked child and spends roughly `56 ms` per copied child in +`linker.instance_group.memory.copy_to_store`. That path is therefore not a +promotable replacement for `EXEC_BACKEND` until the runtime has no-copy/COW +instance-group memory and the startup trap is fixed. + +## Current Linux sealed-carrier follow-up + +### Release-O3 final-module ordering regression + +The current release-O3 investigation found a stronger root cause than +relation-extension throughput. PostgreSQL's ThinLTO input still contains the +expected sequentially consistent fences in `SetLatch`, `ResetLatch`, and +`WaitEventSetWait`, but the final release-O3 `postgres` module contains no +`atomic.fence` operators. The exact safe-O2 control module contains 275. A +standalone Binaryen 130 pass over the safe module preserves those operators, +and Wasmer's LLVM lowering emits a sequentially consistent fence when the +operator reaches it. The loss therefore occurs in the PostgreSQL optimized +guest pipeline before Wasmer AOT compilation; Wasmer cannot restore an +operator absent from the input module. + +This explains the diagnostic in which the relation-extension lock holder had +`granted=true`, no blockers, and remained asleep while three peers queued. The +lock manager had completed the grant, but the separately instantiated backend +missed its latch wake. + +Patch `0008-wasix-packed-atomic-latch-state.patch` makes that critical +predicate independently robust under the explicit +`PG_WASIX_ATOMIC_LATCH_STATE` feature macro: + +- one naturally aligned, compile-time lock-free `pg_atomic_uint32` carries + `SET=1` and `SLEEPING=2`; a reserved word plus static size and field-offset + checks preserve the existing WASIX `Latch` layout; +- `SetLatch` uses one sequentially consistent fetch-or, and only the caller + that changes SET from clear to set sends a wake when its old value also + contained SLEEPING; +- the waiter publishes SLEEPING with fetch-or, rechecks SET with a true + sequentially consistent atomic load, and retracts SLEEPING both when SET was + already visible and after the readiness primitive returns; +- `ResetLatch` clears SET with fetch-and without manufacturing or clearing a + waiter publication; and +- existing `pg_memory_barrier()` calls remain in place as a separately + verifiable secondary contract. Native PostgreSQL retains the exact upstream + field layout and source path under the compile-time `#else` branches. + +This is a source-level repair awaiting a rebuilt sealed carrier. It makes no +liveness or performance claim until the final-module verifier proves the +required atomic state operations and fences, the cross-instance latch probe +passes, and repeated fresh-postmaster backend waves complete. + +The final 2026-08 Linux x86_64 carrier changes the memory interpretation and +exposes a separate sustained-write limit: + +- Runtime ABI `995f6a9bf69ce6ff154533369eb4f9d6c45d9dfca13fdc213e0f6be8ae405217` + with payload inventory + `8e907e600fa9d7197c2ae98ddece5cb3093e4e7e3caf8f27f325a24955c120a7` + and manifest + `cea8c0933fa01f6646184c1f97c2156300e50bcf8a5d1d2e38fbb4ed2bb11fec` + passed the predeclared `embedded-c4-lower-pressure-v1` memory gate on one + host. Readiness PSS was 66.862 MiB; the four-client peak was 132.708 MiB PSS, + 77.473 MiB anonymous PSS, and 1.164 MiB page tables. Whole-run cgroup charge + peaked at 224.707 MiB under 256/224/0 MiB hard/high/swap limits. +- Required fan-out phases recorded 2,528 `memory.high` events and 0.003289 PSI + some/full stall fractions, within the predeclared 4,096, 0.015, and 0.010 + ceilings, with no `memory.max`, OOM, OOM-kill, or swap event. Bulk rates were + 352,846.832/359,874.044/42,854.082/140,449.438 logical row ops/s for + read/mixed-write/update/insert. The profile remains candidate/unqualified + until its independent performance, latency, lifecycle, and durability lanes + compose. +- That run's verifier and initdb operated outside the server cgroup, so it is + warm active-scope evidence, not a cold page-cache ownership proof. Cold + qualification requires zero carrier/PGDATA regular-file residency at the + final prelaunch boundary and attributed first-query cgroup I/O; see + [cold-ownership-qualification.md](cold-ownership-qualification.md). +- Earlier exact five-module carrier runs reached 79.101 MiB quiescent readiness + PSS in the staged ladder. Four-client indexed read reached 108.274 MiB PSS; + default-profile writes peaked near 220 MiB. These remain historical + optimization and attribution points, not current-carrier values. +- Raw RSS is strongly inflated by aliases inside the one host process. A live + 14–15-guest diagnostic attributed roughly 650 MiB RSS to repeated mappings + of one approximately 142 MiB PostgreSQL shared-memory backing. PSS and cgroup + charge, not raw RSS, are the physical viability gates. +- The optimized O3 guest reached 0.754/0.801/0.779/0.829 of native one-client + bulk throughput for read/mixed/update/insert. These are batch wall-time + ratios, not query-tail or backend-launch latency. A balanced one-block 100k + diagnostic passed its throughput gates at 0.746/0.788/0.833/0.874, but failed + batch-residual gates for mixed-write, update, and insert; it remains + diagnostic. +- A fresh O3 four-client, one-million-iteration mixed-write run still did not + complete after 300 seconds. Sampling showed all clients serialized on + PostgreSQL relation extension while the holder progressed extremely slowly. + Switching to synchronous I/O removed three AIO-worker guests and reduced + memory, but did not remove the relation-extension throughput limit. +- The earlier synchronous-I/O/32 MiB profile completed all four 100k + workloads at a 192/176 MiB hard/high limit with 66.515 MiB readiness and at + most 131.709 MiB fan-out PSS. Its 4,650 high events and 34,344 update ops/s + show page-cache/reclaim pressure. At 160/144 MiB it completed but update fell + to 13,703 ops/s amid 11,804 high events; 160 MiB is survival-only. +- A separate multi-workload run was contaminated by timed-out clients and + logged PostgreSQL `IO in wrong state: 0` from the PostgreSQL 18 AIO state + machine. Its later workload rates are invalid; the AIO failure and process + cleanup need independent reproduction. + +This does not invalidate the earlier lost-wake repair or the final carrier's +budget pass. It does mean that sustained table extension/HostFS/I/O scalability +and AIO lifecycle remain write-path blockers outside the qualified workload +shape. O3 is unpromoted. + +### Lifecycle baseline drift: PID 6 WAL-segment VFD + +The verbose lifecycle diagnostic also corrected a false root-cause attribution. +PID 7 already had its `global/1262` descriptor in the first inventory and kept +the same 12 local FDs. The persistent delta occurs in PID 6, PostgreSQL's WAL +writer: it had 11 local FDs through 15.008 seconds, then opened +`pg_wal/000000010000000000000001`, after which both its local count and the +aggregate guest-FD count remained one higher (12 and 71 respectively). + +This is expected PostgreSQL 18 lazy state rather than a reconnect leak. The +background writer's first approximately 15-second maintenance interval calls +`LogStandbySnapshot()`, which inserts an `XLOG_RUNNING_XACTS` record and sets an +asynchronous WAL LSN. That wakes the WAL writer; `XLogBackgroundFlush()` opens +the current segment on first write, and the cached `openLogFile` VFD remains +open while the segment remains current. + +The robust lifecycle boundary is therefore event-driven. Before readiness, the +harness records the `pg_stat_io` row for +`backend_type='walwriter'`, `object='wal'`, `context='normal'`, calls +`pg_log_standby_snapshot()`, and records its returned target LSN. It polls with +fresh statistics snapshots until `writes` and `write_bytes` increase and +`pg_current_wal_flush_lsn()` reaches the target, while requiring an unchanged +`stats_reset`. This produces one natural running-transactions record and +advances the exact lazy state without a 15-second sleep, artificial bulk WAL, +or a path/FD exception. It requires `wal_level >= replica` and a primary. + +After that barrier, stable idle contains six active task leases plus five +suspended parent-continuation leases: `execution_leases=11`, alongside six +registered processes/runtime states and five child edges. Treating the lease +count as six conflates tasks with the parent continuations retained across the +EXEC_BACKEND `vfork`/`exec` topology. + +## Instrumentation controls + +WASIX perf counters and lifecycle snapshots have deliberately separate gates: + +- Build with `wasmer-wasix/perf-stats` or the CLI passthrough feature. +- Enable at runtime with `WASIX_PERF_STATS=1`. +- Write counters to a file with `WASIX_PERF_STATS_FILE=/path/to/log`. +- Dump live wait state while a futex or epoll wait remains parked with the + runtime-owned `WASIX_WAIT_DUMP_INTERVAL_MS=N` and + `WASIX_WAIT_DUMP_FILE=/path/to/log` controls. The benchmark exposes the + interval only through `--wasix-wait-dump-interval-ms` in its untimed + `--wasix-lifecycle-plateau` lane. Timed workloads and latency qualifiers + reject wait-dump options and ambient variables, then remove all legacy and + current wait-dump keys from the Wasmer child environment. +- Compact ordered lifecycle records are available in `headless-minimal` + without `perf-stats`; they contain aggregate process/task ownership, + process-topology/thread/publication/retirement state, runtime-state observer + ownership, private/shared futex waiters and wakers, epoll internals, + shared-registry slots, mappings, and guest-FD occupancy. +- The lifecycle harness publishes atomic nonce-bound fence requests. The + runtime takes the referenced snapshot under its global writer lock, appends + and syncs the visible fence, then atomically publishes a separate committed + ACK with the exact fence-end offset. The harness removes stale ACKs before + each request, freezes only that offset, and binds ACK/frozen hashes into a + receipt verified again by the parser. The live diagnostic log is never + parsed as qualification evidence. +- The baseline policy is hashed before the server starts and bound to resolved + profile/runtime/module/carrier identities. It currently remains an + `exploratory-unbounded` + `relative-to-fresh-baseline` gate: tuple equality proves no reconnect + accumulation, while absolute PSS budgets—not a nonzero readiness tuple—judge + whether the baseline is viable for embedding. +- Detailed per-entry traversal additionally requires a `perf-stats` build and + explicit `WASIX_WAIT_DUMP_VERBOSE=1`. +- Without `perf-stats`, counter operations compile to inline no-ops. With the + feature but without `WASIX_PERF_STATS=1`, counters do not take locks or + timestamps. + +Use the separate helper so production release builds are not changed: + +```sh +WASIX_CORE_PROFILE=release-o3 \ + ./assets/wasix-build/experiments/fresh-wasix-postgres/bin/build-wasmer-perf-stats.sh +``` + +Use the separate binary only for untimed diagnostics. For the compact +lifecycle gate, run: + +```sh +WASIX_CORE_PROFILE=release-o3 \ +WASMER_BIN=/assets/wasix-build/work/upstream/wasmer/target/perf-stats/release/wasmer \ + ./assets/wasix-build/experiments/fresh-wasix-postgres/bin/bench-wasix-concurrent-query-suite.sh \ + --target wasix --skip-build --skip-precompile \ + --resource-detail off --wasix-lifecycle-plateau \ + --wasix-lifecycle-reconnects 64 \ + --wasix-wait-dump-interval-ms 100 +``` + +## Fixed paths + +- `LocalTcpListener` now preserves level-triggered accept readiness while the local accept backlog remains non-empty. This fixes the earlier concurrent connection stall at the TCP listener level. +- Guest signal interruption now distinguishes host-only wakeups from guest-delivered signals, so host `Sigwakeup` no longer leaks to PostgreSQL as `EINTR`. +- The repeated `failed to epoll during deep sleep - intr` log is gone in the 20x1000 concurrent suite. +- `futex_wake` now prefers a waiter with a registered host waker before consuming an unregistered waiter. Focused unit coverage was added for that wake selection. +- The socket readiness registry now supports an external epoll handler and direct socket waiters at the same time. The virtual-io selector wraps them in a multiplexed handler instead of replacing one with the other. +- Blocking socket send/recv/accept waits now use the source readiness polling path instead of installing/removing a one-off socket handler. This prevents a direct socket wait from detaching an active epoll subscription. + +## Key measurements + +20x1000 release WASIX smoke after the futex wake change: + +- `indexed-read`: 16,975 ops/s, 20/20 clients, no timeout. +- `mixed-write`: 23,529 ops/s, 20/20 clients, no timeout. +- `indexed-update`: 13,899 ops/s, 20/20 clients, no timeout. +- `indexed-insert`: 13,908 ops/s, 20/20 clients, no timeout. +- Peak WASIX RSS during fanout: 315-328 MiB. + +Useful reports: + +- `reports/release-o3/concurrent-query-suite/codex-futexfix-smoke-20x1000-wasix/summary.tsv` +- `reports/release-o3/concurrent-query-suite/codex-futexfix-smoke-20x1000-wasix/resource-summary.tsv` + +Long hot read shape: + +- Native 20x100000 indexed read: 433,839 ops/s, peak summed RSS 357 MiB. +- WASIX release 20x100000 indexed read: 1,189,832 ops/s, peak RSS 672 MiB. +- WASIX perf/profiler 20x100000 indexed read: 143,071 ops/s, peak RSS 676 MiB. +- In the active profiler sample, excluding idle parked threads, visible top-stack samples were about 95% guest JIT code and under 4% host kernel/malloc/memory operations. + +Useful reports: + +- `reports/release-o3/concurrent-query-suite/codex-native-rootcause-read-20x100000/summary.tsv` +- `reports/release-o3/concurrent-query-suite/codex-wasix-release-read-20x100000/summary.tsv` +- `reports/release-o3/concurrent-query-suite/codex-wasix-rootcause-read-20x100000/wasix/indexed-read/symbolized-sample.txt` + +Backend creation and mapping costs from the 20x1000 profiled WASIX run: + +- Each 20-client fanout creates 20 backend stores. +- `task_wasm.new_with_store.plain`: about 0.7-0.9s cumulative per 20-client fanout. +- `syscall.proc_exec3`: about 1.6-2.1s cumulative per 20-client fanout. +- `syscall.mem_mmap.bytes`: 60 mappings and about 3.0 GB requested mapping bytes per 20-client fanout. + +Useful reports: + +- `reports/release-o3/concurrent-query-suite/codex-wasix-rootcause-20x1000/wasix/wasix-perf-server.top-time.tsv` +- `reports/release-o3/concurrent-query-suite/codex-wasix-rootcause-20x1000/wasix/wasix-perf-server.top-bytes.tsv` + +## Liveness result + +The long 20x10000 WASIX release sequence that previously parked now completes all four workloads with 20 clients and no epoll interrupts: + +- `indexed-read`: 136,901 ops/s, 20/20 clients, no timeout. +- `mixed-write`: 276,817 ops/s, 20/20 clients, no timeout. +- `indexed-update`: 110,497 ops/s, 20/20 clients, no timeout. +- `indexed-insert`: 132,013 ops/s, 20/20 clients, no timeout. + +Current native comparator for the same 20x10000 shape: + +- `indexed-read`: 634,006 ops/s. +- `mixed-write`: 412,797 ops/s. +- `indexed-update`: 151,976 ops/s. +- `indexed-insert`: 198,610 ops/s. + +Useful reports: + +- `reports/concurrent-query-suite/codex-wait-registry-fix-release-long-20x10000-wasix/summary.tsv` +- `reports/concurrent-query-suite/codex-wait-registry-fix-release-long-20x10000-wasix/resource-summary.tsv` +- `reports/concurrent-query-suite/codex-wait-registry-fix-native-long-20x10000/summary.tsv` +- `reports/concurrent-query-suite/codex-wait-registry-fix-native-long-20x10000/resource-summary.tsv` + +The historical detailed wait-registry dump was verified with +`codex-wait-registry-snapshot-smoke`; verbose snapshots include private/shared +futex counts, shared memory mappings, every FD kind, epoll queue length, and +per-subscription pending/enqueued/join state. The current lifecycle gate uses +the bounded compact record instead and verifies an exact readiness-to- +post-reconnect plateau without walking entries or sweeping weak slots first. + +## Root-cause assessment + +The constrained paths are now split: + +- Short concurrent fanout batch wall time is materially constrained by WASIX + backend creation: `proc_exec3`, store creation, and repeated guest memory + mapping. The historical harness did not isolate a backend-launch latency + distribution. +- Hot read execution is not primarily constrained by socket syscalls; active CPU samples are overwhelmingly guest JIT code, and release WASIX can outperform the current native oracle on the hot read shape. +- High RSS is real and scales with concurrent backend activity. WASIX keeps PostgreSQL postmaster and backend state inside one Wasmer process, while native uses OS processes and copy-on-write. The single-process WASIX RSS reaches 672-919 MiB in long read/insert runs; this is memory amplification, not a simple monotonic leak. +- Long write/update liveness was blocked by handler replacement in the wait registry. A socket source could hold only one readiness handler, so a direct blocking socket wait could replace the epoll handler and later remove it. The fix makes readiness fan out to epoll and direct waiters without changing the source registration. +- Remaining throughput/RSS gaps are now performance work, not the same liveness blocker. In the current 20x10000 run, WASIX reaches 21.6% of native indexed-read throughput and 66-73% of native write/update/insert throughput. Peak WASIX RSS is 1.2-1.6x native for write/update/insert and 3.1x native for indexed-read in the sampled run. diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/docs/copied-fork-continuation.md b/src/runtimes/liboliphaunt/wasix-postmaster/docs/copied-fork-continuation.md new file mode 100644 index 00000000..490cdb98 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/docs/copied-fork-continuation.md @@ -0,0 +1,131 @@ +# Copied Fork Continuation Design + +> Historical runtime design note imported from commit +> `aa4211f4485327cb7493863ea3380acf27c5f951`. Copied fork is retained as prior +> art and a probe target; the PostgreSQL build defaults to exec-backend. + +Current disposition: PostgreSQL's normal client-backend path is +`vfork()` followed by a fresh `exec`, so it does not call +`proc_fork(copy_memory=true)`. A prototype that retained the immutable +preinitialization image while copying only pages dirtied relative to that image +is technically plausible and its focused copy/COW tests can be made to pass, +but it would optimize a different runtime path. It is therefore deferred from +the canonical patch until a product workload uses copied fork and can justify +its complexity. It is not a proposed fix for current postmaster RSS or launch +latency. + +## Goal + +Support `proc_fork(copy_memory=true)` for WASIX modules in a way that is +generic enough for upstream Wasmer and strong enough for PostgreSQL's native +process model. + +The runtime pieces are mostly separable: + +- clone or recreate the Wasm instance group; +- copy private linear memory; +- replay `MAP_SHARED | MAP_FIXED` mappings at identical guest addresses; +- clone process state, file descriptors, wait state, and signal delivery; +- resume parent and child at the same guest call site with different return + values. + +The last item is the hard boundary. It is a continuation problem, not a dynamic +linking problem. + +## Current State + +Wasmer has an existing copied-fork path based on logical stack rewind. The +original backend is Asyncify: the module exports `asyncify_start_unwind`, +`asyncify_stop_unwind`, `asyncify_start_rewind`, and +`asyncify_stop_rewind`, and the WASIX runtime stores the logical stack plus +globals before resuming parent and child. + +The local runtime/compiler patch adds a native LLVM continuation backend for +EH/PIC modules. The compiler records fork continuation frames on direct calls +and signature-reachable indirect calls that can reach `proc_fork`, the VM +stores those frames in the instance state, and the WASIX runtime installs the +captured continuation into the copied parent and child instance groups. The +native LLVM backend captures empty operand-stack callsites broadly. For +non-empty operand stacks it currently captures only control-local prefixes: +active nested Wasm control frames must have entered with an empty stack prefix, +the function body must stay under the bounded hardening threshold, and only one +non-empty callsite is captured per function. This is a conservative dominance +guard. Earlier broad capture let a continuation-local base-stack load flow +through an unrelated merge predecessor in `pg_dump`; the current guard is +proven by the dynamic fork probes plus an LLVM verifier compile of `pg_dump`. +The longer-term upstreamable endpoint is explicit PHI repair for arbitrary +non-empty operand-stack continuations. The runtime also extends the +instance-group side of copied fork to dynamic-main +modules, replays shared mmap mappings, and gives copied-fork children a +detached dynamic-linker coordination state. Thread-created instance groups still +share linker operations, but fork-created children can `dlopen` new side modules +without rendezvousing with a parent blocked in `waitpid`. The copied-fork path +creates and adopts child process state only after continuation capture succeeds. + +The local wasix-libc patch exposes `fork()` and `_Fork()` for EH builds, using +the same WASIX `proc_fork(copy_memory=true)` syscall path as non-EH builds. The +`libc-eh-fork` probe proves that POSIX surface links and now succeeds with a +clean child wait under the patched runtime. This is an ABI cleanup; the +continuation machinery remains in the compiler/runtime boundary. + +## Why Dynamic Linking Is Not The Blocker + +`dynamic-dlopen` proves the production extension-loading shape: an EH/PIC +dynamic-main module can load a side module and call exported symbols. The +`dynamic-fork-dlopen` probe is separate because it composes dynamic linking with +copied fork, shared mmap replay, and child-side `dlopen`. It now passes under +strict dynamic mode. Wasmer's own `test_fork_dlopen` fixture adds the stronger +case: a child calls a function from a side module loaded before fork, then loads +a different side module after fork. `dynamic-fork-indirect` and +`test_fork_indirect` prove the same native continuation path through a +function-pointer callsite. + +## Rejected Shortcuts + +- Postgres-side fork bypasses: not generic and would hide runtime semantics. +- Asyncify in production: conflicts with the EH/PIC performance direction and + is already rejected by the build profile unless explicitly enabled as an + experiment. +- Host OS `fork()`: preserves native stack addresses, but is non-portable and + unsafe in a multithreaded Rust runtime after arbitrary host state has been + initialized. It also bypasses Wasmer's in-process WASIX process model. +- Raw coroutine stack copying: not sound in-process because native stack frames + contain pointers to the old stack and store/runtime objects. The copied stack + would need relocation that the runtime cannot infer. +- Guest `setjmp`/`longjmp` trampoline from a fresh entrypoint: not sufficient + for Wasm EH/SjLj because the compiler-generated setjmp catch frames must be + active on the current Wasm call stack. + +## Upstreamable Direction + +Introduce copied fork as a runtime operation over a generic continuation +backend: + +1. The WASIX syscall validates that the current module has a fork continuation + backend before forking observable state. +2. The backend captures a logical continuation at the syscall boundary. +3. The runtime clones process state, creates the copied instance group, copies + private memory, and replays shared mappings. +4. The backend resumes the parent continuation with the child PID and the child + continuation with zero. + +Asyncify is one backend. The PostgreSQL lane now uses a native LLVM backend +that captures continuation frames without Asyncify. This belongs in the +compiler/runtime boundary rather than in PostgreSQL or libc alone. + +## Near-Term Runtime Work + +- Keep the dynamic instance-group copied-memory path generic and covered by + focused tests. +- Keep the fork syscall guarded by a generic continuation capability check; + any rejection must happen before observable child process state is adopted. +- Continue broadening upstream-style Wasmer tests beyond the current dynamic + copied-fork fixtures. +- Add focused tests for copied fork returning an errno before process-state + mutation when no continuation backend exists. +- Track native continuation hardening separately from dynamic linking and + shared memory. +- Keep image-aware dirty-page copying as a separate research branch. Do not + merge it into the EXEC_BACKEND carrier on focused-test evidence alone; first + require an end-to-end copied-fork consumer, lifecycle proof, and measured + memory/latency benefit. diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/docs/embedded-memory-architecture.md b/src/runtimes/liboliphaunt/wasix-postmaster/docs/embedded-memory-architecture.md new file mode 100644 index 00000000..9f7c7b87 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/docs/embedded-memory-architecture.md @@ -0,0 +1,600 @@ +# Embedded memory and throughput architecture + +This document records the measured causes of the WASIX-postmaster memory and +throughput gap, the product constraints, and the staged architecture for making +the real PostgreSQL postmaster plus fresh `EXEC_BACKEND` model viable in an +embedded memory budget. It is an architecture record, not a claim that the +current carrier already meets every gate. + +## Product invariants + +Optimizations in this product must preserve all of the following: + +- one real PostgreSQL postmaster, with PostgreSQL's normal auxiliary roles; +- a fresh `EXEC_BACKEND` guest process for every client backend; +- normal PostgreSQL durability, crash recovery, WAL retention, archiving, + streaming replication, checkpointing, and recycling semantics; +- sealed, compiler-free, exact-AOT product carriers; +- no ambient environment variable that can enable a new acting policy; +- fail-closed behavior when an OS cannot prove the evidence needed by a policy; +- explicit runtime, artifact, carrier, and measurement-tool identities; +- native behavior unchanged by WASIX-only guest patches. + +Allocator knobs, smaller PostgreSQL settings, fork-only sharing, fixed-address +prelinking, KSM, and sampled-memory tricks are not substitutes for these +invariants. + +## Measured memory attribution + +The paired c4 indexed-update run under a 1 GiB cgroup measured a WASIX/native +anonymous-memory delta of 52,895,744 bytes (50.445 MiB). The file-cache charges +were effectively equal, so the difference is runtime-private memory rather than +PostgreSQL data. + +At quiescent readiness, exact mapping attribution found: + +| Owner | Resident anonymous memory | Consequence | +| --- | ---: | --- | +| Wasmer AOT `CodeMemory` | 20,784 KiB | 17,692 KiB RX plus 3,092 KiB adjacent data/custom sections are copied into an anonymous mapping | +| Wasmer live heap and worker arenas | about 24 MiB | `MALLOC_ARENA_MAX=1` merely moved allocations into `brk` and saved less than 1 MiB | +| Four active client instances | about 9,016 KiB above pre-fanout | per-instance structures matter after the fixed engine floor | +| Six guest linear-memory groups at readiness | 2,048 KiB | preinitialized file mappings already avoid a large eager guest-memory copy | +| Resident returned coroutine stacks | less than 1 MiB | virtual reservations are large, but they are not the principal RSS cause | + +The copied AOT artifact contains 21,274,624 bytes. Of 5,194 pages, 5,033 +(96.90%) receive an absolute relocation in the current non-PIC layout. Mapping +the existing artifact privately would therefore retain at most 161 clean pages, +about 0.629 MiB. A direct `mmap` of today's serialized artifact is not an RSS +solution. + +Sparse store snapshots reduce a PostgreSQL snapshot from 174,584 bytes to 129 +bytes by recording four mutable globals instead of 10,911 dense global values. +They remove transient allocation/copy work, but the measured active c4 cgroup +anonymous charge remains essentially unchanged. Their promotion gate is backend +launch/reconnect latency, not a fixed-RSS claim. + +## Measured throughput split + +The same c4 indexed-update workload shows two different regimes: + +- at 1 GiB, the adaptive-v3 carrier reached a diagnostic paired median + WASIX/native ratio of 0.867; +- at 512, 384, and `memory.max=256M`/`memory.high=224M` with zero swap, the + ratios were 0.602, 0.474, and 0.338 respectively. + +The exact v3 WAL-action counts expose the timing defect: 16/20 completed +segments acted at 256 MiB, 10/20 at 384 MiB, only 2--4/20 at 512 MiB, and 0/20 +at 1 GiB. The one-shot descriptor capability was discarded whenever pressure +had not reached relief level 3 at close. The unique table-plus-index footprint +is about 136.4 MB (130.1 MiB), not 212 MiB; the latter double-counts indexes +already included by `pg_total_relation_size`. The workload also writes about +320 MiB of WAL. Roughly 55 MiB of extra WASIX anonymous residency therefore +leaves very little cache headroom at 224 MiB, and four retained 16 MiB WAL +segments can already displace useful relation pages. Full `smaps` sampling +materially perturbs this workload and must never be used as throughput +evidence. + +The v4 unconditional-immediate experiment falsified the claim that retaining +the close-time WAL capability was sufficient. Every v4 point acted on all +20/20 WAL offers (80/80 aggregate), yet its paired c4 indexed-update ratio did +not produce a generally better knee: + +| Cgroup memory | v3 ratio | v4 immediate ratio | Outcome | +| ---: | ---: | ---: | --- | +| 256 MiB | 0.3383 | 0.3486 | ratio uptick came from native variance; WASIX median fell 0.71% | +| 384 MiB | 0.4740 | 0.4579 | regression | +| 512 MiB | 0.6022 | 0.5344 | regression | +| 1 GiB | 0.8669 | 0.7598 | regression; bulk p95 gate failed at 1.559 | + +At 256 MiB, v4's WASIX median was 53,884 ops/s versus v3's 54,268 ops/s; +the ratio only rose because the paired native median was lower. The result +proves both that the descriptor-loss defect was real and that +unconditional retirement-time eviction sacrifices useful cache behavior when +pressure does not justify it. It is not a candidate win. The v4 acting mode is +restricted to explicitly identified pressure experiments while the paired +stream-open/retirement architecture below replaces it; observe/retain remains +the product default. No release or general embedded profile may enable +unconditional immediate eviction from this evidence. + +## Near-term pressure architecture + +PostgreSQL emits semantic cache offers at boundaries where it knows more than a +generic host filesystem. The host validates the already-open regular-file +descriptor and exact range; it never reconstructs a database path. + +WAL class 6 has two independent version-1 bits: + +- bit 0, `WAL_RECLAIM_ELIGIBLE`, preserves the original low-expected-reuse + predicate (`!XLogIsNeeded()` and non-direct WAL). It is not a correctness + proof; +- bit 1, `WAL_CACHE_DROP_SAFE`, positively proves an exact fd/segment/timeline + generation is complete, durable, and buffered. Dropping those cached pages + cannot remove or change the WAL file. Archivers and walsenders may refault it. + +An acting controller must require bit 1. Old guests, flags zero, and bit-0-only +offers remain retain-required. Old hosts reject bit 1 as an errno-preserving, +hint-only call, which is safe. + +Adaptive-linux.v5/embedded-v4 preserves the useful capability insight from v4 +without retaining its unconditional action rule. Its compact configuration is +bound by SHA-256 +`01668b856435cb8c34b2d2324ab55b7f1f5961b8b403c1ee49d9ee4b5c865f53`: + +1. Discover a descriptor-pinned cgroup-v2 pressure source with a finite + effective limit. If discovery or sampling is incomplete, remain + observe-only. +2. Duplicate only an exact bit-1 WAL offer's already-open regular-file + descriptor into a path-free capability ledger. A candidate does not act at + offer time. The ledger is capped simultaneously at four entries, four host + descriptors, 64 MiB, and a four-second TTL. +3. Keep sampler/clock failure, causal PSI/refault breaker state, advice error, + and finalization fail-closed. A successful action arms only the exact next + sample interval; high but flat background pressure is not blamed on it. +4. Resolve the host page size at admission, align every range inward without + widening it, and record that resolved alignment separately from the sealed + host-page policy. This admits both 4 KiB and 64 KiB Linux kernels without a + host-specific configuration identity. +5. Leave relation bulk/vacuum offers observe-only in embedded-v4. Their reuse + is not proven by the WAL contract, and the measured thousands of small + relation `DONTNEED` calls did not explain the cliff. The pressure/token + planner remains available only for a separately identified and qualified + future profile. +6. Let only a newly acquired sample, at least 250 ms after the prior sample, + authorize WAL work. It must be in relief level 2 or 3 and at or above the L2 + exit threshold. It drains at most one descriptor-ledger entry; reusing a + previous high-pressure observation is forbidden. Busy advice is dropped with + zero retries. The timer may expire and release pins but never advises. +7. Keep the global dirty veto for relations. Bit-1 WAL may bypass that global + gauge because the exact offered range has already crossed its durability + boundary; Linux also refuses to discard unwritten dirty pages. +8. Serialize only actionable candidate planning. Normal reads and sync hints + update saturating telemetry atomics without taking the policy lock. A + contended relation offer retains, while a contended proven WAL offer may be + queued after a nonblocking state recheck; PostgreSQL WAL close never waits + behind another cache-advice syscall. +9. Arm PSI/refault breakers only for the exact sample interval containing a + successful advice. PSI must worsen relative to a same-source no-advice + baseline; high but unchanged workload pressure is not attributed to the + policy. Evaluate that causal window before the next WAL action. +10. Before the first payload write on every normal-writer, walreceiver, or + bootstrap open, synchronously revoke class 6/flags zero for that exact file + identity. The host marks a global sequence odd, waits for in-flight action, + detaches and destroys matching pins, then restores an even sequence. Offers + enqueue only when one unchanged even sequence surrounds descriptor capture; + this closes mutation races without paths or unbounded tombstones. +11. Degrade to retain and flush deferred capabilities on sampler/clock errors, + causal PSI/refault trips, advice errors, or revoke errors. +12. Mirror every validation and offer into the unchanged observe-only record and + publish acting-policy telemetry separately and atomically. +13. Finalize under the action gate and publish only after queued and in-flight + work, descriptor capabilities, and mutation identities are zero. Receipt + fields independently recompute terminal entry and byte totals and require + exact conservation against all enqueues. + +Policy/configuration identity belongs in the sealed manifest and carrier +inventory. Output-path environment variables may select where evidence is +written; they cannot select whether eviction acts. + +These rules document the v5 candidate, not a promotion decision. V4's +unconditional immediate lane remains falsified and pressure-campaign-only; v5 +must earn promotion through the handle-lifecycle, correctness, memory, and +performance gates below. + +### Platform cache-relief boundary + +The semantic guest proof and admission model are portable; the cache action is +not. A carrier must name its host implementation and fall back to retain when +the host cannot provide equivalent semantics. + +| Host | Pressure evidence | Candidate mechanism | Current disposition | +| --- | --- | --- | --- | +| Linux | descriptor-pinned cgroup v2 limit/events/PSI | v5 range-scoped `POSIX_FADV_DONTNEED` through a bounded, synchronously revoked descriptor ledger; future per-I/O `RWF_DONTCACHE` only after an exact filesystem probe | v5 is a qualification candidate; v4 immediate mode is falsified as a default; per-I/O mode is unimplemented | +| macOS | task/dispatch memory-pressure sources, with a separately qualified embedder budget | post-durability `MS_INVALIDATE` on an exact handle-backed mapping, or descriptor-wide `F_NOCACHE` selected at stream admission | observe-only until the paired stream-open/retire hooks exist and each mode is qualified | +| Windows | Job Object memory accounting/notifications | `FILE_FLAG_NO_BUFFERING` selected atomically with handle creation, with adapter-owned aligned buffers and offsets | observe-only until the HostFS open/write hook can meet the complete unbuffered-I/O contract | + +Linux documents `POSIX_FADV_DONTNEED` as an attempt to release cached pages, +ignores partial pages, and does not discard unwritten dirty pages; a zero return +therefore means `advice_accepted`, not physical reclamation +([Linux `posix_fadvise(2)`](https://man7.org/linux/man-pages/man2/posix_fadvise.2.html)). +`POSIX_FADV_NOREUSE` is deliberately rejected for this design. Since Linux 6.3 +it changes replacement-recency treatment, but its implementation is whole-file +and does not cover descriptor reads; it neither consumes the one-shot durable +retirement capability nor proves existing WAL pages were released +([Linux NOREUSE implementation](https://github.com/torvalds/linux/commit/17e810229cb3068b692fa078bd9b3a6527e0866a)). + +Linux 6.14's `RWF_DONTCACHE` prunes cache instantiated by the particular read or +write, starts writeback for pages it dirties, and explicitly leaves ranges that +were already cached before that operation alone +([Linux `preadv2`/`pwritev2` contract](https://man7.org/linux/man-pages/man2/readv.2.html)). +The generic VFS rejects it with `EOPNOTSUPP` unless the filesystem advertises +support +([VFS capability gate](https://github.com/torvalds/linux/commit/af6505e5745b9f3a670de405b08b73573343c15c)); +support was added independently for +[XFS](https://github.com/torvalds/linux/commit/974c5e6139db30fae668e44c381d13bcc63b65fa) +and +[ext4](https://github.com/torvalds/linux/commit/ae21c0c0ac56aa734327e9c8b7dfef4270ab54d4). +Kernel-version detection is therefore insufficient. A future adapter must +probe the exact opened filesystem, preserve PostgreSQL's configured sync-open +or explicit-sync durability semantics on every write, and retain close-time +`DONTNEED` for preexisting pages and as a deterministic fallback. + +Apple documents `MS_INVALIDATE` as invalidating cached data for the specified +mapped range +([macOS `msync(2)`](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/msync.2.html)), +and the corresponding XNU entry describes `VM_SYNC_INVALIDATE` as discarding +pages while returning dirty/precious pages only under the declared sync mode +([XNU `vm_user.c`](https://github.com/apple-oss-distributions/xnu/blob/f6217f891ac0bb64f3d375211650a4c1ff8ca1ea/osfmk/vm/vm_user.c#L1122-L1168)). +Apple separately documents `F_NOCACHE` as turning caching off or on for a +descriptor +([macOS `fcntl(2)`](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/fcntl.2.html)). +They are different adapters: retirement-time invalidation needs a bounded +mapping of the exact open file object, whereas `F_NOCACHE` must be selected +before WAL writes begin. The macOS receipt must use `mincore` only as physical +residency evidence, never as the semantic action +([macOS `mincore(2)`](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/mincore.2.html)). + +On Windows, `FILE_FLAG_NO_BUFFERING` is a `CreateFile` contract, not a +post-close hint. Access sizes and offsets must be multiples of the volume's +logical sector and buffer addresses must satisfy physical-sector alignment +([Windows file-buffering contract](https://learn.microsoft.com/en-us/windows/win32/fileio/file-buffering)); +write-through and no-buffering are independent creation flags +([Windows `CreateFile`](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea)). +The adapter must own aligned bounce buffers without changing PostgreSQL's write +grouping, report the queried logical/physical alignment, preserve durability +with the admitted write-through or documented +[`FlushFileBuffers`](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-flushfilebuffers) +path, and fall back before the first write. Retrofitting `NO_BUFFERING` after a +buffered handle has written is not admissible. + +Neither macOS mode nor Windows unbuffered I/O is a drop-in emulation of Linux +`DONTNEED`. Reopening a mutable WAL path, applying a process-wide cache purge, +or silently claiming that a no-op reclaimed memory is forbidden. Until its +handle-bound hook is implemented, macOS and Windows remain observe-only. + +### WAL safety and reuse lifecycle + +Bit 1 proves cache-drop correctness, not absence of a future reader. The sealed +standalone finite-memory v5 profile retains the capability briefly and acts only +under fresh L2/L3 pressure, treating a later archive/recovery read as a refault. +A general archive/replication profile +must not infer the same reuse policy from `wal_level` or a magic memory cutoff. +Its next guest ABI should carry a positive active-consumer/reuse fact and emit a +second consumer-boundary offer after an archiver or walsender has passed the +segment. That profile can retain a bounded genuinely hot segment, release it at +the consumer boundary, and still use the bit-1 safety capability under a proven +memory emergency. Each profile needs a distinct manifest identity and archive, +standby-lag, refault, and breaker qualification. + +The portable successor must expose two handle-bound, generation-checked hooks: + +```text +wal_stream_open(token, file_identity, generation, range) +wal_segment_retired_durable( + token, file_identity, generation, range, durability_epoch +) +``` + +`token` is an unforgeable host capability for the exact HostFS open operation +or still-open file object; it is never a guest path or reusable integer fd. +`file_identity` binds the native volume/mount plus file identity, +`generation` binds timeline and WAL segment, and `range` must be the canonical +whole segment. `wal_stream_open` runs before the first write and atomically +selects buffered, per-I/O drop-behind, descriptor-nocache, or aligned +unbuffered admission. `wal_segment_retired_durable` is accepted exactly once +only after final-page completion and the matching configured durability +operation; its monotonic `durability_epoch` prevents a stale completion from +authorizing a newly reused token. Any identity, generation, range, mode, or +epoch mismatch retains and records failure. Closing or rebinding the token +consumes the generation. + +The current version-1 class-6 bit-1 offer plus synchronous class-6/flags-zero +mutation revoke is a safe bounded implementation of only the retirement half +for buffered Linux WAL. V5 retains the exact descriptor capability until a +fresh pressure trigger, mutation, expiry, displacement, or finalization. A +future guest/host ABI is still needed for stream-open admission. No runtime may +infer the missing open event by watching generic opens or writes. + +Every generation ends in exactly one result from this deny-unknown taxonomy: + +| Result | Exact meaning | +| --- | --- | +| `admission_bypassed` | The open transaction admitted a descriptor/handle cache-bypass mode and every write satisfied that mode. No retirement-time eviction is claimed. | +| `per_io_dropbehind` | Every eligible write used a capability-probed per-I/O drop-behind mode; any preexisting page range was handled by the declared retirement fallback. | +| `postwrite_invalidated` | A supported host invalidated the exact handle-bound range after the durability epoch, for example qualified macOS `MS_INVALIDATE`. | +| `advice_accepted` | A range advice call such as Linux `POSIX_FADV_DONTNEED` returned success. This does not claim that pages were physically reclaimed. | +| `unsupported` | The OS, kernel, filesystem, mount, handle type, alignment, or sealed profile could not admit a mechanism; the range was retained. | +| `failed` | Validation, I/O, sync, clock, breaker, or host action failed after the hook was available; the controller degraded and retained or used only its already-declared safe fallback. | + +Results cannot be collapsed into a success boolean. The per-run capability +receipt binds the hook/ABI version; sealed profile; runtime/carrier identities; +OS and kernel build; filesystem type, mount/volume and file identity; page, +allocation-granule, logical-sector, and physical-sector sizes; probe operation +and exact errno/OS result; admitted mode; generation/range/durability epoch; +fallback; per-result calls and bytes; and raw residency/cgroup evidence digest. +Unknown receipt fields, missing probes, counter imbalance, or a mode/result +combination not permitted by that platform invalidate the run. + +Two policies consume the same correctness fact differently: + +- `embedded-standalone` defaults to observe/retain. Its separately + receipt-bound pressure mode may treat a durable writer-retired generation as + a bounded cache-relief capability and accept a later recovery or consumer + refault only after a fresh L2/L3 pressure trigger. V4 exercised immediate + action but falsified it as an unconditional default; v5 removes immediate + action without weakening archive, recovery, or retention correctness. +- `replication-archive` treats writer retirement as safety, not low reuse. Its + ordinary path retains while an archiver, walsender, restore, or lagged + standby may be a near-term consumer. Routine release needs a future positive + consumer-retirement fact; only a separately qualified, bounded emergency + policy may act earlier. Archive lag, slot retention, standby lag, consumer + refaults, and breaker behavior are part of that profile's receipt. + +The profiles have distinct manifest/configuration identities. `wal_level`, an +enabled archive command, elapsed time, or a memory cutoff cannot silently +switch between them. + +## Runtime-generated relocated code image bridge + +The measured 20,784 KiB anonymous `CodeMemory` can be made reclaimable before +the final position-independent carrier format exists. +`RelocatedCodeImage linux-v1` preserves today's absolute relocation model but +moves its published bytes to a reclaimable regular-disk inode. This is a strict +Linux bridge, not a new cross-platform artifact format. + +### Linux-v1 construction and publication + +The loader performs one transaction per activated AOT module: + +1. Open the receipt-bound private scratch directory by trusted directory fd. + Create an unnamed mode-0600 `O_TMPFILE`, verify a regular inode and the exact + mount/filesystem identity, reject tmpfs, ramfs, memfd, DAX, network, or an + unqualified filesystem, and preallocate the complete page-aligned image so + allocation failure is handled before publication. Linux defines + `O_TMPFILE` as an unnamed regular file that disappears with its last reference + ([Linux `open(2)`](https://man7.org/linux/man-pages/man2/open.2.html)). A + `memfd` is explicitly RAM-backed and uses anonymous memory, so it cannot + support this RSS/reclaimability claim + ([Linux `memfd_create(2)`](https://man7.org/linux/man-pages/man2/memfd_create.2.html)). +2. Reserve the complete final virtual-address interval as runtime-owned + `PROT_NONE`. Validate page-aligned, nonoverlapping RX, RO, and bounded RW + fixup segments and every relocation destination before replacing any page. + `MAP_FIXED` is allowed only inside this exact still-owned reservation; Linux + otherwise documents it as destructive to overlapping mappings + ([Linux `mmap(2)`](https://man7.org/linux/man-pages/man2/mmap.2.html)). +3. Size and preallocate the inode, then map the future RX/RO extent at the final + address as `MAP_SHARED|PROT_READ|PROT_WRITE`, never executable. Copy the + verified serialized bytes once and apply all current absolute relocations in + place. Reject a relocation into an undeclared page, overflow, unsupported + width/kind, overlapping segment, or any page that would remain writable and + executable. +4. Flush the CPU instruction cache where the architecture requires it, compute + the relocated layout digest, `msync(MS_SYNC)` and `fdatasync` the exact + extent, and obtain an identity-checked read-only description of the same + unnamed inode. Remove write access from the complete staging view before + closing every writable file description. The runtime must prove there is no + leaked writable mapping or duplicated `O_RDWR` fd; either invalidates strict + mode. +5. Replace only the owned reservation with page-aligned `MAP_PRIVATE` views from + that read-only description: RX for native instructions, RO for constants and + metadata, and a separately bounded anonymous RW fixup area where mutation is + genuinely required. Replacement occurs without an unreserved address-space + gap. There is no writable executable alias: staging is RW/NX, the complete + staging view becomes inaccessible, and only then is final code RX. Linux + protections distinguish read, write, execute, and none at page granularity + ([Linux `mprotect(2)`](https://man7.org/linux/man-pages/man2/mprotect.2.html)). +6. Revalidate every function, trampoline, unwind, trap, custom-section, and + fixup pointer against the final views. Wasmer may collect in-memory unwind + descriptors while copying, but OS-visible unwind/frame and trap registration + occurs only after W^X finalization. Then atomically publish the finished + `CodeMemory`. On destruction, stop new users, deregister unwind/trap + metadata, wait for the existing runtime ownership barrier, unmap views, and + finally release the inode. +7. Drop only clean construction-induced residency after publication. A + discard/advice return is recorded separately from subsequent `mincore`, + `smaps`, fault, and cgroup evidence; it is never itself reported as bytes + reclaimed. Demand faults then warm the actually used PostgreSQL paths from + the held disk inode, while cold pages remain reclaimable. + +The module registry key is the exact AOT digest, target/CPU policy, runtime ABI, +relocation recipe, and final base/layout. Construction is single-flight and +bounded; all stores in the host process share the one published immutable code +image, while mutable fixups remain at their declared ownership level. Absolute +relocations prevent cross-process sharing and survive only as a bridge until +`NativeCodeImage` replaces them with RVAs. + +### Failure, identity, and compatibility contract + +No partially transitioned image is visible. Any validation, scratch, +preallocation, write, sync, read-only-handle, fixed-map, protection, digest, or +metadata-registration failure rolls back before publication. A carrier that +declares `relocated-code-image-linux-v1=strict` fails startup rather than +silently allocating anonymous code. A separately identified +`anonymous-code-memory-compat-v1` carrier may use the existing loader on macOS, +Windows, read-only/noexec installations, and unqualified Linux filesystems, but +its receipt reports anonymous ownership and makes no bridge RSS claim. + +The strict receipt binds carrier/AOT/runtime/relocation identities; scratch +directory and mount identities; filesystem type and mount flags; page size; +inode device/number, runtime allocation generation, and exact length; +preallocation/sync results; +segment offsets, lengths, protections, and relocated digest; writable-handle +closure proof; unwind/trap registration lifetime; discard results; page-fault +counts; and before/after `smaps` plus cgroup `anon`/`file` evidence. It also +records bytes written and construction count so startup latency and flash wear +are first-class gates. O_TMPFILE crash cleanup, noexec, disk-full, inode +pressure, and abrupt teardown are tested explicitly. + +This bridge succeeds only if the same workload moves `CodeMemory` from +anonymous to clean file-backed accounting and preserves balanced throughput, +tail latency, unwind/error behavior, and cold-start faults under pressure. With +`swap.max=0`, memfd/tmpfs can change a mapping label but cannot substantiate the +required reclaim-to-disk property. + +## Durable file-backed native code image + +The largest fixed anonymous owner requires a new artifact, not a loader trick. +`NativeCodeImage v1` is an offline-prelinked, position-independent image with: + +- page-aligned file-backed RX and RO segments; +- a small anonymous private fixup/RELRO segment; +- function, trampoline, custom-section, and unwind descriptors expressed as + RVAs; +- local PLT stubs and a deduplicated private import table for host libcalls; +- one canonical exception-personality import cell; +- target, CPU policy, linker identity, ABI, segment, and payload digests. + +The producer reuses Wasmer's object emitter and a target system linker offline. +It must reject unknown relocations, text relocations, undefined symbols, +overlapping segments, W+X pages, range overflow, or any residual relocation +whose destination is RX/RO. The runtime mapper verifies the immutable carrier +handle, reserves an address range, maps RX/RO from that same handle, populates +the bounded private fixup area, makes RELRO read-only, and only then registers +unwind/trap metadata and publishes `base + RVA` pointers. + +The current artifact ABI remains an explicit development fallback. Strict +carriers must bump artifact/runtime/carrier identity before admitting the new +image. + +Platform backends are different implementations of this same format contract: + +- Linux: `PROT_NONE` reservation, `MAP_PRIVATE|MAP_FIXED` file mappings, and + anonymous fixup pages; verification pages may be dropped after hashing. +- macOS: vnode-backed private mappings with strict W^X and qualified compact + unwind/hardened-runtime behavior. `MAP_JIT` is unnecessary when executable + pages are never writable. +- Windows: reserved placeholders, 64 KiB-aligned file views, private committed + fixups, DEP/CFG/CET qualification, and `RtlAddFunctionTable`/ + `RtlDeleteFunctionTable` lifetime ordering. + +Unsupported targets reject strict native-code images; they do not silently +activate an anonymous JIT-style copy. + +## Owner-census diagnostic carrier + +Allocator and mapping labels are not sufficient ownership evidence. Before +compacting per-instance state, build a separate +`runtime-owner-census-linux-v1` diagnostic carrier feature. It has its own +runtime ABI and immutable carrier identity, cannot be enabled by an ambient +environment variable, and is never used as promotional throughput evidence; +its counters and registry perturb the process being measured. + +The in-runtime census has two deliberately different instruments: + +- a fixed-capacity range registry covers `CodeMemory` segments, guest linear + memory reservations/committed windows, and coroutine-stack + reservations/committed windows. Registration returns an allocation-generation + token; only that token can update or remove the slot, so an old destructor + cannot erase a reused address. The registry performs no unbounded allocation, + growth, path lookup, or stack capture. Capacity, occupied/high-water slots, + collision, overflow, stale-drop, and inconsistent-snapshot counters are + sealed and receipted. Overflow makes the census incomplete; it never falls + back to an unbounded map. +- saturating aggregate counters cover heap-owned `StoreObjects`, `VMInstance`, + `VMTable`, imports/funcrefs/globals, worker/task records, and returned-stack + pool entries. They report live, capacity, high-water, allocation, and release + cardinalities. Unless the allocator provides an exact exclusive span, a + counter is not converted into bytes by multiplying `size_of` by count. + +Every record declares one ownership shape: + +| Shape | Meaning and aggregation rule | +| --- | --- | +| `exclusive` | An exact disjoint allocated/mapped interval owned by this class; it may participate in the exact-range byte subtotal. Two live exclusive ranges may not overlap. | +| `overlap` | An alias, view, or child interval whose parent is named explicitly; report it for attribution but never add it again. | +| `virtual` | Reserved address space or guard capacity; report start/end and committed subrange but make no residency or allocation-byte claim. | +| `cardinality` | An object count without a stable exclusive address interval; report counts/capacities only. | + +There is no synthetic `runtime_total_bytes`. Only nonoverlapping `exclusive` +ranges may be summed, and cardinality, virtual capacity, overlapping views, +allocator-owned heap bytes, PSS, and cgroup charge remain separate fields. +Address overflow, an unknown ownership shape, missing parent, exclusive +overlap, generation imbalance, or counter underflow invalidates the snapshot. + +### Commit and OS reconciliation + +At each post-activation, idle-role, active-c1/c4/c8, and drained boundary, the +runtime appends the complete bounded census record **before** the existing +runtime-writer flush/fence record and separate committed ACK. The ACK contains +the exact synced-log end offset, so a validator freezes only a prefix that +already contains the owner record. Sampling cannot move the fence or ACK and +the workload never waits for `/proc` parsing on its hot path. + +After observing the committed ACK, the external Linux collector captures PID +plus `/proc/PID/stat` start time before and after one complete `smaps` read and +the cgroup sample. A changed birth identity, partial/raced read, lifecycle +sequence change, or missing cgroup key invalidates the row. Linux defines +`starttime` as time since boot +([Linux `/proc/PID/stat`](https://man7.org/linux/man-pages/man5/proc_pid_stat.5.html)) +and documents `smaps` PSS, anonymous, clean/dirty, and mapping fields +([Linux `/proc/PID/smaps`](https://man7.org/linux/man-pages/man5/proc_pid_smaps.5.html)). + +The reconciler splits OS mappings only at exact registered boundaries and +attributes PSS/anonymous bytes only where a complete VMA or independently +measurable mapping can be assigned without guessing. A subrange inside an +allocator arena remains `ambiguous_heap`; bytes are not apportioned by virtual +length. It publishes: + +```text +attributed_exact_anon_pss +attributed_exact_file_pss +ambiguous_heap_anon_pss +unattributed_anon_pss +``` + +`unattributed_anon_pss` is the observed process `Pss_Anon` less only exact +attributions. A negative residual is a reconciliation error, not a value to +clamp to zero. The raw `smaps`, owner record, ACK, validator, and resulting +attribution digests are all receipt-bound. + +Cgroup v2 is an independent process-tree envelope: `memory.stat` includes +anonymous memory, page cache, and kernel-owned charges +([Linux cgroup-v2 memory controller](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#memory)). +Its `anon`, `file`, `shmem`, kernel, current/peak, events, PSI, and swap values +are recorded beside, not forced to equal, summed process PSS. Differences can +come from page-cache charge ownership, shared-page proportional accounting, +kernel memory, sampling boundaries, and tasks outside a single PID. A census +passes when identities and conservation rules reconcile; equality between PSS +and cgroup charge is neither expected nor manufactured. + +The first diagnostic goal is to account for the measured roughly 24 MiB live +runtime heap, validate the fixed `CodeMemory` owner, and measure the c1/c4/c8 +slope. Only then can a release optimization claim which owner and lifecycle it +changed. + +## Per-instance compaction after ownership census + +These changes are secondary to the file-backed code image and require the +owner-census receipt above first: + +- dense-rank or lazily materialize only funcref-observable local functions; + the measured conservative opportunity is about 389,824 bytes per backend; +- pool non-exported immutable globals while preserving imported expressions, + mutable globals, exported handle identity, side modules, and snapshots; + the conservative opportunity is about 349 KiB per backend; +- remove the duplicate raw table pointer vector while keeping the hot + `VMCallerCheckedAnyfunc` representation; about 37,912 bytes per backend; +- bound returned coroutine-stack ownership and discard resident payload pages; +- measure, then consider file-backed immutable fork bases for the small copied + guest-private ranges. + +Each compaction gets its own exact before/after owner record, `smaps` and cgroup +receipt. No saving is claimed from a type-size estimate alone. + +## Qualification order + +Every stage gets a distinct immutable carrier and is compared in balanced order: + +1. sampler-off c1/c4 throughput at 1 GiB and 256/224 MiB; +2. light cgroup memory decomposition at 1 GiB; +3. nonperturbing persistent-query and reconnect latency; +4. constrained full memory evidence, whose rates are nonpromotional; +5. immediate crash recovery and clean reopen; +6. checkpoint/recycle with exact acting-policy telemetry; +7. lagged physical standby, archive/stream readers, WAL refault, and breaker + behavior; +8. Linux/macOS/Windows artifact mapping and unwind lifecycle lanes. + +Final throughput evidence uses at least ten balanced blocks. The c4 viability +floor remains a paired median ratio of 0.75 and one-sided bootstrap 95% lower +bound of 0.70, with no OOM/max event, zero swap, bounded pressure stalls, and +all independent recovery/lifecycle/memory gates passing. diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/docs/experiment-checkpoint.md b/src/runtimes/liboliphaunt/wasix-postmaster/docs/experiment-checkpoint.md new file mode 100644 index 00000000..f4fa1d42 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/docs/experiment-checkpoint.md @@ -0,0 +1,419 @@ +# Fresh WASIX PostgreSQL Experiment + +> Historical checkpoint imported from commit +> `aa4211f4485327cb7493863ea3380acf27c5f951`. Paths and PostgreSQL 18.3 +> commands below describe that run; use the project root README and Moon tasks +> for the current PostgreSQL 18.4 replay. + +> Performance terms below retain the historical experiment's labels. In the +> current harness, fan-out duration is explicitly **bulk batch wall time** and +> the derived rate is logical row operations per second. Neither is a +> per-query latency, backend-launch latency, request rate, or tail percentile. +> Current evidence and limits are indexed in `replay-status.md` and +> `rss-memory-model.md`. + +This is a quarantined implementation lane for building upstream PostgreSQL +`REL_18_3` under WASIX. It is source-controlled here so the work is not lost, +but it must not affect the existing single-user production asset path. + +The scripts default to ignored work/cache directories: + +- experiment work root: + `assets/wasix-build/work/experiments/fresh-wasix-postgres` +- upstream runtime/libc work root: + `assets/wasix-build/work/upstream` + +Those paths hold source checkouts, native and WASIX build trees, installs, +Wasmer caches, patched sysroots, probe binaries, and generated reports. They are +intentionally ignored so the repo tracks only source inputs and harness logic. + +## Tracked Inputs + +- `bin/`: PostgreSQL baseline, overlay, build, smoke, and fast `make` helpers. +- `lib/common.sh`: shared path, Docker, Wasmer, and report helpers. +- `bench/sql/`: smoke and performance SQL probes. +- `overlays/wasix-core/`: WASIX PostgreSQL port overlay copied onto clean + PostgreSQL. +- `patches/`: PostgreSQL source patches applied to clean `REL_18_3`. +- `runtime-probes/`: early standalone WASIX capability probes. +- `upstream/bin/`: contained Wasmer/wasix-libc blocker check harness. +- `upstream/probes/`: C probes for runtime/libc blockers. +- `upstream/patches/`: exported local Wasmer and wasix-libc WIP patch sets. + +## Isolation Rules + +- Do not change the existing production `assets/wasix-build/*.sh` scripts for + this experiment unless we explicitly promote a piece of work. +- Do not write build outputs into this tracked tree. +- Keep generated source checkouts and compiled artifacts under the ignored + `work/` roots above. +- Treat the existing single-user code as reference material only, not as source lineage. +- Preserve PostgreSQL semantics. Missing POSIX behavior is a WASIX/runtime/libc + blocker unless a narrow PostgreSQL port-layer patch is justified by evidence. + +## Fast Path + +```sh +cd /assets/wasix-build/experiments/fresh-wasix-postgres +./bin/prepare-baseline.sh +./bin/apply-wasix-core-overlay.sh +./bin/build-native-oracle.sh +./bin/smoke-native-oracle.sh +./bin/build-wasix-core.sh +./bin/smoke-wasix-core.sh +``` + +Use `./bin/run-acceptance.sh --continue` to run the lane and collect reports +even after expected blockers. + +## Tight Iteration Loop + +Baseline network refresh is opt-in. After the first checkout, scripts reuse the +local `REL_18_3` tag unless `--refresh` is passed to `prepare-baseline.sh`. + +The WASIX build directory is incremental. It is cleaned only when the upstream +commit, overlay digest, script digest, or sysroot input changes. For focused +compiler work: + +```sh +./bin/wasix-make.sh -C src/backend/port pg_shmem.o +./bin/wasix-make.sh -C src/bin/initdb initdb +./bin/wasix-make.sh -C src/bin/psql psql +``` + +The upstream blocker probes also reuse their compiled `.wasm` files when the +probe source and sysroot signature have not changed: + +```sh +./upstream/bin/run-blocker-probes.sh --strict +``` + +During dynamic process/linker work, run just the decision probe: + +```sh +./upstream/bin/run-blocker-probes.sh --probe dynamic-fork-dlopen --strict-dynamic +./upstream/bin/run-blocker-probes.sh --probe dynamic-fork-indirect --strict-dynamic +./upstream/bin/run-blocker-probes.sh --probe rlimit-stack --strict +``` + +Filtered probe runs keep their own compile signatures, so this path rebuilds +only the selected probe artifacts when sources or the sysroot change. + +For the upstream-style Wasmer fixtures, run: + +```sh +cargo test --manifest-path ../../work/upstream/wasmer/lib/wasix/Cargo.toml --test wasix-wasm dynamic_library_tests::test_fork_dlopen +cargo test --manifest-path ../../work/upstream/wasmer/lib/wasix/Cargo.toml --test wasix-wasm dynamic_library_tests::test_fork_indirect +``` + +A small upstream PostgreSQL regression subset can run against the cached WASIX +postmaster without rebuilding: + +```sh +./bin/run-native-regress-subset.sh +./bin/run-wasix-regress-subset.sh +``` + +The default subset is `boolean case copy`. `run-wasix-regress-subset.sh` now +builds the WASIX `regress.so` support library, uses the hash-scoped Wasmer +cache, and precompiles the core modules unless `WASIX_SKIP_PRECOMPILE=1`. +Pass explicit test names to widen it, for example: + +```sh +./bin/run-wasix-regress-subset.sh test_setup boolean case copy database create_function_c transactions encoding euc_kr infinite_recurse +``` + +The concurrent backend/socket smoke starts one WASIX postmaster and fans out +native `psql` clients over TCP: + +```sh +./bin/smoke-wasix-concurrent-connections.sh --connections 2 --iterations 4 +``` + +It checks that each client got a distinct backend PID, that all client +lifetimes overlapped, and that the concurrent backends can insert/update indexed +rows successfully. Raise `--connections` for stress runs; the default is the +current blocker gate. The default acceptance run does not require this yet; run +`./bin/run-acceptance.sh --continue` or set `WASIX_ACCEPTANCE_CONCURRENT=1` to +include it while collecting broader process/socket evidence. + +For concurrent throughput work, run the native/WASIX query suite: + +```sh +./bin/bench-wasix-concurrent-query-suite.sh \ + --connections 8 \ + --iterations 1000 \ + --rows 100000 +``` + +It keeps setup outside the measured fanout window and runs indexed reads, +mixed insert/update writes, indexed updates, and indexed inserts against one +postmaster per target. The summary TSV records fanout wall time, logical +operations per second, per-client status/timing, verification counts, timeout +status, server log path, and the WASIX `failed to epoll during deep sleep - intr` +count. The resource summary TSV records per-phase process-tree RSS, VSZ, CPU, +sample count, and process count; native samples sum the postmaster process tree +while WASIX samples the Wasmer process. A nonzero verification mismatch, client +failure, timeout, or repeated WASIX epoll interrupt log fails the run. Use +`--target native` for harness validation, `--target wasix` for runtime-focused +testing, `--resource-interval` for sustained resource probes, and +`--postgres-guc` or `--wasmer-arg` only for recorded diagnostic A/B runs. + +The runner sets an explicit bounded Wasmer stack (`WASMER_STACK_SIZE`, default +32 MiB) and reports it with the Wasmer hash/cache path. The patched runtime +exports `proc_rlimit_get`, and wasix-libc `getrlimit(RLIMIT_STACK)` reports the +runtime-owned limit. The CLI derives that limit conservatively from the actual +Wasmer stack size and caps it by the guest C stack. With the default 32 MiB +Wasmer stack this reports a 4 MiB `RLIMIT_STACK`, letting PostgreSQL use its +normal 2 MiB dynamic default while still keeping `infinite_recurse` on the SQL +stack-depth error path instead of exhausting the Wasmer host stack. + +The fresh WASIX build supports multiple artifact-preserving profiles through +`WASIX_CORE_PROFILE`: + +- `safe-o2` keeps the current conservative bring-up flags and existing artifact + roots: `builds/wasix-core` and `install/wasix-core`. +- `o3` enables `-O3` without LTO or Binaryen post-link optimization. +- `o3-wasmopt` adds Binaryen post-link converge/strip without ThinLTO. +- `o3-thinlto` adds ThinLTO without Binaryen post-link optimization. +- `release-o3` mirrors the production release lane: `-O3 -g0 -flto=thin`, + link-time `-flto=thin`, and wasixcc wasm-opt with + `--converge:--strip-debug:--strip-producers`. +- `release-o3-symbols` keeps the same compiler and linker flags as + `release-o3`, but uses wasm-opt `--converge:--debuginfo` so Wasmer perfmap + profiling can emit Wasm function names instead of `function_N` fallbacks. + +Current optimization status: `o3`, `o3-wasmopt`, `o3-thinlto`, and +`release-o3` all build and pass the WASIX `initdb` smoke against the patched +Wasmer/libc stack. The release-lane blocker was split into two defensible fixes: +wasix-libc now invokes the internal default signal handler directly instead of +seeding it through an indirect table entry, and pg_dump's local `executeQuery` +helper is renamed so ThinLTO does not collide with `fe_utils/query_utils.c`. +`release-o3` is therefore benchmarkable, but it is not yet the fastest warm +query profile on all workloads. + +Non-default profiles are written to suffixed roots such as +`builds/wasix-core-release-o3` and `install/wasix-core-release-o3`, so the +current `safe-o2` artifacts remain available for A/B benchmarks. + +`precompile-wasix-core.sh` now defaults to the release-hot runtime scope: +`postgres`, `initdb`, `pg_dump`, `libpq`, and installed PostgreSQL side modules. +Use `WASIX_PRECOMPILE_SCOPE=minimal` for the old three-module loop, or +`WASIX_PRECOMPILE_SCOPE=all` when intentionally probing every installed tool. + +To run the native/WASIX performance matrix: + +```sh +./bin/bench-wasix-core-profiles.sh --profiles "safe-o2 o3 o3-wasmopt o3-thinlto release-o3" +``` + +The matrix writes raw phase timings and logs under +`assets/wasix-build/work/experiments/fresh-wasix-postgres/reports/perf-matrix`. +It measures native and WASIX `initdb`, postmaster readiness, and +`bench/sql/perf-probes.sql`; use `--skip-build` to reuse existing artifacts or +`--profile release-o3` for a focused optimized run. The SQL probe defaults to +100k generated rows; use `--rows 1000` for quick iteration and +`--sql-timeout 0` only when intentionally allowing an unbounded run. + +For query-focused performance work, use the per-workload suite: + +```sh +./bin/bench-wasix-query-suite.sh \ + --profiles "safe-o2 o3 o3-wasmopt o3-thinlto release-o3" \ + --rows 100000 \ + --update-rows 100000 \ + --transaction-rows 100000 \ + --sql-timeout 120 +``` + +It runs native and WASIX against the same SQL workload files in +`bench/sql/query-perf`: bulk insert, unlogged bulk insert, single-transaction +insert, indexed insert, index build, indexed reads, indexed updates, +transactional update batches, and COPY out. Each workload keeps setup outside +`psql` timing and enables timing only for the measured operation(s). The summary +TSV records wall time, summed `psql` time, timed-statement count, log path, and +the row-count/GUC context. + +Use `--postgres-guc name=value` or `POSTGRES_GUCS='name=value ...'` for +diagnostic parity runs. For example, `--postgres-guc wal_buffers=16MB` isolates +the current 100k logged-insert cliff, and adding +`--postgres-guc synchronous_commit=off` isolates commit-flush cost. These are +diagnostic controls, not production bypasses; the target remains native +PostgreSQL durability semantics. + +Use `--wasmer-arg value` or `WASMER_RUN_EXTRA_ARGS='value ...'` for targeted +runtime feature comparisons against the same PostgreSQL artifacts. Keep the +default path as the baseline and record any extra Wasmer arguments in the report +before comparing timings. + +Use `WASMER_COMPILER=llvm|cranelift|singlepass` for backend A/B runs, but only +with a Wasmer CLI built with the matching backend feature. The harness requires +the explicit CLI backend flag for the selected compiler so a Cranelift or +Singlepass run cannot silently fall back to LLVM. LLVM is the only viable backend +for the current EH/PIC/dynamic PostgreSQL module in the patched release binary: +`o3-cranelift-unlogged-backend-toggle` fails at `initdb` with Wasmer reporting +that Cranelift does not support the module's required features. Use +`WASMER_LLVM_NATIVE_CPU=1` to opt into host-CPU LLVM codegen only for a native +target carrying the exact detected host feature set; the harness stores those +artifacts under a separate cache suffix. Treat this lane as host-local: LLVM's +CPU name can encode ISA details beyond Wasmer's portable feature taxonomy, so +native-CPU artifacts must not enter the postmaster product cache or carrier. + +Current WAL latency findings for the stable `o3` artifact and patched release +Wasmer: + +- Host-file `O_DSYNC`/`O_SYNC`, blocking positioned writes, native + `fsync`/`fdatasync`, blocking `pwritev`, repeated-ciovec coalescing, and + verified zero-write are implemented below PostgreSQL in Wasmer/virtual-fs. +- WAL segment initialization is now native-level in the fdatasync stats probe: + `o3-wal-io-stats-fdatasync-single-iov-fastpath` reports 16 MiB WAL init at + 2.29 ms write + 1.37 ms sync on WASIX versus 2.20 ms + 1.52 ms native. +- The warm `open_datasync` path is now at-or-better-than native for the 100k + insert/update pair: `o3-warm-open_datasync-single-iov-fastpath` reports + 283/504 ms WASIX versus 351/579 ms native. +- The explicit `fdatasync` path remains the next runtime target: + `o3-warm-fdatasync-single-iov-fastpath` reports 189/387 ms WASIX versus + 105/231 ms native. The remaining measured gap is no longer WAL segment + zero-fill; it is concentrated in normal WAL sync/syscall boundary cost. +- CPU/query execution is now the broader warm-path blocker after the WAL + zero-fill fix: `o3-cpu-wal-isolation-single-iov-fastpath` reports logged + bulk insert at 217 ms WASIX versus 105 ms native, unlogged bulk insert at + 127 ms versus 76 ms, indexed read at 4.0 ms versus 2.2 ms, and COPY out at + 25.9 ms versus 19.1 ms. Explicit `--enable-simd --enable-relaxed-simd + --enable-extended-const` does not improve the current `o3` artifact; wasixcc + already emits the SIMD/relaxed-SIMD feature flags in the build. +- The optimized release profiles are now correctness-unblocked but do not close + the tuple-heavy execution gap. `o3-wasmopt-nativecpu-unlogged` is the best + constant-insert sample so far at 101.7 ms, while + `o3-thinlto-nativecpu-unlogged` is better on md5 scan, unlogged bulk insert, + and COPY out at 30.0/122.7/25.2 ms. `release-o3-nativecpu-unlogged` passes but + is not faster on this set. Native samples in the same runs remain roughly + 55-79 ms constant insert, 25 ms md5 scan, 77-78 ms unlogged bulk insert, and + 19 ms COPY out, so the next target is compiled Wasm/VM execution overhead, not + WAL zero-fill or release-lane correctness. + +Use `--precompile-scope minimal` for focused edit-test loops that only need +`initdb`, `postgres`, and `libpq`, or `--skip-precompile` when the current +Wasmer binary hash already has the needed cache artifacts. Full matrix runs +should keep the default `runtime` scope. + +Accepted warm-path artifacts can be pinned as a named runtime bundle so later +experiments do not force a rebuild or a Wasmer recompile before benchmarking: + +```sh +WASMER_BIN=/path/to/accepted/wasmer \ +WASMER_LLVM_NATIVE_CPU=1 \ +WASMER_LLVM_FULL_O3_PIPELINE=1 \ +./bin/pin-runtime-artifacts.sh --name release-o3-3a31-nativecpu-fullo3 --profile release-o3 + +./bin/bench-pinned-warm-matrix.sh --pin release-o3-3a31-nativecpu-fullo3 \ + --rows 100000 --update-rows 100000 --transaction-rows 100000 \ + --warmup-runs 2 --measure-runs 5 +``` + +The pinned wrapper fails if the pinned install tree or Wasmer cache changes +during a run. Set `FRESH_PINNED_VERIFY_HASH=1` for full content-hash checking; +the default uses a faster file list/size/mtime check. + +To force the existing patched sysroot into the PostgreSQL build without +rebuilding it: + +```sh +export WASIXCC_SYSROOT_PREFIX=/assets/wasix-build/work/upstream/build/patched-wasixcc-sysroot +./bin/build-wasix-core.sh +``` + +## Cached Artifacts + +The latest expensive local artifacts are intentionally preserved outside Git: + +- patched Wasmer: + `assets/wasix-build/work/upstream/wasmer/target/release/wasmer` +- patched wasix-libc sysroot: + `assets/wasix-build/work/upstream/build/patched-wasixcc-sysroot` +- compiled blocker probes: + `assets/wasix-build/work/upstream/build/probes` +- WASIX PostgreSQL install: + `assets/wasix-build/work/experiments/fresh-wasix-postgres/install/wasix-core` +- optimized WASIX PostgreSQL installs: + `assets/wasix-build/work/experiments/fresh-wasix-postgres/install/wasix-core-$WASIX_CORE_PROFILE` +- native PostgreSQL oracle install: + `assets/wasix-build/work/experiments/fresh-wasix-postgres/install/native-oracle` +- Wasmer module caches: + `assets/wasix-build/work/experiments/fresh-wasix-postgres/tools/wasmer-cache/$WASMER_HASH` +- pinned accepted runtime bundles: + `assets/wasix-build/work/experiments/fresh-wasix-postgres/tools/pinned-runtimes/$PIN_NAME` + +## Current Status + +- Baseline default is now upstream PostgreSQL `REL_18_3`. +- Native PostgreSQL 18.3 oracle build and smoke pass. +- WASIX PostgreSQL 18.3 core build and `initdb` smoke pass under the patched + Wasmer/WASIX stack. +- The WASIX `pg_regress` slice `test_setup boolean case copy database + create_function_c transactions encoding euc_kr infinite_recurse` passes + against PostgreSQL 18.3 expected files, including backend-side loading of + the WASIX `regress.so` support library. +- Runtime-owned `RLIMIT_STACK` reporting is wired through WASIX + `proc_rlimit_get` and wasix-libc `getrlimit()`; it keeps PostgreSQL + stack-depth checks ahead of Wasmer host stack exhaustion without a PostgreSQL + shim. +- The psql teardown blocker is fixed by preserving `SOCK_NONBLOCK` and + `SOCK_CLOEXEC` in wasix-libc `socket()`: the latest trace shows the final + nonblocking `recv` returning `EAGAIN` immediately and psql sending + `Terminate`. +- Local TCP listener readiness in Wasmer now preserves level-triggered accept + semantics when multiple inbound connections are already queued. The + `virtual-net` test + `test_local_tcp_listener_reports_readable_while_accept_backlog_remains` + proves readiness is re-emitted after each accept while backlog remains. +- Host-only WASIX `Sigwakeup` notifications are processed internally during + asyncify/deep-sleep polling instead of surfacing as guest `EINTR`. Real guest + signals still return `EINTR`; pending dynamic-link operations are drained + before the syscall resumes. The epoll unit suite passes, and the concurrent + query benchmark treats repeated PostgreSQL `failed to epoll during deep sleep + - intr` logs as a failure. +- The rebuilt release-o3 Wasmer hash + `2ce6b09de8327dbc55e63bcaed7e6f41157f770d9ebabaed54f28b35dc220a2d` passes + the concurrent query suite with zero repeated epoll warning count. The + 20-connection/1000-iteration native-vs-WASIX sample + `codex-concurrent-20x1000` passed indexed read, mixed write, indexed update, + and indexed insert verification for both targets. A longer + 20-connection/10000-iteration indexed-read resource run also passed; WASIX + peak fanout RSS was 387.8 MiB versus 92.0 MiB sampled native process-tree RSS, + and neither target showed monotonic memory growth across fanout samples. +- Upstream blocker probes pass in strict mode and strict dynamic mode with the + current patched Wasmer and patched wasix-libc sysroot. +- Wasmer LLVM now lowers Wasm `i32/i64.rotl/rotr` through LLVM funnel-shift + intrinsics. The real PostgreSQL `md5_calc` hot function now emits AArch64 + `ror` instructions instead of shift/add rotate sequences. This is a generic + compiler fix and improves the 100k `release-o3` md5/query warm repeat, but + the 5M md5 pass shows it is not sufficient by itself for native latency. +- The `libc-eh-fork` probe proves wasix-libc exposes the standard `fork()` + surface for EH/PIC builds and now succeeds with a child wait under the + patched runtime. +- The strict `dynamic-dlopen` probe proves the production extension-loading + shape: an EH/PIC dynamic-main module loads and calls a WASIX side module. +- The `dynamic-fork-dlopen` decision probe records whether Wasmer dynamic main + modules, copied `proc_fork`, shared mmap replay, and child-side `dlopen` + compose cleanly. It is reported separately because it is a copied-fork + continuation question, not the extension-loading gate. +- The current dynamic decision result is green: copied dynamic instance-group + creation, native LLVM fork continuation frames, signature-reachable indirect + calls, shared-mmap replay, inherited side-module calls, detached child + linker state, and child-side `dlopen` compose. The remaining hardening is + broader PostgreSQL process/extension tests and additional failure-path + coverage, not a PostgreSQL shim candidate. + +## Patch Discipline + +PostgreSQL patches here must explain: + +- what native PostgreSQL behavior is being preserved; +- which WASIX/runtime/compiler fact forced the patch; +- which alternative was rejected and why; +- which smoke/regression/performance gate proves the patch. + +Upstream Wasmer/wasix-libc patches are WIP export artifacts. Before sending any +piece upstream, split it into one coherent change, remove stale experiment code, +match upstream style, and prove it with the smallest relevant probe/test. diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/docs/immutable-carrier-deployment.md b/src/runtimes/liboliphaunt/wasix-postmaster/docs/immutable-carrier-deployment.md new file mode 100644 index 00000000..2815bd26 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/docs/immutable-carrier-deployment.md @@ -0,0 +1,243 @@ +# Linux Immutable Carrier Deployment + +## Purpose and claim boundary + +The Linux embedded candidate executes the sealed carrier directly from verified +immutable inodes. It does not copy AOT archives or preinitialized executable +memory images into temporary files, memfds, a writable Wasmer cache, or a +reflink. This removes loader writeback and duplicate snapshot residency without +changing the postmaster design, fresh backend instances, PostgreSQL shared +memory, or private copy-on-write memory semantics. + +This is a Linux ext-family deployment policy, not an all-platform release +claim. The generic runtime can also recognize a direct read-only-filesystem +mapping for a future SquashFS/EROFS carrier. The ext candidate qualifiers are +stricter: every selected AOT and executable memory-image row must report +`direct-immutable-inode`. + +This immutable payload boundary is distinct from executable-code working +state. On Linux x86_64 the product executor mandates +`wasmer.code-memory.relocated-regular-file.linux-x86_64.v1` and creates a +private mode-0700 sibling directory named +`.oliphaunt-wasix-postmaster-code-memory-v1` beside the carrier. It pins that +directory by device/inode and creates unnamed regular-file code images there. +The sibling is writable runtime state and is intentionally outside the +immutable payload closure; placing it inside the `+i` carrier would make strict +activation impossible. On other compiled hosts the product selects +`wasmer.code-memory.anonymous.v1` for functional compatibility only, with no +Linux RSS/reclaimability claim. + +## Two closures with different jobs + +The deployment receipt binds the complete verified `payload.files` closure: +every regular file and every directory, including the carrier root. All are +marked with `FS_IMMUTABLE_FL`. Files transition first, followed by directories +from deepest to shallowest and finally the root. + +The direct activation subset is smaller and is derived from the signed sealed +manifest: five AOT artifacts and the `initdb` and `postgres` preinitialized +memory images. Wasmer maps only that subset as executable inputs. Marking the +whole carrier immutable is still necessary because cold qualification closes +all verifier descriptors before launch and the runtime subsequently reopens +paths. A writable sibling, directory, manifest, or inventory would leave the +reopened closure weaker than the one that was verified. + +The external deployment receipt is part of the immutable state. It is created +before any carrier flag changes, owned by root, mode `0444`, and itself marked +immutable. Its canonical JSON binds carrier identities, every path's device, +inode, size, mode, uid/gid and digest, pre/post flags, and the direct subset. Benchmark +plans bind the receipt's SHA-256, device and inode plus the carrier closure +identity; a pathname by itself is never evidence. + +The v2 receipt also records `core_profile` and +`guest_build_recipe_sha256`. Qualifiers perform one complete content check at +campaign start and one at campaign end. Per-sample `--fast` checks reopen the +receipt-bound closure and compare inode identity, type, size, mode, and `+i` +without reading payload contents. This is safe for the stated unprivileged +runner threat model because every parent directory and leaf remains immutable; +the privileged deployment authority remains trusted. + +Carrier entry ownership is deliberately preserved rather than rewritten to +root. Both full and fast verification compare each live uid/gid with the +receipt. The security boundary is the immutable flag and +`CAP_LINUX_IMMUTABLE`: an unprivileged owner cannot clear `+i`, modify metadata +or bytes, or replace an immutable entry. Preserving ownership avoids a second, +destructive ownership migration and rollback protocol while still making any +ownership drift fail closed. + +## Deploy, qualify, and remove + +Build and verify a sealed carrier first. Choose a receipt parent controlled by +root and outside the carrier, then deploy with the capability required by the +Linux inode API: + +```bash +project=src/runtimes/liboliphaunt/wasix-postmaster +carrier=/absolute/path/to/sealed-carrier +receipt=/var/lib/oliphaunt/wasix-postmaster/carrier.immutable.json + +sudo "$project/bin/deploy-immutable-sealed-carrier.sh" \ + --sealed-carrier "$carrier" \ + --receipt "$receipt" +``` + +Deployment fails unless the caller has effective UID 0 and effective +`CAP_LINUX_IMMUTABLE`, every carrier inode is on a supported ext-family +filesystem, the payload inventory and manifest are exact, and no receipt +already exists. The verifier is read-only and can be run by the eventual +unprivileged service account: + +```bash +"$project/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$carrier" \ + --receipt "$receipt" +``` + +Inside an already bounded campaign, use the byte-constant verifier: + +```bash +"$project/bin/verify-immutable-sealed-carrier.sh" \ + --sealed-carrier "$carrier" \ + --receipt "$receipt" \ + --fast +``` + +Run performance qualification as that unprivileged account, with no effective +`CAP_LINUX_IMMUTABLE`. The explicit receipt and loader policy are mandatory for +the cold and checkpoint lanes and opt-in for the generic throughput/latency +lanes: + +```bash +"$project/bin/qualify-wasix-single-backend.sh" \ + --sealed-carrier "$carrier" \ + --require-zero-write-aot \ + --immutable-carrier-receipt "$receipt" \ + --blocks 10 \ + --cgroup-memory-max 256M \ + --cgroup-memory-high 224M \ + --cgroup-swap-max 0 \ + --label immutable-throughput + +"$project/bin/qualify-wasix-cold-ownership.sh" \ + --sealed-carrier "$carrier" \ + --immutable-carrier-receipt "$receipt" \ + --blocks 10 \ + --label immutable-cold +``` + +Remove the deployment only through the exact receipt-bound rollback operation: + +```bash +sudo "$project/bin/deploy-immutable-sealed-carrier.sh" \ + --sealed-carrier "$carrier" \ + --receipt "$receipt" \ + --remove +``` + +Removal clears flags in reverse transition order and restores every inode's +recorded pre-deployment flags. It then clears the exact receipt inode's +immutable bit and unlinks it. It never uses a recursive `chattr` command or an +unverified glob. + +## Loader fail-closed contract + +Qualifiers remove both loader variables from their ambient environment before +launch. Only the measured WASIX `initdb` and postmaster commands receive: + +```text +OLIPHAUNT_WASIX_REQUIRE_ZERO_WRITE_AOT=1 +OLIPHAUNT_WASIX_SEALED_LOADER_AUDIT_FILE= +``` + +The first variable rejects streamed-copy and reflink compatibility paths. The +second produces newline-delimited JSON containing two schemas. Mapping rows use +`oliphaunt.wasix-postmaster.sealed-loader-receipt.v2`. Each row binds the PID, +artifact kind, module SHA-256, snapshot mode, logical/read/write/hash byte +counters, sync-call count, and write policy. It also records every read hint and +source/mapping DONTNEED call with applicability, host support, success, and +errno, plus point-in-time file residency after hash/inspection, after the AOT +archive mapping has been released, and immediately after source eviction. +Compatibility mode retains the exact already-open original carrier descriptor +when activation uses a streamed or reflink snapshot, advises the carrier source +and private activation snapshot separately, and reports both outcomes. It does +not relabel private-snapshot eviction as original-source eviction. Direct mode +has one inode, so snapshot eviction is explicitly not applicable. +Linux residency probes use a separate `PROT_NONE` mapping and `mincore(2)`; +they do not read payload bytes or inspect active CodeMemory mappings, regardless +of whether those mappings are anonymous compatibility memory or strict +regular-file-backed RX/RO memory. +The archive-release checkpoint is explicitly not applicable to memory images; +their row instead carries the DONTNEED outcome from the verified image mapping. + +After the complete WASIX process tree is quiescent, the same executor appends +exactly one +`oliphaunt.wasix-postmaster.attested-start-runtime-summary.v1` row for each +loaded `(pid,module)` memory image. Terminal means the root status, execution +leases, pending child publications, and every process in the admitted epoch have +all joined; publishing a root exit status alone is insufficient. Summary rows +bind memory-image schema `oliphaunt.wasix-postmaster.memory-image.v2`, proof and +proof-output SHA-256, mapped size, ordinary-start/fresh/nonfresh counts, +comparison/reuse/remap outcomes and compared/skipped bytes. They appear after +their matching mapping row and are never emitted for an image that was not +loaded. + +Qualification requires paired AOT and preinitialized-memory rows for every +expected fresh executable launch, `mapping_bytes_hashed == logical_bytes`, zero +source and snapshot writes, zero sync calls, successful supported advice, +well-formed measured Linux residency, `write_policy == none-immutable-source`, +and exact mode `direct-immutable-inode`. A successful advisory call is not a +claim that every page was reclaimed: the post-advice mincore counts are the +evidence, and may remain nonzero when the kernel retains or another mapping +uses clean pages. + +The authority for the combined stream is +`oliphaunt.wasix-postmaster.sealed-loader-audit-validation.v3`. For every +preinitialized activation it requires exactly one terminal summary and: + +```text +ordinary starts = fresh + nonfresh +validations = ordinary starts +full compare attempts = full compare successes + failures = 1 +validations = compare attempts + reuse successes + reuse failures +reuse successes = ordinary starts - 1 +compared bytes = mapped size * compare successes +skipped bytes = mapped size * reuse successes +remap successes + failures = compare successes + reuse successes +remap successes = ordinary starts +``` + +`nonfresh`, comparison/reuse/remap failures, and counter overflow must all be +zero. The validator also binds the mapping and summary module/proof/layout data +to the exact final `oliphaunt.wasix-postmaster.sealed-aot.v5` format-6 +manifest. A mapping-only v2 audit is incomplete evidence and cannot satisfy a +v3 qualification gate. + +The checkpoint lane counts its whole campaign rather than accepting one lucky +launch: two fresh WASIX initdb/postmaster pairs per ABBA/BAAB block, plus one +initdb and two postmasters for the standalone recycle lane when enabled. + +## Crash recovery and trust assumptions + +Receipt publication precedes carrier mutation, so the receipt is the recovery +journal. A failure while applying flags rolls completed transitions back. If a +host or process dies in the middle, rerun `--remove` with the same exact paths; +removal accepts receipt-bound inodes in either recorded pre-state or immutable +post-state. It also accepts an already-cleared receipt immutable bit, covering +a crash after that step but before unlink. + +The helper anchors carrier traversal at a no-follow root descriptor, opens the +complete closure relative to directory descriptors, compares device/inode +identity, and retains descriptors through the flag transition. This closes +ordinary symlink and leaf replacement races. The current wrappers still +canonicalize ancestor paths before entering the helper, and final receipt +unlink is pathname-based. Therefore the receipt parent and carrier ancestors +must be trusted against hostile rename/replacement by another privileged actor. +Production hardening requires an anchored parent descriptor throughout the +wrapper boundary and `openat2`-style resolution (or retained ancestor +descriptors through receipt removal). Until that is implemented and tested, +this deployment is candidate research and must not be described as hostile +root-race release-grade. + +Never give the runtime or benchmark process root or `CAP_LINUX_IMMUTABLE` merely +to make deployment convenient. Deployment is a separate privileged phase; +execution is deliberately unprivileged and read-only. diff --git a/src/runtimes/liboliphaunt/wasix-postmaster/docs/libpq-latency-qualification.md b/src/runtimes/liboliphaunt/wasix-postmaster/docs/libpq-latency-qualification.md new file mode 100644 index 00000000..78750783 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix-postmaster/docs/libpq-latency-qualification.md @@ -0,0 +1,148 @@ +# True-libpq latency qualification + +This lane answers a narrower question than the concurrent workload harness: +can one embedded WASIX postmaster serve a persistent libpq request and create, +serve, and reap a reconnecting backend within declared latency ceilings without +leaking host descriptors or changing the measured runtime, native oracle, or +PostgreSQL configuration? + +The probe uses `CLOCK_MONOTONIC`. `persistent` measures `PQexec("SELECT 1")` on +an already-open connection. `reconnect` measures the complete `PQconnectdb`, +`SELECT 1`, result cleanup, and `PQfinish` interval. The latter includes guest +backend creation and teardown; neither metric is interchangeable with the +bulk-client wall time or `psql` `\timing` emitted by other lanes. + +## Balanced fresh-server design + +Run: + +```sh +project=src/runtimes/liboliphaunt/wasix-postmaster +"$project/bin/qualify-wasix-libpq-latency.sh" \ + --sealed-carrier \ + target/oliphaunt-wasix-postmaster/carriers/headless-research \ + --blocks 10 \ + --warmup 100 \ + --samples 1000 \ + --runtime-footprint embedded-concurrent \ + --durability safe \ + --cgroup-memory-max 256M \ + --cgroup-memory-high 224M \ + --cgroup-swap-max 0 \ + --label embedded-true-libpq-v1 +``` + +The block count must be even. An odd block is native, WASIX, WASIX, native (`ABBA`). An even block is WASIX, +native, native, WASIX (`BAAB`). Every position is a separate invocation of the +concurrent harness and therefore a fresh server and fresh cluster. Adjacent +positions form two native/WASIX pairs per block. Ten blocks produce 20 server +runs per target, 20 same-neighborhood ratios per mode, and equal representation +of native-first and WASIX-first order. + +The latency-qualification floor is 10 blocks, 100 warmups, and 1000 measured +calls for each mode on each server. A passing full matrix is classified +`latency-qualified-non-promotional`; smaller matrices are +`latency-diagnostic-non-promotional`. Neither classification composes lifecycle +or memory evidence. The lane always uses the controlled checkpoint policy, +zero host-FD growth allowance, `--resource-detail off`, and +`WASIX_PERF_STATS=0`. PSS, cgroup, and PSI evidence belongs in the separate +resource lane; sampler and verbose instrumentation overhead must not enter this +latency distribution. A configured cgroup triple is still enforced: all three +limits are required together, frozen in the pre-run plan and machine result, +and passed explicitly to every fresh-server harness invocation after the +ambient `WASIX_CGROUP_*` variables are removed. This prevents an unrecorded +ambient limit from changing the latency population, but it does not turn the +timed lane into memory evidence. The runner also removes all ten legacy/current wait-dump +environment names—including both perf-prefixed names and +the request/ACK pair `WASIX_WAIT_DUMP_FENCE_REQUEST_FILE` and +`WASIX_WAIT_DUMP_FENCE_ACK_FILE`—before it invokes the harness. Their exact +list is part of the pre-run plan receipt. + +## Statistical decision + +For each server and mode, the comparator re-opens the raw sample TSV and +independently calculates nearest-rank p50, p95, and p99. For every adjacent +native/WASIX pair it calculates: + +```text +WASIX server p95 / native server p95 +WASIX server p99 / native server p99 +``` + +It then gates the nearest-rank p95 across the 20 server-pair ratios. Separately, +it gates the nearest-rank p95 across the 20 WASIX server p95 values and across +the 20 WASIX server p99 values. The paired gate controls same-host drift; the +absolute gate prevents an anomalously slow native denominator from concealing +unviable WASIX latency. Server repetitions remain the experimental units—raw +calls from different servers are not pooled and presented as thousands of +independent server observations. + +The initial declared defaults are: + +| Mode | Max paired p95-ratio distribution p95 | Max paired p99-ratio distribution p95 | Max WASIX server-p95 distribution p95 | Max WASIX server-p99 distribution p95 | +| --- | ---: | ---: | ---: | ---: | +| Persistent | 2.0x | 2.5x | 0.25 ms | 0.40 ms | +| Reconnect | 3.5x | 4.5x | 20 ms | 30 ms | + +These are initial viability/regression ceilings, not a claim of native parity. +They were chosen before a qualifying rerun from the retained +`true-libpq-safe-o2-c1-n1000-192m-v1` evidence. That run observed: + +| Mode/target | Samples | p95 | p99 | WASIX/native p95 | WASIX/native p99 | +| --- | ---: | ---: | ---: | ---: | ---: | +| Persistent/native | 1000 | 0.108599 ms | 0.123791 ms | — | — | +| Persistent/WASIX | 1000 | 0.139250 ms | 0.193360 ms | 1.282x | 1.562x | +| Reconnect/native | 1000 | 5.137682 ms | 5.608269 ms | — | — | +| Reconnect/WASIX | 879 | 13.495785 ms | 17.407965 ms | 2.627x | 3.104x | + +The retained run is explicitly non-qualifying: the WASIX reconnect stream +ended at 879 of 1000 samples with `connect_error`. Its successful prefix is +used only to set conservative pre-run ceilings and cannot establish that the +product passes them. The new qualifier requires every declared sample from +every server and rejects any failed, missing, duplicated, or reordered row. + +## Frozen identities and receipts + +Before the first server starts, the runner writes and hashes +`qualification-plan.tsv`. The receipt fixes the matrix size, ordering, +PostgreSQL profiles, finite cgroup binding (or its explicit absence), +instrumentation policy, and all eight gates. It also: + +- verifies the sealed carrier twice and freezes the manifest, build receipt, + payload inventory, and headless binary as one closure identity; +- inventories native `postgres`, `initdb`, `psql`, every regular libpq + artifact, and libpq symlink topology twice to freeze the native oracle; +- freezes the libpq probe source and requires the rebuilt probe binary to be + byte-identical in every server run; +- requires the linked libpq path and hash to be one exact regular artifact in + the frozen native oracle and identical across all native and WASIX runs; +- freezes the named PostgreSQL profile resolution, compares each adjacent + pair's effective settings, and requires each benchmark's profile receipts + to match the qualification copies byte-for-byte; and +- re-verifies the plan, probe source, profile sources, carrier closure, and + native oracle before and after every fresh-server sample. + +Input mutation is an authority failure and stops the run. A structurally bad +sample causes the comparator to fail closed without publishing aggregate +receipts. A valid matrix that exceeds a gate still publishes the exact sample +and pair receipts with `failed` gate rows, so the regression remains +diagnosable. + +The durable outputs below +`reports/libpq-latency-qualification/