diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 76accf1c2731..712381cf3c79 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -2681,12 +2681,11 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo& args) { return; } - // A callback may detach/modify the input buffer mid-apply, so copy it. - // With no callbacks, no JS runs during sqlite3changeset_apply(), so no - // copy is needed. + // SQLite may invoke JavaScript through explicit callbacks or user-defined + // SQL functions while applying the changeset. Copy the input so JavaScript + // cannot detach or modify the memory while SQLite is still reading it. std::unique_ptr changeset; - if (buf.length() > 0 && - (context.filterCallback || context.conflictCallback)) { + if (buf.length() > 0) { changeset = ArrayBuffer::NewBackingStore( env->isolate(), buf.length(), diff --git a/test/parallel/test-sqlite-session.js b/test/parallel/test-sqlite-session.js index c36ed84a0858..40da13c4bb7f 100644 --- a/test/parallel/test-sqlite-session.js +++ b/test/parallel/test-sqlite-session.js @@ -462,6 +462,42 @@ test('database.applyChangeset() - changeset detached by filter', (t) => { ]); }); +test('database.applyChangeset() - changeset detached by SQL function', (t) => { + const database1 = new DatabaseSync(':memory:'); + const database2 = new DatabaseSync(':memory:'); + let changeset; + let detached = false; + + database1.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)'); + database2.function('validate_changeset_value', (value) => { + if (!detached) { + detached = true; + const transferred = structuredClone(changeset.buffer, { + transfer: [changeset.buffer], + }); + new Uint8Array(transferred).fill(0); + } + return value.length; + }); + database2.exec(` + CREATE TABLE data( + key INTEGER PRIMARY KEY, + value TEXT CHECK (validate_changeset_value(value)) + ) + `); + + const session = database1.createSession(); + database1.exec("INSERT INTO data VALUES (1, 'hello'), (2, 'world')"); + changeset = session.changeset(); + + t.assert.strictEqual(database2.applyChangeset(changeset), true); + t.assert.strictEqual(changeset.byteLength, 0); + deepStrictEqual(t)(database2.prepare('SELECT * FROM data').all(), [ + { key: 1, value: 'hello' }, + { key: 2, value: 'world' }, + ]); +}); + test('database.createSession() - filter changes', (t) => { const database1 = new DatabaseSync(':memory:'); const database2 = new DatabaseSync(':memory:');