When inserting a Buffer into a BLOB, BINARY, or VARBINARY column via bindParameters (1-D), bindParametersSync, or the legacy 2-D bindParam, the driver silently overwrites the caller's input Buffer after SQLExecute. For larger payloads the overwrite also overruns the caller's allocation and the Node.js process dies with SIGSEGV / SIGILL.
This affects any application that keeps a reference to a Buffer it just inserted (common — e.g. computing a checksum, retrying on failure, logging) and any application inserting a BLOB at roughly 1 MiB or above.
- Node.js version: v22.22.1
- idb-connector version: 1.2.19
- IBM i version: 5.7
To Reproduce
Minimal standalone script using idb-connector. Creates a BLOBREPRO table in the user's default schema and inserts BLOBs of increasing size, printing the first 16 bytes of the caller's Buffer before and after each INSERT.
const { dbconn, dbstmt } = require('idb-connector');
const TABLE = 'BLOBREPRO';
const SIZES = [
['8 B', 8], ['32 B', 32], ['128 B', 128], ['512 B', 512],
['1 KiB', 1024], ['64 KiB', 65536], ['1 MiB', 1048576],
['2 MiB', 2097152], ['4 MiB', 4194304], ['8 MiB', 8388608],
];
function execParam(conn, sql, params) {
return new Promise((resolve, reject) => {
const s = new dbstmt(conn);
s.prepare(sql, (err) => {
if (err) { s.close(); return reject(err); }
s.bindParameters(params, (err) => {
if (err) { s.close(); return reject(err); }
s.execute((_out, err) => {
s.close();
if (err) return reject(err);
resolve();
});
});
});
});
}
async function main() {
const conn = new dbconn();
conn.conn('*LOCAL');
const setup = new dbstmt(conn);
setup.execSync(`CREATE OR REPLACE TABLE ${TABLE} (ID INTEGER NOT NULL, DATA BLOB(256M))`);
setup.close();
try {
for (const [label, size] of SIZES) {
const payload = Buffer.alloc(size, 0xAB);
const before = payload.subarray(0, 16).toString('hex');
await execParam(conn, `DELETE FROM ${TABLE} WHERE ID = ? WITH NONE`, [1]);
await execParam(conn, `INSERT INTO ${TABLE} (ID, DATA) VALUES (?, ?) WITH NONE`, [1, payload]);
const after = payload.subarray(0, 16).toString('hex');
const verdict = after === before ? 'unchanged' : 'MUTATED';
console.log(`${label}: before=${before} after=${after} [${verdict}]`);
}
} finally {
try { conn.disconn(); } catch (_) {}
try { conn.close(); } catch (_) {}
}
}
main().catch((err) => { console.error(err); process.exit(1); });
Output (representative run)
8 B: before=abababababababab after=0030333033333330 [MUTATED]
32 B: before=abababababababababababababababab after=00303330333333303333333333333330 [MUTATED]
128 B: before=abababababababababababababababab after=00000001807296c00000000000000060 [MUTATED]
512 B: before=abababababababababababababababab after=000000018072bbc00000000000000060 [MUTATED]
1 KiB: before=abababababababababababababababab after=000000018072e4600000000000000060 [MUTATED]
64 KiB: before=abababababababababababababababab after=00000000000000000000000000000000 [MUTATED]
1 MiB: before=abababababababababababababababab after=00000000000000000000000000000000 [MUTATED]
Illegal instruction (core dumped)
Determinism across five runs: mutation occurs at every size reached — never observed a clean "unchanged" row. The process always dies between 64 KiB and 2 MiB. The exact crash point (immediately after 64 KiB vs. after 1 MiB) and the signal (SIGSEGV ~3/5, SIGILL ~2/5) vary run-to-run depending on heap layout, but both symptoms are reliably present.
The after values are not random garbage: 0030333033333330 is EBCDIC-encoded digits (CLI length-indicator metadata), and 00000001807… are live 64-bit PPC heap pointers from the current process. CLI is writing its output area through the caller's Buffer.
Root Cause
In src/db2ia/dbstmt.cc, the BLOB/BINARY/VARBINARY branches of bindParams set param[i].buf directly to the result of buffer.Data() — a pointer into V8-owned heap — then pass that pointer to SQLBindParameter with BufferLength = 0:
- 1-D
bindParameters path: lines ~2722–2745
- 2-D legacy
bindParam path: lines ~2653–2663
Because 1-D bindParameters also binds every parameter as SQL_PARAM_INPUT_OUTPUT (line ~2673), CLI treats the caller's Buffer as an output location and writes back through it after execute. CLI does not honour BufferLength for binary types, so the write-back extent is bounded only by the declared column size — which for a BLOB(256M) column is vastly larger than any realistic input Buffer.
The CHAR / CLOB branches at lines ~2747–2790 already handle this correctly: they calloc an owned buffer sized to max(paramSize, inputLen) and strcpy the input in. The binary branches do not, and that is the defect.
Secondary consequence: because param[i].buf is set to a non-malloc'd pointer, the subsequent free(param[i].buf) in freeSp() (line ~2527) is undefined behaviour — a latent corruption source on top of the write-back bug.
Affects any caller binding a Buffer to a binary SQL type:
bindParameters(...) / bindParametersSync(...) (1-D) — BLOB, BINARY, VARBINARY
bindParam(...) / bindParamSync(...) (2-D) — with [buffer, IN, BLOB] or [buffer, IN, BINARY]
Downstream projects (idb-pconnector and anything using it) inherit the bug.
Proposed approach
Copy the caller's Buffer into an owned allocation sized for the declared column before binding, matching the CHAR/CLOB pattern. Happy to send a PR once this issue is triaged.
When inserting a
Bufferinto aBLOB,BINARY, orVARBINARYcolumn viabindParameters(1-D),bindParametersSync, or the legacy 2-DbindParam, the driver silently overwrites the caller's inputBufferafterSQLExecute. For larger payloads the overwrite also overruns the caller's allocation and the Node.js process dies with SIGSEGV / SIGILL.This affects any application that keeps a reference to a
Bufferit just inserted (common — e.g. computing a checksum, retrying on failure, logging) and any application inserting a BLOB at roughly 1 MiB or above.To Reproduce
Minimal standalone script using
idb-connector. Creates aBLOBREPROtable in the user's default schema and inserts BLOBs of increasing size, printing the first 16 bytes of the caller'sBufferbefore and after each INSERT.Output (representative run)
Determinism across five runs: mutation occurs at every size reached — never observed a clean "unchanged" row. The process always dies between 64 KiB and 2 MiB. The exact crash point (immediately after 64 KiB vs. after 1 MiB) and the signal (SIGSEGV ~3/5, SIGILL ~2/5) vary run-to-run depending on heap layout, but both symptoms are reliably present.
The
aftervalues are not random garbage:0030333033333330is EBCDIC-encoded digits (CLI length-indicator metadata), and00000001807…are live 64-bit PPC heap pointers from the current process. CLI is writing its output area through the caller'sBuffer.Root Cause
In
src/db2ia/dbstmt.cc, the BLOB/BINARY/VARBINARY branches ofbindParamssetparam[i].bufdirectly to the result ofbuffer.Data()— a pointer into V8-owned heap — then pass that pointer toSQLBindParameterwithBufferLength = 0:bindParameterspath: lines ~2722–2745bindParampath: lines ~2653–2663Because 1-D
bindParametersalso binds every parameter asSQL_PARAM_INPUT_OUTPUT(line ~2673), CLI treats the caller'sBufferas an output location and writes back through it after execute. CLI does not honourBufferLengthfor binary types, so the write-back extent is bounded only by the declared column size — which for aBLOB(256M)column is vastly larger than any realistic inputBuffer.The
CHAR/CLOBbranches at lines ~2747–2790 already handle this correctly: theycallocan owned buffer sized tomax(paramSize, inputLen)andstrcpythe input in. The binary branches do not, and that is the defect.Secondary consequence: because
param[i].bufis set to a non-malloc'd pointer, the subsequentfree(param[i].buf)infreeSp()(line ~2527) is undefined behaviour — a latent corruption source on top of the write-back bug.Affects any caller binding a
Bufferto a binary SQL type:bindParameters(...)/bindParametersSync(...)(1-D) — BLOB, BINARY, VARBINARYbindParam(...)/bindParamSync(...)(2-D) — with[buffer, IN, BLOB]or[buffer, IN, BINARY]Downstream projects (
idb-pconnectorand anything using it) inherit the bug.Proposed approach
Copy the caller's
Bufferinto an owned allocation sized for the declared column before binding, matching theCHAR/CLOBpattern. Happy to send a PR once this issue is triaged.