Skip to content

Commit 758b4d2

Browse files
mcollinajuanarbol
authored andcommitted
sqlite: keep source database alive during backup
Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: nodejs#62673 Reviewed-By: Daniel Lemire <daniel@lemire.me> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 1b4f487 commit 758b4d2

2 files changed

Lines changed: 54 additions & 3 deletions

File tree

src/node_sqlite.cc

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -511,8 +511,10 @@ class BackupJob : public ThreadPoolWork {
511511
TryCatch try_catch(env()->isolate());
512512
USE(fn->Call(env()->context(), Null(env()->isolate()), 1, argv));
513513
if (try_catch.HasCaught()) {
514+
Local<Value> exception = try_catch.Exception();
514515
Finalize();
515-
resolver->Reject(env()->context(), try_catch.Exception()).ToChecked();
516+
resolver->Reject(env()->context(), exception).ToChecked();
517+
delete this;
516518
return;
517519
}
518520
}
@@ -531,11 +533,15 @@ class BackupJob : public ThreadPoolWork {
531533
resolver
532534
->Resolve(env()->context(), Integer::New(env()->isolate(), total_pages))
533535
.ToChecked();
536+
delete this;
534537
}
535538

536539
void Finalize() {
537540
Cleanup();
538-
source_->RemoveBackup(this);
541+
if (source_) {
542+
source_->RemoveBackup(this);
543+
source_.reset();
544+
}
539545
}
540546

541547
void Cleanup() {
@@ -556,28 +562,32 @@ class BackupJob : public ThreadPoolWork {
556562
Local<Object> e;
557563
if (!CreateSQLiteError(env()->isolate(), dest_).ToLocal(&e)) {
558564
Finalize();
565+
delete this;
559566
return;
560567
}
561568

562569
Finalize();
563570
resolver->Reject(env()->context(), e).ToChecked();
571+
delete this;
564572
}
565573

566574
void HandleBackupError(Local<Promise::Resolver> resolver, int errcode) {
567575
Local<Object> e;
568576
if (!CreateSQLiteError(env()->isolate(), errcode).ToLocal(&e)) {
569577
Finalize();
578+
delete this;
570579
return;
571580
}
572581

573582
Finalize();
574583
resolver->Reject(env()->context(), e).ToChecked();
584+
delete this;
575585
}
576586

577587
Environment* env() const { return env_; }
578588

579589
Environment* env_;
580-
DatabaseSync* source_;
590+
BaseObjectPtr<DatabaseSync> source_;
581591
Global<Promise::Resolver> resolver_;
582592
Global<Function> progressFunc_;
583593
sqlite3* dest_ = nullptr;

test/parallel/test-sqlite-backup.mjs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Flags: --expose-gc
12
import { isWindows, skipIfSQLiteMissing } from '../common/index.mjs';
23
import tmpdir from '../common/tmpdir.js';
34
import { join } from 'node:path';
@@ -242,3 +243,43 @@ test('backup has correct name and length', (t) => {
242243
t.assert.strictEqual(backup.name, 'backup');
243244
t.assert.strictEqual(backup.length, 2);
244245
});
246+
247+
test('source database is kept alive while a backup is in flight', async (t) => {
248+
// Regression test: previously, BackupJob stored a raw DatabaseSync* and the
249+
// source could be garbage-collected while the backup was still running,
250+
// leading to a use-after-free when BackupJob::Finalize() dereferenced the
251+
// stale pointer via source_->RemoveBackup(this).
252+
const destDb = nextDb();
253+
254+
let database = makeSourceDb();
255+
// Insert enough rows to ensure the backup takes multiple steps.
256+
const insert = database.prepare('INSERT INTO data (key, value) VALUES (?, ?)');
257+
for (let i = 3; i <= 500; i++) {
258+
insert.run(i, 'A'.repeat(1024) + i);
259+
}
260+
261+
const p = backup(database, destDb, {
262+
rate: 1,
263+
progress() {},
264+
});
265+
// Drop the last strong JS reference to the source database. With the bug,
266+
// the DatabaseSync could be collected here and the in-flight backup would
267+
// later crash while accessing the freed source.
268+
database = null;
269+
270+
// Nudge the GC aggressively, but the backup must keep the source alive
271+
// regardless. Without the fix, the source DatabaseSync would be collected
272+
// and BackupJob::Finalize() would crash the process.
273+
for (let i = 0; i < 5; i++) {
274+
global.gc();
275+
await new Promise((resolve) => setImmediate(resolve));
276+
}
277+
278+
const totalPages = await p;
279+
t.assert.ok(totalPages > 0);
280+
281+
const backupDb = new DatabaseSync(destDb);
282+
t.after(() => { backupDb.close(); });
283+
const rows = backupDb.prepare('SELECT COUNT(*) AS n FROM data').get();
284+
t.assert.strictEqual(rows.n, 500);
285+
});

0 commit comments

Comments
 (0)