Last active
August 5, 2026 05:45
-
-
Save moznion/501dd99be95f266f1dd2eda3fb446967 to your computer and use it in GitHub Desktop.
Crash reproduction code for PGlite WASI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Reproducible build of the *stock* PGLite WASI module: branch | |
| # REL_17_5_WASM-pglite at its current head (2194acf — the only branch | |
| # with the WASI flavor; the default REL_18_3-pglite branch has none), | |
| # compiled by the branch's own wasm-build.sh inside the portable-sdk | |
| # toolchain it pins. None of the wasipg patches are applied. | |
| # | |
| # The single local change is dropping -Wl,--no-stack-first from | |
| # pglite-wasm/build.sh: the SDK's lld 19 does not know that flag, so | |
| # the branch does not link at all without this (build-level only, no | |
| # behavioral effect). | |
| # | |
| # ./build-stock.sh # artifacts land in ./stock-build/out/ | |
| # | |
| # Self-contained: docker (buildkit), curl and git are the only | |
| # requirements — no other file from this repository is needed. The | |
| # toolchain image is ~4 GB. | |
| set -euo pipefail | |
| cd "$(dirname "$0")" | |
| REPO=https://github.com/electric-sql/postgres-pglite | |
| REF=2194acf356e9b7bf7cff29c4ed4bd6b82b93895d # head of REL_17_5_WASM-pglite | |
| SDK_VERSION=3.1.74.12.0 | |
| SDK_SHA256_AARCH64=ae5be6198935ca67bba69eb80a1ad6e9fcca4f32bc1c993bdf41026700c49a50 | |
| case "$(uname -m)" in | |
| arm64|aarch64) UARCH=aarch64 ;; | |
| x86_64) UARCH=x86_64 ;; | |
| *) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;; | |
| esac | |
| ROOT=$PWD/stock-build | |
| mkdir -p "$ROOT/cache" "$ROOT/out" | |
| # --- 1. portable-sdk: the self-contained toolchain the branch pins ---- | |
| SDK_TARBALL=python3.13-wasm-sdk-debian12-${UARCH}.tar.lz4 | |
| if [ ! -f "$ROOT/cache/$SDK_TARBALL" ]; then | |
| echo "fetching portable-sdk ${SDK_VERSION} (${UARCH})" | |
| curl -fL --retry 3 -o "$ROOT/cache/$SDK_TARBALL.part" \ | |
| "https://github.com/electric-sql/portable-sdk/releases/download/${SDK_VERSION}/${SDK_TARBALL}" | |
| mv "$ROOT/cache/$SDK_TARBALL.part" "$ROOT/cache/$SDK_TARBALL" | |
| fi | |
| if [ "$UARCH" = aarch64 ]; then | |
| echo "$SDK_SHA256_AARCH64 $ROOT/cache/$SDK_TARBALL" | shasum -a 256 -c - | |
| else | |
| echo "WARNING: no pinned sha256 for the $UARCH portable-sdk tarball" >&2 | |
| fi | |
| # --- 2. builder image: debian:12 + the SDK at /tmp/sdk, nothing else -- | |
| IMG=pglite-wasi-stock-builder:${SDK_VERSION}-${UARCH} | |
| docker build -t "$IMG" --build-arg SDK_TARBALL="$SDK_TARBALL" -f - "$ROOT/cache" <<'EOF' | |
| # syntax=docker/dockerfile:1 | |
| FROM debian:12 | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| git wget curl ca-certificates lz4 xz-utils bison flex pkg-config \ | |
| autoconf automake libtool make patch file python3 \ | |
| && rm -rf /var/lib/apt/lists/* | |
| ARG SDK_TARBALL | |
| RUN --mount=type=bind,source=.,target=/cache \ | |
| tar xf /cache/${SDK_TARBALL} --use-compress-program=lz4 -C / | |
| WORKDIR /workspace | |
| EOF | |
| # --- 3. pristine source checkout at the pinned head ------------------- | |
| SRC=$ROOT/postgres-pglite | |
| if [ ! -d "$SRC/.git" ]; then | |
| mkdir -p "$SRC" | |
| git -C "$SRC" init -q | |
| git -C "$SRC" remote add origin "$REPO" | |
| fi | |
| git -C "$SRC" fetch -q --depth 1 origin "$REF" | |
| git -C "$SRC" checkout -qf "$REF" | |
| git -C "$SRC" clean -qfdx | |
| # --- 4. the one unavoidable fixup: lld 19 has no --no-stack-first ----- | |
| perl -pi -e 's/ -Wl,--no-stack-first//' "$SRC/pglite-wasm/build.sh" | |
| if grep -q 'no-stack-first' "$SRC/pglite-wasm/build.sh"; then | |
| echo "failed to drop --no-stack-first" >&2 | |
| exit 1 | |
| fi | |
| # --- 5. the runtime-filesystem packer, run inside the container after | |
| # wasm-build.sh. The branch references a wasmfs.txt manifest that | |
| # does not exist in-tree, so this packs the whole share/ + lib/ | |
| # trees the module expects at /tmp/pglite instead. --------------- | |
| PACK_SH=$ROOT/pack.sh | |
| cat > "$PACK_SH" <<'EOF' | |
| #!/bin/bash | |
| set -euo pipefail | |
| PGROOT=/tmp/pglite | |
| OUT=/tmp/sdk/dist | |
| # wasm-build.sh's tail steps fail on harmless missing optional pieces, | |
| # so we run unconditionally after it; the real success signal is the | |
| # linked module. | |
| if [ ! -f "$PGROOT/bin/pglite.wasi" ]; then | |
| echo "pack: $PGROOT/bin/pglite.wasi missing — build failed" >&2 | |
| exit 1 | |
| fi | |
| # Placeholders the boot path expects to exist (argv[0] is | |
| # $PREFIX/bin/postgres; initdb is probed by pgl_initdb). | |
| touch "$PGROOT/bin/initdb" "$PGROOT/bin/postgres" | |
| # The module and the share/ tree are PostgreSQL binaries, and the | |
| # PostgreSQL License requires its copyright notice to travel with every | |
| # copy. Fail loudly rather than silently packing a notice-less bundle. | |
| if [ ! -f /workspace/COPYRIGHT ]; then | |
| echo "pack: /workspace/COPYRIGHT missing — refusing to pack PostgreSQL binaries without their copyright notice" >&2 | |
| exit 1 | |
| fi | |
| cp /workspace/COPYRIGHT "$PGROOT/COPYRIGHT" | |
| cp /workspace/COPYRIGHT "$OUT/COPYRIGHT" | |
| cd / | |
| tar -cJf "$OUT/pglite-wasi.tar.xz" \ | |
| tmp/pglite/COPYRIGHT \ | |
| tmp/pglite/bin/pglite.wasi \ | |
| tmp/pglite/bin/initdb \ | |
| tmp/pglite/bin/postgres \ | |
| tmp/pglite/password \ | |
| tmp/pglite/etc/postgresql/locale \ | |
| tmp/pglite/lib/postgresql \ | |
| tmp/pglite/share/postgresql | |
| du -h "$OUT/pglite-wasi.tar.xz" | |
| EOF | |
| chmod +x "$PACK_SH" | |
| # --- 6. run the branch's own build, then pack the runtime FS ---------- | |
| docker run --rm \ | |
| -e WASI=true -e CI=true -e DEBUG=false \ | |
| -e PG_VERSION=17.5 -e PG_BRANCH=REL_17_5_WASM \ | |
| -e SDKROOT=/tmp/sdk -e GETZIC=false -e ZIC=/usr/sbin/zic \ | |
| -v "$SRC:/workspace:rw" \ | |
| -v "$ROOT/out:/tmp/sdk/dist:rw" \ | |
| -v "$PACK_SH:/pack.sh:ro" \ | |
| -w /workspace \ | |
| "$IMG" \ | |
| bash -c './wasm-build.sh; /pack.sh' | |
| echo | |
| echo "stock artifacts:" | |
| (cd "$ROOT/out" && shasum -a 256 pglite.wasi pglite-wasi.tar.xz) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| // Minimal node:wasi repro for broken ERROR handling in the PGLite WASI build. | |
| // node pglite-node-wasi.mjs <workdir> [sql]... # workdir = extracted pglite-wasi.tar.xz | |
| // Boot recipe as in pglite-bindings (_start → pgl_initdb → use_wire), wire pump over | |
| // PGDATA/.s.PGSQL.5432.{in,out}. Default scenario: good query, ERROR, good query — | |
| // the stock WASI build traps on the first ERROR; a working-sjlj build keeps serving. | |
| import { WASI } from 'node:wasi'; | |
| import fs from 'node:fs'; | |
| import path from 'node:path'; | |
| import crypto from 'node:crypto'; | |
| const workdir = process.argv[2] ?? (console.error('usage: node pglite-node-wasi.mjs <workdir> [sql]...'), process.exit(2)); | |
| const sqls = process.argv.slice(3); | |
| if (!fs.existsSync(path.join(workdir, 'tmp/pglite/bin/pglite.wasi'))) { | |
| console.error(`${workdir}: not a workdir — extract the bundle first: tar xf pglite-wasi.tar.xz -C ${workdir}`); | |
| process.exit(2); | |
| } | |
| const dev = path.join(workdir, 'dev'); | |
| fs.mkdirSync(dev, { recursive: true }); | |
| fs.writeFileSync(path.join(dev, 'urandom'), crypto.randomBytes(128)); // guest opens /dev/urandom by path | |
| const pgLog = fs.openSync(path.join(workdir, 'pg.log'), 'a'); | |
| const wasi = new WASI({ | |
| version: 'preview1', | |
| args: ['/tmp/pglite/bin/postgres', '--single', 'postgres'], | |
| env: { PREFIX: '/tmp/pglite', PGDATA: '/tmp/pglite/base', PGSYSCONFDIR: '/tmp/pglite', | |
| PGUSER: 'postgres', PGDATABASE: 'template1', REPL: 'N', TZ: 'UTC', PGTZ: 'UTC', PATH: '/tmp/pglite/bin' }, | |
| preopens: { '/tmp': path.join(workdir, 'tmp'), '/dev': dev }, | |
| stdout: pgLog, stderr: pgLog, returnOnExit: true, | |
| }); | |
| const inst = await WebAssembly.instantiate( | |
| await WebAssembly.compile(fs.readFileSync(path.join(workdir, 'tmp/pglite/bin/pglite.wasi'))), | |
| wasi.getImportObject()); | |
| wasi.start(inst); // runs main(); with REPL=N it returns | |
| const ex = inst.exports; | |
| const io = (s) => path.join(workdir, 'tmp/pglite/base/.s.PGSQL.5432' + s); | |
| const frames = (buf) => { | |
| const out = []; | |
| for (let n; buf.length >= 5 && (n = buf.readUInt32BE(1)) >= 4 && 1 + n <= buf.length; buf = buf.subarray(1 + n)) | |
| out.push({ type: String.fromCharCode(buf[0]), body: buf.subarray(5, 1 + n) }); | |
| return out; | |
| }; | |
| // Send one client message, tick the guest until ReadyForQuery — or until output | |
| // goes quiet (the auth handshake legitimately ends without Z). | |
| function exchange(payload) { | |
| ex.use_wire(1); | |
| fs.writeFileSync(io('.in'), payload); | |
| let buf = Buffer.alloc(0); | |
| for (let idle = 0; idle < 10 && !frames(buf).some((f) => f.type === 'Z'); ) { | |
| ex.interactive_one(); | |
| ex.wasipg_flush?.(); // patched builds: side-effect-free reply flush | |
| try { buf = Buffer.concat([buf, fs.readFileSync(io('.out'))]); fs.rmSync(io('.out')); idle = 0; } | |
| catch { if (!fs.existsSync(io('.in'))) idle++; } | |
| } | |
| return frames(buf); | |
| } | |
| // ErrorResponse/NoticeResponse body: repeated <field-type byte><value\0>, then a final \0. | |
| const fields = (body) => Object.fromEntries( | |
| body.toString('utf8').split('\0').filter(Boolean).map((s) => [s[0], s.slice(1)])); | |
| const report = (f) => { | |
| const e = fields(f.body); | |
| console.log(`<! ${e.S ?? 'ERROR'}: ${e.M ?? ''}${e.C ? ` (SQLSTATE ${e.C})` : ''}`); | |
| for (const [label, v] of [['DETAIL', e.D], ['HINT', e.H], ['WHERE', e.W]]) if (v) console.log(` ${label}: ${v}`); | |
| }; | |
| const u32 = (n) => { const b = Buffer.alloc(4); b.writeUInt32BE(n); return b; }; | |
| const msg = (t, s) => { const b = Buffer.from(s + '\0'); return Buffer.concat([Buffer.from(t), u32(b.length + 4), b]); }; | |
| const md5 = (s) => crypto.createHash('md5').update(s).digest('hex'); | |
| try { | |
| ex.pgl_initdb(); ex.pgl_backend?.(); ex.use_socketfile?.(); | |
| const kv = Buffer.from('user\0postgres\0database\0template1\0\0'); | |
| const auth = exchange(Buffer.concat([u32(kv.length + 8), u32(196608), kv])) // StartupMessage, protocol 3.0 | |
| .find((f) => f.type === 'R' && f.body.readUInt32BE(0) === 5); // AuthenticationMD5Password | |
| exchange(msg('p', 'md5' + md5(md5('password' + 'postgres') + auth.body.subarray(4, 8).toString('binary')))); | |
| let failed = 0; | |
| for (const sql of sqls) { | |
| console.log('->', sql); | |
| const errs = exchange(msg('Q', sql)).filter((f) => f.type === 'E'); | |
| errs.forEach(report); | |
| if (errs.length) failed++; | |
| } | |
| console.log(`instance survived all queries (${failed}/${sqls.length} returned an ERROR)`); | |
| } catch (e) { | |
| console.log('guest trapped:', String(e)); | |
| console.log(fs.readFileSync(path.join(workdir, 'pg.log'), 'utf8').trimEnd().split('\n').slice(-3).join('\n')); | |
| process.exit(1); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment