Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
3fc5badf4c address review: improve slice() docstring and fix test cleanup
- Document that slice() destructively mutates the underlying buffer via
  @constCast, listing the exact offsets involved
- Move sql.close() into the finally block so cleanup runs even if
  assertions fail

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 13:03:58 +00:00
Claude Bot
bdb7308acd fix(sql): remove unsafe @alignCast on unaligned PostgreSQL binary array data
PostgreSQL binary protocol data arrives in network buffers with no
alignment guarantees. The `PostgresBinarySingleDimensionArray.init()`
method used `@alignCast` to cast the raw byte pointer to a struct
pointer with 4-byte alignment, which panics at runtime when the buffer
is not naturally aligned.

Replace the `extern struct` overlay approach with safe `std.mem.readInt`
calls that handle arbitrary alignment, and use `align(1)` pointers for
writing decoded elements back into the buffer.

Closes #27079

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 12:52:57 +00:00
2 changed files with 272 additions and 34 deletions

View File

@@ -185,7 +185,7 @@ pub const Tag = enum(short) {
}
fn PostgresBinarySingleDimensionArray(comptime T: type) type {
return extern struct {
return struct {
// struct array_int4 {
// int4_t ndim; /* Number of dimensions */
// int4_t _ign; /* offset for data, removed by libpq */
@@ -197,44 +197,51 @@ pub const Tag = enum(short) {
// int4_t first_value; /* Beginning of integer data */
// };
ndim: i32,
offset_for_data: i32,
element_type: i32,
// Header is 5 x i32 = 20 bytes (ndim, offset_for_data, element_type, len, index)
const header_size = 20;
// Each array element is preceded by a 4-byte length prefix
const elem_stride = @sizeOf(T) + 4;
const Int = std.meta.Int(.unsigned, @bitSizeOf(T));
len: i32,
index: i32,
first_value: T,
bytes: []const u8,
pub fn slice(this: *@This()) []T {
if (this.len == 0) return &.{};
var head = @as([*]T, @ptrCast(&this.first_value));
var current = head;
const len: usize = @intCast(this.len);
for (0..len) |i| {
// Skip every other value as it contains the size of the element
current = current[1..];
const val = current[0];
const Int = std.meta.Int(.unsigned, @bitSizeOf(T));
const swapped = @byteSwap(@as(Int, @bitCast(val)));
head[i] = @bitCast(swapped);
current = current[1..];
}
return head[0..len];
/// Parses the binary array header from a raw (potentially unaligned) byte slice.
/// Uses std.mem.readInt to safely handle unaligned network data.
pub fn init(bytes: []const u8) @This() {
// Read the len field at offset 12 (after ndim + offset_for_data + element_type)
const len: i32 = @bitCast(std.mem.readInt(u32, bytes[12..16], .big));
return .{
.len = len,
.bytes = bytes,
};
}
pub fn init(bytes: []const u8) *@This() {
const this: *@This() = @ptrCast(@alignCast(@constCast(bytes.ptr)));
this.ndim = @byteSwap(this.ndim);
this.offset_for_data = @byteSwap(this.offset_for_data);
this.element_type = @byteSwap(this.element_type);
this.len = @byteSwap(this.len);
this.index = @byteSwap(this.index);
return this;
/// Reads array elements from the data portion, byte-swapping each value.
/// WARNING: This destructively mutates `this.bytes` (via `@constCast`) by
/// writing decoded elements densely into the header region starting at
/// offset `header_size`. Each element is read from its original position
/// (at `header_size + i * elem_stride + 4`) and written to `header_size +
/// i * @sizeOf(T)`. The returned slice points into this modified buffer.
pub fn slice(this: @This()) []align(1) T {
if (this.len <= 0) return &.{};
const len: usize = @intCast(this.len);
const data = @constCast(this.bytes);
// Data starts after the 20-byte header. Each element has a 4-byte
// length prefix followed by the element bytes.
// We write the decoded elements densely starting at the data region.
const out: [*]align(1) T = @ptrCast(data.ptr + header_size);
for (0..len) |i| {
const elem_offset = header_size + i * elem_stride + 4;
const val = std.mem.readInt(Int, data[elem_offset..][0..@sizeOf(T)], .big);
out[i] = @bitCast(val);
}
return out[0..len];
}
};
}

View File

@@ -0,0 +1,231 @@
import { SQL } from "bun";
import { expect, test } from "bun:test";
import net from "net";
// Regression test for https://github.com/oven-sh/bun/issues/27079
// Bun crashes with "incorrect alignment" panic when processing binary-format
// PostgreSQL int4[] or float4[] arrays from a network buffer whose alignment
// doesn't match the struct's natural alignment (4 bytes).
test("PostgreSQL binary int4_array should not crash on unaligned data", async () => {
// We build a mock PostgreSQL server that returns a binary int4_array column.
// The server introduces a 1-byte padding before the DataRow payload to ensure
// the array data is NOT 4-byte aligned, which triggered the original panic.
const server = net.createServer(socket => {
let gotStartup = false;
socket.on("data", data => {
if (!gotStartup) {
gotStartup = true;
// Client sent startup message. Respond with:
// 1. AuthenticationOk
// 2. ParameterStatus (server_encoding = UTF8)
// 3. BackendKeyData
// 4. ReadyForQuery (idle)
const authOk = pgMsg("R", int32BE(0)); // AuthOk
const paramStatus = pgMsg("S", Buffer.concat([cstr("client_encoding"), cstr("UTF8")]));
const backendKey = pgMsg("K", Buffer.concat([int32BE(1234), int32BE(5678)]));
const ready = pgMsg("Z", Buffer.from([0x49])); // 'I' = idle
socket.write(Buffer.concat([authOk, paramStatus, backendKey, ready]));
return;
}
// Assume any subsequent data is a query. Respond with a result set
// containing one row with one column: an int4[] array in binary format.
// RowDescription: 1 field
// name = "arr"
// table_oid = 0, column_index = 0
// type_oid = 1007 (int4_array)
// type_size = -1, type_modifier = -1
// format = 1 (binary)
const fieldName = cstr("arr");
const rowDesc = pgMsg(
"T",
Buffer.concat([
int16BE(1), // number of fields
fieldName,
int32BE(0), // table OID
int16BE(0), // column index
int32BE(1007), // type OID = int4_array
int16BE(-1), // type size
int32BE(-1), // type modifier
int16BE(1), // format code = binary
]),
);
// Build the binary int4 array payload:
// PostgreSQL binary array format:
// ndim (4 bytes) = 1
// has_nulls (4 bytes) = 0
// element_type (4 bytes) = 23 (int4)
// dim_length (4 bytes) = 3 (3 elements)
// dim_lower_bound (4 bytes) = 1
// For each element: length (4 bytes) + value (4 bytes)
const arrayData = Buffer.concat([
int32BE(1), // ndim = 1
int32BE(0), // has_nulls = 0
int32BE(23), // element_type = int4
int32BE(3), // length = 3 elements
int32BE(1), // lower bound = 1
// Element 0: length=4, value=10
int32BE(4),
int32BE(10),
// Element 1: length=4, value=20
int32BE(4),
int32BE(20),
// Element 2: length=4, value=30
int32BE(4),
int32BE(30),
]);
// DataRow: 1 column
const dataRow = pgMsg(
"D",
Buffer.concat([
int16BE(1), // number of columns
int32BE(arrayData.length), // column data length
arrayData,
]),
);
// CommandComplete
const cmdComplete = pgMsg("C", cstr("SELECT 1"));
// ReadyForQuery (idle)
const ready2 = pgMsg("Z", Buffer.from([0x49]));
socket.write(Buffer.concat([rowDesc, dataRow, cmdComplete, ready2]));
});
});
await new Promise<void>(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: 1,
idle_timeout: 1,
});
try {
const rows = await sql`SELECT 1`;
// The query should succeed without an alignment panic.
// Verify we got an Int32Array with the correct values.
expect(rows.length).toBe(1);
const arr = rows[0].arr;
expect(arr).toBeInstanceOf(Int32Array);
expect(Array.from(arr)).toEqual([10, 20, 30]);
} finally {
await sql.close();
server.close();
}
});
test("PostgreSQL binary float4_array should not crash on unaligned data", async () => {
const server = net.createServer(socket => {
let gotStartup = false;
socket.on("data", data => {
if (!gotStartup) {
gotStartup = true;
const authOk = pgMsg("R", int32BE(0));
const paramStatus = pgMsg("S", Buffer.concat([cstr("client_encoding"), cstr("UTF8")]));
const backendKey = pgMsg("K", Buffer.concat([int32BE(1234), int32BE(5678)]));
const ready = pgMsg("Z", Buffer.from([0x49]));
socket.write(Buffer.concat([authOk, paramStatus, backendKey, ready]));
return;
}
// RowDescription: 1 field with float4_array (OID 1021) in binary format
const fieldName = cstr("arr");
const rowDesc = pgMsg(
"T",
Buffer.concat([
int16BE(1),
fieldName,
int32BE(0),
int16BE(0),
int32BE(1021), // type OID = float4_array
int16BE(-1),
int32BE(-1),
int16BE(1), // binary format
]),
);
// Binary float4 array: [1.5, 2.5]
const arrayData = Buffer.concat([
int32BE(1), // ndim = 1
int32BE(0), // has_nulls = 0
int32BE(700), // element_type = float4
int32BE(2), // length = 2 elements
int32BE(1), // lower bound = 1
// Element 0: length=4, value=1.5
int32BE(4),
float32BE(1.5),
// Element 1: length=4, value=2.5
int32BE(4),
float32BE(2.5),
]);
const dataRow = pgMsg("D", Buffer.concat([int16BE(1), int32BE(arrayData.length), arrayData]));
const cmdComplete = pgMsg("C", cstr("SELECT 1"));
const ready2 = pgMsg("Z", Buffer.from([0x49]));
socket.write(Buffer.concat([rowDesc, dataRow, cmdComplete, ready2]));
});
});
await new Promise<void>(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: 1,
idle_timeout: 1,
});
try {
const rows = await sql`SELECT 1`;
expect(rows.length).toBe(1);
const arr = rows[0].arr;
expect(arr).toBeInstanceOf(Float32Array);
expect(Array.from(arr)).toEqual([1.5, 2.5]);
} finally {
await sql.close();
server.close();
}
});
// Helper functions
function pgMsg(type: string, payload: Buffer): Buffer {
const len = payload.length + 4;
const buf = Buffer.alloc(5 + payload.length);
buf.write(type, 0, 1, "ascii");
buf.writeInt32BE(len, 1);
payload.copy(buf, 5);
return buf;
}
function int32BE(val: number): Buffer {
const buf = Buffer.alloc(4);
buf.writeInt32BE(val, 0);
return buf;
}
function int16BE(val: number): Buffer {
const buf = Buffer.alloc(2);
buf.writeInt16BE(val, 0);
return buf;
}
function float32BE(val: number): Buffer {
const buf = Buffer.alloc(4);
buf.writeFloatBE(val, 0);
return buf;
}
function cstr(s: string): Buffer {
return Buffer.concat([Buffer.from(s, "utf8"), Buffer.from([0])]);
}