From b30efdb076bc3bb98c8f665d042f302f28472531 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 25 Oct 2026 04:12:06 +0000 Subject: [PATCH] fix(v18): pin libpg_query with checked scan allocations; report its out of memory as out of WASM memory --- templates/full/index.ts | 8 +++++++- versions/18/Makefile | 2 +- versions/18/package.json | 2 +- versions/18/src/index.ts | 8 +++++++- versions/18/test/scan.test.js | 29 +++++++++++++++++++++++++++++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/templates/full/index.ts b/templates/full/index.ts index 767089a..a47644a 100644 --- a/templates/full/index.ts +++ b/templates/full/index.ts @@ -245,14 +245,20 @@ function ptrToString(ptr: number): string { return wasmModule.UTF8ToString(ptr); } +const NO_RESULT_OUT_OF_MEMORY = 'libpg_query returned no result: out of WASM memory (heap limit is WASM_MAXIMUM_MEMORY, 1 GiB); parse large inputs in smaller pieces'; + function readDetailedResult(resultPtr: number): string { if (!resultPtr) { - throw new Error('libpg_query returned no result: out of WASM memory (heap limit is WASM_MAXIMUM_MEMORY, 1 GiB); parse large inputs in smaller pieces'); + throw new Error(NO_RESULT_OUT_OF_MEMORY); } const ptrAt = (offset: number) => wasmModule.getValue(resultPtr + offset, 'i32'); const str = (ptr: number) => (ptr ? wasmModule.UTF8ToString(ptr) : undefined); if (ptrAt(0)) { const message = str(ptrAt(4)) ?? 'Unknown error'; + // libpg_query reports a failed malloc as a plain "out of memory" error. + if (message === 'out of memory') { + throw new Error(NO_RESULT_OUT_OF_MEMORY); + } const lineno = ptrAt(16); const cursorpos = ptrAt(20); throw new SqlError(message, { diff --git a/versions/18/Makefile b/versions/18/Makefile index 268e2f1..ebe0b54 100644 --- a/versions/18/Makefile +++ b/versions/18/Makefile @@ -19,7 +19,7 @@ WASM_STACK_SIZE ?= 33554432 # byte-for-byte. Set in package.json `x-publish` and applied by copy:templates. LIBPG_QUERY_REPO := https://github.com/constructive-io/libpg_query.git LIBPG_QUERY_TAG := 18-constructive -LIBPG_QUERY_COMMIT := b0f5f331dc50d69597ec610de7b21afa47a66808 +LIBPG_QUERY_COMMIT := 8076f09aecc655a717e4a038e182a9dd41cc3eb8 CACHE_DIR := .cache diff --git a/versions/18/package.json b/versions/18/package.json index 1ee183f..be6400d 100644 --- a/versions/18/package.json +++ b/versions/18/package.json @@ -15,7 +15,7 @@ "distTag": "pg18", "libpgQueryRepo": "https://github.com/constructive-io/libpg_query.git", "libpgQueryTag": "18-constructive", - "libpgQueryCommit": "b0f5f331dc50d69597ec610de7b21afa47a66808", + "libpgQueryCommit": "8076f09aecc655a717e4a038e182a9dd41cc3eb8", "fullApi": true }, "files": [ diff --git a/versions/18/src/index.ts b/versions/18/src/index.ts index 31ce966..badaa19 100644 --- a/versions/18/src/index.ts +++ b/versions/18/src/index.ts @@ -252,14 +252,20 @@ function ptrToString(ptr: number): string { return wasmModule.UTF8ToString(ptr); } +const NO_RESULT_OUT_OF_MEMORY = 'libpg_query returned no result: out of WASM memory (heap limit is WASM_MAXIMUM_MEMORY, 1 GiB); parse large inputs in smaller pieces'; + function readDetailedResult(resultPtr: number): string { if (!resultPtr) { - throw new Error('libpg_query returned no result: out of WASM memory (heap limit is WASM_MAXIMUM_MEMORY, 1 GiB); parse large inputs in smaller pieces'); + throw new Error(NO_RESULT_OUT_OF_MEMORY); } const ptrAt = (offset: number) => wasmModule.getValue(resultPtr + offset, 'i32'); const str = (ptr: number) => (ptr ? wasmModule.UTF8ToString(ptr) : undefined); if (ptrAt(0)) { const message = str(ptrAt(4)) ?? 'Unknown error'; + // libpg_query reports a failed malloc as a plain "out of memory" error. + if (message === 'out of memory') { + throw new Error(NO_RESULT_OUT_OF_MEMORY); + } const lineno = ptrAt(16); const cursorpos = ptrAt(20); throw new SqlError(message, { diff --git a/versions/18/test/scan.test.js b/versions/18/test/scan.test.js index c96ef4e..c23fd65 100644 --- a/versions/18/test/scan.test.js +++ b/versions/18/test/scan.test.js @@ -1,6 +1,7 @@ const query = require("../"); const { describe, it, before, after, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); +const { execFileSync } = require('node:child_process'); describe("Query Scanning", () => { before(async () => { @@ -364,6 +365,34 @@ describe("scan input limits", () => { assert.equal(query.scanSync("SELECT 1;\n".repeat(1000000)).tokens.length, 3000000); assert.throws(() => query.scanSync("SELECT 1;\n".repeat(2000000)), /out of WASM memory/); }); + + // About 50 MB (18.6M tokens) used to make libpg_query write the token protobuf + // through a NULL pointer, abort, and break the next sync call. Runs in a child + // process because the abort corrupts the instance. + it("throws out of WASM memory at 50 MB, then sync and async calls work", () => { + const script = `const query = require(${JSON.stringify(require.resolve("../"))}); + (async () => { + await query.parse("SELECT 1"); + let message = "no error"; + try { + query.scanSync("SELECT a, b FROM t WHERE c = 1;\\n".repeat(1690000)); + } catch (error) { + message = error.message; + } + const sync = query.scanSync("SELECT 1").tokens.length; + const async = (await query.scan("SELECT 1")).tokens.length; + process.stdout.write("\\nresult=" + JSON.stringify({ message, sync, async })); + })();`; + const output = execFileSync(process.execPath, ["-e", script], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + maxBuffer: 256 * 1024 * 1024, + }); + const result = JSON.parse(output.match(/^result=(.*)$/m)[1]); + assert.match(result.message, /out of WASM memory/); + assert.equal(result.sync, 2); + assert.equal(result.async, 2); + }); }); describe("scan control characters", () => {