From 09c56c8ba8517e15fc9633332fe3db7a46f83339 Mon Sep 17 00:00:00 2001 From: robobun Date: Wed, 10 Sep 2025 18:47:50 -0700 Subject: [PATCH] Fix PostgreSQL StringBuilder assertion failure with empty error messages (#22558) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Fixed a debug build assertion failure in PostgreSQL error handling when all error message fields are empty - Added safety check before calling `StringBuilder.allocatedSlice()` to handle zero-length messages - Added regression test to prevent future occurrences ## The Problem When PostgreSQL sends an error response with completely empty message fields, the `ErrorResponse.toJS` function would: 1. Calculate `b.cap` but end up with `b.len = 0` (no actual content) 2. Call `b.allocatedSlice()[0..b.len]` unconditionally 3. Trigger an assertion in `StringBuilder.allocatedSlice()` that requires `cap > 0` This only affected debug builds since the assertion is compiled out in release builds. ## The Fix Check if `b.len > 0` before calling `allocatedSlice()`. If there's no content, use an empty string instead. ## Test Plan - [x] Added regression test that triggers the exact crash scenario - [x] Verified test crashes without the fix (debug build) - [x] Verified test passes with the fix - [x] Confirmed release builds were not affected 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/sql/postgres/protocol/ErrorResponse.zig | 4 +- ...stringbuilder-assertion-aggressive.test.ts | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 test/regression/issue/postgres-stringbuilder-assertion-aggressive.test.ts diff --git a/src/sql/postgres/protocol/ErrorResponse.zig b/src/sql/postgres/protocol/ErrorResponse.zig index ee2d5b7ee4..64d13549bf 100644 --- a/src/sql/postgres/protocol/ErrorResponse.zig +++ b/src/sql/postgres/protocol/ErrorResponse.zig @@ -132,7 +132,9 @@ pub fn toJS(this: ErrorResponse, globalObject: *jsc.JSGlobalObject) JSValue { const line_slice = if (line.isEmpty()) null else line.byteSlice(); const routine_slice = if (routine.isEmpty()) null else routine.byteSlice(); - return createPostgresError(globalObject, b.allocatedSlice()[0..b.len], .{ + const error_message = if (b.len > 0) b.allocatedSlice()[0..b.len] else ""; + + return createPostgresError(globalObject, error_message, .{ .code = error_code, .errno = errno, .detail = detail_slice, diff --git a/test/regression/issue/postgres-stringbuilder-assertion-aggressive.test.ts b/test/regression/issue/postgres-stringbuilder-assertion-aggressive.test.ts new file mode 100644 index 0000000000..991d85447d --- /dev/null +++ b/test/regression/issue/postgres-stringbuilder-assertion-aggressive.test.ts @@ -0,0 +1,46 @@ +import { SQL } from "bun"; +import { expect, test } from "bun:test"; +import net from "net"; + +test("PostgreSQL StringBuilder assertion - aggressive empty error test", async () => { + const server = net.createServer(socket => { + // Immediately send an error response with completely empty fields + // This is more likely to trigger the assertion + socket.write(createPostgresPacket("E", Buffer.from("\0"))); // Terminator only + socket.destroy(); + }); + + await new Promise(r => server.listen(0, "127.0.0.1", () => r())); + const port = (server.address() as net.AddressInfo).port; + + const sql = new SQL({ + url: `postgres://test@127.0.0.1:${port}/test`, + max: 10, + connection_timeout: 0.1, + }); + + const promises = []; + for (let i = 0; i < 20; i++) { + promises.push( + sql`SELECT ${i}`.catch(err => { + // We expect errors, just checking for crashes + return null; + }), + ); + } + + await Promise.all(promises); + await sql.close(); + server.close(); + + // If we get here without crashing, the test passes + expect(true).toBe(true); +}); + +function createPostgresPacket(type: string, data: Buffer): Buffer { + const len = data.length + 4; + const header = Buffer.alloc(5); + header.write(type, 0); + header.writeInt32BE(len, 1); + return Buffer.concat([header, data]); +}