Summary
Following up on #591 (which fixed argon2 / bcrypt FFI by flipping NATIVE_MODULE_TABLE entries from NA_F64 to NA_STR — landed in v0.5.699): a follow-on regression now crashes the runtime when a parameterized INSERT carrying a Buffer parameter follows a SELECT on the same Pool. The standalone form (single direct pool.query) works; routing through a small exec(sql, params) helper that calls getPool().query(sql, params) segfaults.
Process exits with code 139 (SIGSEGV). No JS-level error; nothing in stderr beyond the trace logs we placed.
Reproducer
// repro.ts
import { Pool } from "@perryts/mysql";
import * as crypto from "crypto";
let pool: Pool | null = null;
function initPool(dsn: string) { pool = new Pool({ url: dsn }); }
async function exec(sql: string, params: unknown[] = []): Promise<void> {
await pool!.query(sql, params);
}
async function query(sql: string, params: unknown[] = []): Promise<unknown[]> {
const r = await pool!.query(sql, params);
return r.rows;
}
async function main() {
initPool("mysql://root:@127.0.0.1:3306/shopadmin");
await exec("DROP TABLE IF EXISTS cryptoKeys");
await exec(`CREATE TABLE cryptoKeys (
kid VARCHAR(64) NOT NULL,
algorithm VARCHAR(32) NOT NULL,
wrappedKey VARBINARY(2048) NOT NULL,
status ENUM('active','retired') NOT NULL,
createdAt DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (kid)
) ENGINE=InnoDB`);
// SELECT first (returns 0 rows on fresh table).
const rows = await query("SELECT kid FROM cryptoKeys LIMIT 1");
console.log("rows.length=" + rows.length);
// Build a 64-byte Buffer. Same pattern as `Buffer.concat([dek, mac])`.
const dek = crypto.randomBytes(32);
const mac = crypto.createHmac("sha256", crypto.randomBytes(32)).update(dek).digest();
const wrapped = Buffer.concat([dek, mac]);
console.log("wrapped.length=" + wrapped.length); // 64
// INSERT with the Buffer as a parameter — segfaults here.
await exec(
"INSERT INTO cryptoKeys (kid, algorithm, wrappedKey, status) VALUES (?, 'aes-256-gcm', ?, 'active')",
["dek-1", wrapped],
);
console.log("inserted ok"); // never reached
}
main().catch((e) => { console.error("FATAL:", e); process.exit(2); });
$ perry compile repro.ts -o repro && ./repro
rows.length=0
wrapped.length=64
$ echo "exit: $?"
exit: 139
A single-statement reproducer without the SELECT-then-INSERT pattern, and without the indirection through the exec helper, runs fine:
// inline pool.query, no helper, no preceding SELECT — works
const pool = new Pool({...});
await pool.query("CREATE TABLE probe (kid VARCHAR(64), body VARBINARY(2048))");
await pool.query("INSERT INTO probe (kid, body) VALUES (?, ?)", ["k", buf]); // ok
So the segfault appears tied to the combination of (a) a small wrapper that re-invokes pool.query from a different stack frame, (b) a prior parameterized SELECT on the same Pool, and (c) a Buffer parameter to the subsequent INSERT.
Discovery context
Hit this in our app's boot path:
[1/6] init MySQL pool ✓
[2/6] run migrations ✓ (5/5 applied)
applied: 0001_init.sql ... 0005_shop_timezone.sql
[3/6] ensure active crypto KID
[trace] ensureActiveDek: A getActiveCryptoKey ✓ (returns null on fresh DB)
[trace] ensureActiveDek: B existing=no
[crypto] WARNING: Perry stdlib lacks AES; ...
[trace] ensureActiveDek: C build kid ✓
[trace] ensureActiveDek: D kid=dek-... ✓
[trace] ensureActiveDek: E dek len=32 ✓
[trace] ensureActiveDek: F wrapped len=64 ✓
✗ (SIGSEGV before G)
The await insertCryptoKey(kid, wrapped) on the next line never logs G ("inserted") — the runtime is gone.
The exact same code path runs cleanly under tsx server/main.ts (24 e2e suites green).
Environment
perry 0.5.706
@perryts/mysql 0.1.3
--target macos, arm64
- MySQL 9.6
Why we filed
Last gap blocking the native server binary deploy after #591's argon2 fix landed. With #591 the binary boots through migrations and the crypto KID step if there's already an active key in cryptoKeys (existing branch — uses cache, no INSERT). On a fresh DB the boot dies at the first INSERT into cryptoKeys. Same symptom blocks any boot path that does parameterized INSERT with a Buffer following a SELECT through a wrapper — which is most of our auth/session/idempotency code.
Summary
Following up on #591 (which fixed argon2 / bcrypt FFI by flipping NATIVE_MODULE_TABLE entries from NA_F64 to NA_STR — landed in v0.5.699): a follow-on regression now crashes the runtime when a parameterized INSERT carrying a
Bufferparameter follows a SELECT on the same Pool. The standalone form (single directpool.query) works; routing through a smallexec(sql, params)helper that callsgetPool().query(sql, params)segfaults.Process exits with code 139 (SIGSEGV). No JS-level error; nothing in stderr beyond the trace logs we placed.
Reproducer
A single-statement reproducer without the SELECT-then-INSERT pattern, and without the indirection through the
exechelper, runs fine:So the segfault appears tied to the combination of (a) a small wrapper that re-invokes
pool.queryfrom a different stack frame, (b) a prior parameterized SELECT on the same Pool, and (c) a Buffer parameter to the subsequent INSERT.Discovery context
Hit this in our app's boot path:
The
await insertCryptoKey(kid, wrapped)on the next line never logs G ("inserted") — the runtime is gone.The exact same code path runs cleanly under
tsx server/main.ts(24 e2e suites green).Environment
perry 0.5.706@perryts/mysql 0.1.3--target macos, arm64Why we filed
Last gap blocking the native server binary deploy after #591's argon2 fix landed. With #591 the binary boots through migrations and the crypto KID step if there's already an active key in
cryptoKeys(existing branch — uses cache, no INSERT). On a fresh DB the boot dies at the first INSERT intocryptoKeys. Same symptom blocks any boot path that does parameterized INSERT with a Buffer following a SELECT through a wrapper — which is most of our auth/session/idempotency code.