Skip to content

Commit efad0d8

Browse files
committed
sqlite: throw on oversized string values
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 <arauujogui@gmail.com>
1 parent 191a3b2 commit efad0d8

2 files changed

Lines changed: 41 additions & 2 deletions

File tree

‎src/node_sqlite.cc‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ using v8::Value;
7373

7474
inline MaybeLocal<String> Utf8StringMaybeOneByte(Isolate* isolate,
7575
std::string_view input) {
76+
// SQLITE_MAX_LENGTH exceeds String::kMaxLength, and V8 returns an empty
77+
// handle without throwing. Raise the error here or the value is dropped.
78+
if (input.size() > static_cast<size_t>(String::kMaxLength)) [[unlikely]] {
79+
isolate->ThrowException(node::ERR_STRING_TOO_LONG(isolate));
80+
return MaybeLocal<String>();
81+
}
82+
7683
const int len = static_cast<int>(input.size());
7784
if (simdutf::validate_ascii(input.data(), input.size())) {
7885
return String::NewFromOneByte(
@@ -351,7 +358,11 @@ class Database;
351358
inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, Database* db) {
352359
if (db->ShouldIgnoreSQLiteError()) {
353360
db->SetIgnoreNextSQLiteError(false);
354-
return;
361+
// Suppression that swallows no pending exception would also swallow the
362+
// SQLite error, reporting a failed statement as a success.
363+
if (isolate->HasPendingException()) {
364+
return;
365+
}
355366
}
356367

357368
Local<Object> e;

‎test/parallel/test-sqlite-statement.js‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Flags: --expose-gc
22
'use strict';
3-
const { skipIfSQLiteMissing } = require('../common');
3+
const { enoughTestMem, skipIfSQLiteMissing } = require('../common');
44
skipIfSQLiteMissing();
55
const { Database, Statement } = require('node:sqlite');
6+
const { constants } = require('node:buffer');
67
const { suite, test } = require('node:test');
78

89
suite('Statement() constructor', () => {
@@ -1442,3 +1443,30 @@ suite('options.persistent', () => {
14421443
t.assert.deepStrictEqual(stmt.get(), { __proto__: null, val: 42n });
14431444
});
14441445
});
1446+
1447+
suite('values larger than the maximum string length', { skip: !enoughTestMem }, () => {
1448+
// hex() doubles its input, so this is the smallest blob whose text form
1449+
// exceeds what V8 can hold in a string.
1450+
const blobSize = (constants.MAX_STRING_LENGTH >>> 1) + 1;
1451+
const tooLong = { code: 'ERR_STRING_TOO_LONG', name: 'Error' };
1452+
1453+
test('get() throws instead of returning undefined', (t) => {
1454+
using db = new DatabaseSync(':memory:');
1455+
using stmt = db.prepare('SELECT hex(zeroblob(?))');
1456+
t.assert.throws(() => {
1457+
stmt.get(blobSize);
1458+
}, tooLong);
1459+
});
1460+
1461+
test('exec() surfaces the error from a user-defined function', (t) => {
1462+
using db = new DatabaseSync(':memory:');
1463+
db.exec('CREATE TABLE data(val TEXT)');
1464+
db.function('identity', (val) => val);
1465+
1466+
t.assert.throws(() => {
1467+
db.exec(`INSERT INTO data (val) VALUES (identity(hex(zeroblob(${blobSize}))))`);
1468+
}, tooLong);
1469+
using stmt = db.prepare('SELECT count(*) AS count FROM data');
1470+
t.assert.deepStrictEqual(stmt.get(), { __proto__: null, count: 0 });
1471+
});
1472+
});

0 commit comments

Comments
 (0)