Repository navigation
fix: stack-depth guard and shadow-stack restore for deep recursion (follow-up to #179) - #180
pyramation wants to merge 3 commits into
Conversation
libpg_query's check_stack_depth() never had a base pointer, so recursion past the WASM stack trapped (memory access out of bounds). Call the fork's pg_query_set_stack_limit() at every export with the stack actually left (weak symbol: upstream pins skip it) and pin 17/18 to the fork branches. Restore the shadow stack pointer in callWasm after a throw so sync calls on the stale instance do not start half-way down it. Regression test: 10 parseSync of a 100k-term expression under --stack-size=8000.
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
Review complete. 🟠 1 high 📍 Findings outside the diff (1) — 🟠 1 high — defects on lines GitHub can't attach comments to🟠 High — Add // versions/18/src/index.ts
126 interface WasmModule {
127 _malloc: (size: number) => number;
128 _free: (ptr: number) => void;
129 _wasm_free_string: (ptr: number) => void;
130 _wasm_parse_query: (queryPtr: number) => number;
131 _wasm_parse_query_raw: (queryPtr: number) => number;
132 _wasm_free_parse_result: (ptr: number) => void;
133 _wasm_parse_plpgsql: (queryPtr: number) => number;
134 _wasm_fingerprint: (queryPtr: number) => number;
135 _wasm_normalize_query: (queryPtr: number) => number;
136 _wasm_scan: (queryPtr: number) => number;
137 lengthBytesUTF8: (str: string) => number;
🧹 Nitpicks (1) — 🟢 1 low
The change hardens the WASM boundary: every Reviewed commit: 548eac8 |
Summary
Follow-up to #179 (ship that first). Two related crash-safety fixes for deep recursion, plus the fork pins that carry the C side.
1. Stack-depth guard (needs the fork PRs). libpg_query's
check_stack_depth()has always been a no-op in our builds —stack_base_ptris only set byset_stack_base()from PostgreSQL'smain(), which the library never calls. So when recursion in the JSON/protobuf output, fingerprint, deparse or PL/pgSQL dump exhausts the 32 MiB WASM stack, the instance traps (memory access out of bounds— the Aug-2025 crash). The fork PRs addpg_query_set_stack_limit(bytes)and thecheck_stack_depth()calls; this PR:templates/{,full/}wasm_wrapper.c:set_stack_guard()at the top of every non-free export,pg_query_set_stack_limit(emscripten_stack_get_free() - 512 KiB). The symbol is__attribute__((weak)), so 13–16 (pinned to upstream pganalyze tags without the setter) build unchanged and skip the call.versions/18→constructive-io/libpg_query@b66cc16(tip offeat/stack-depth-guard-18, Make check_stack_depth() effective for embedders; guard recursive output paths libpg_query#10),versions/17→@ab619b8(tip offeat/stack-depth-guard-17, Make check_stack_depth() effective for embedders; guard recursive output paths (17) libpg_query#11). The fork tips also free the PL/pgSQL alias list (pre-existing valgrind failure). CI's v17/v18 WASM jobs fail withfatal: reference is not a treeuntil those fork PRs are merged (with a merge commit, so the pinned commits stay reachable from18-constructive/17-constructive); then re-run CI.Result:
ulimit -s unlimited; node --stack-size=100000,SELECT 1+1+…×250k terms → wasRuntimeError: memory access out of bounds, nowSqlError: stack depth limit exceeded; next call fine.2. Stale-instance shadow stack (wrapper only). After a V8
RangeErrorinside the parser,callWasmre-instantiates, but sync calls in the same tick must keep using the stale instance until the replacement arrives — and the throw unwound the JS frames, not the C shadow stack, so each such call started further down it. Undernode --stack-size=8000, 10 back-to-backparseSynccalls of a 100k-term expression trap on the 4th on both the 128/32 and 40/32 builds. Fix:callWasmsavesstackSave()before the call andstackRestore()s it on throw (stackSave/stackRestoreadded toEXPORTED_RUNTIME_METHODS).Regression test
versions/{17,18}/test/wasm-stale-stack.test.js(child process,--stack-size=8000, 10 sync calls, asserts noRuntimeErrorand that the replacement instance parses). Pre-fix on the current builds:["RangeError"×3, "RuntimeError"×7](fails); post-fix:RangeError×10 (passes).Evidence (v18 built from the fork branch, v17 and v13 rebuilt too)
pnpm test: v18 144/144 (incl. the new test), v17 73/73, v13 72/72 (weak symbol path).pnpm test:stress 18: 4/4.1+1+…20k–500k,->50k, 3k nested PL/pgSQL, 8.8/26 MB files; default and--stack-size=8000): no traps; verdicts unchanged except the raised-stack case above, which is now a clean error.wasm/libpg-query.wasmsha256 with these pins (emsdk 6.0.3, 128 MiB floor on this branch): v1842f112ab2dde2df4…, v17e98ac195eb54f881…— these change again once build: 40 MiB WASM initial memory default, retire low-memory variants (patch 13.8.1–18.2.1) #179 (40 MiB floor) is merged in.Link to Devin session: https://app.devin.ai/sessions/2a9e06519f424ebab14772c74c05dd83
Open in Devin Desktop: https://app.devin.ai/desktop/session/2a9e06519f424ebab14772c74c05dd83?variant=devin
Requested by: @pyramation