From 61fffa5c5a84636e15bf936baa117ab70a55d0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 22 Sep 2026 13:48:30 -0300 Subject: [PATCH] sqlite: throw on oversized string values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SQLite serves TEXT up to SQLITE_MAX_LENGTH, past what V8 can represent as a string. V8 returns an empty handle without throwing, and the user-defined function path then suppressed the SQLite error as if a JavaScript exception were pending. exec() reported success for a statement that never ran, and get() returned undefined. Throw ERR_STRING_TOO_LONG when the value cannot be converted, and suppress a SQLite error only when an exception is actually pending. Assisted-by: Claude Code Signed-off-by: Guilherme Araújo --- src/node_sqlite.cc | 13 ++++++++++- test/parallel/test-sqlite-statement.js | 30 +++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 3234d9881b51..e73bbf12cd50 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -73,6 +73,13 @@ using v8::Value; inline MaybeLocal Utf8StringMaybeOneByte(Isolate* isolate, std::string_view input) { + // SQLITE_MAX_LENGTH exceeds String::kMaxLength, and V8 returns an empty + // handle without throwing. Raise the error here or the value is dropped. + if (input.size() > static_cast(String::kMaxLength)) [[unlikely]] { + isolate->ThrowException(node::ERR_STRING_TOO_LONG(isolate)); + return MaybeLocal(); + } + const int len = static_cast(input.size()); if (simdutf::validate_ascii(input.data(), input.size())) { return String::NewFromOneByte( @@ -351,7 +358,11 @@ class Database; inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, Database* db) { if (db->ShouldIgnoreSQLiteError()) { db->SetIgnoreNextSQLiteError(false); - return; + // Suppression that swallows no pending exception would also swallow the + // SQLite error, reporting a failed statement as a success. + if (isolate->HasPendingException()) { + return; + } } Local e; diff --git a/test/parallel/test-sqlite-statement.js b/test/parallel/test-sqlite-statement.js index 8fac6f4ff501..d06b9018bf8e 100644 --- a/test/parallel/test-sqlite-statement.js +++ b/test/parallel/test-sqlite-statement.js @@ -1,8 +1,9 @@ // Flags: --expose-gc 'use strict'; -const { skipIfSQLiteMissing } = require('../common'); +const { enoughTestMem, skipIfSQLiteMissing } = require('../common'); skipIfSQLiteMissing(); const { Database, Statement } = require('node:sqlite'); +const { constants } = require('node:buffer'); const { suite, test } = require('node:test'); suite('Statement() constructor', () => { @@ -1442,3 +1443,30 @@ suite('options.persistent', () => { t.assert.deepStrictEqual(stmt.get(), { __proto__: null, val: 42n }); }); }); + +suite('values larger than the maximum string length', { skip: !enoughTestMem }, () => { + // hex() doubles its input, so this is the smallest blob whose text form + // exceeds what V8 can hold in a string. + const blobSize = (constants.MAX_STRING_LENGTH >>> 1) + 1; + const tooLong = { code: 'ERR_STRING_TOO_LONG', name: 'Error' }; + + test('get() throws instead of returning undefined', (t) => { + using db = new Database(':memory:'); + using stmt = db.prepare('SELECT hex(zeroblob(?))'); + t.assert.throws(() => { + stmt.get(blobSize); + }, tooLong); + }); + + test('exec() surfaces the error from a user-defined function', (t) => { + using db = new Database(':memory:'); + db.exec('CREATE TABLE data(val TEXT)'); + db.function('identity', (val) => val); + + t.assert.throws(() => { + db.exec(`INSERT INTO data (val) VALUES (identity(hex(zeroblob(${blobSize}))))`); + }, tooLong); + using stmt = db.prepare('SELECT count(*) AS count FROM data'); + t.assert.deepStrictEqual(stmt.get(), { __proto__: null, count: 0 }); + }); +});