Skip to content

Commit b5da751

Browse files
authored
sqlite: keep source database alive during backup
Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #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 2f5d7e7 commit b5da751

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
@@ -565,8 +565,10 @@ class BackupJob : public ThreadPoolWork {
565565
TryCatch try_catch(env()->isolate());
566566
USE(fn->Call(env()->context(), Null(env()->isolate()), 1, argv));
567567
if (try_catch.HasCaught()) {
568+
Local<Value> exception = try_catch.Exception();
568569
Finalize();
569-
resolver->Reject(env()->context(), try_catch.Exception()).ToChecked();
570+
resolver->Reject(env()->context(), exception).ToChecked();
571+
delete this;
570572
return;
571573
}
572574
}
@@ -585,11 +587,15 @@ class BackupJob : public ThreadPoolWork {
585587
resolver
586588
->Resolve(env()->context(), Integer::New(env()->isolate(), total_pages))
587589
.ToChecked();
590+
delete this;
588591
}
589592

590593
void Finalize() {
591594
Cleanup();
592-
source_->RemoveBackup(this);
595+
if (source_) {
596+
source_->RemoveBackup(this);
597+
source_.reset();
598+
}
593599
}
594600

595601
void Cleanup() {
@@ -610,28 +616,32 @@ class BackupJob : public ThreadPoolWork {
610616
Local<Object> e;
611617
if (!CreateSQLiteError(env()->isolate(), dest_).ToLocal(&e)) {
612618
Finalize();
619+
delete this;
613620
return;
614621
}
615622

616623
Finalize();
617624
resolver->Reject(env()->context(), e).ToChecked();
625+
delete this;
618626
}
619627

620628
void HandleBackupError(Local<Promise::Resolver> resolver, int errcode) {
621629
Local<Object> e;
622630
if (!CreateSQLiteError(env()->isolate(), errcode).ToLocal(&e)) {
623631
Finalize();
632+
delete this;
624633
return;
625634
}
626635

627636
Finalize();
628637
resolver->Reject(env()->context(), e).ToChecked();
638+
delete this;
629639
}
630640

631641
Environment* env() const { return env_; }
632642

633643
Environment* env_;
634-
DatabaseSync* source_;
644+
BaseObjectPtr<DatabaseSync> source_;
635645
Global<Promise::Resolver> resolver_;
636646
Global<Function> progressFunc_;
637647
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';
@@ -314,3 +315,43 @@ test('backup has correct name and length', (t) => {
314315
t.assert.strictEqual(backup.name, 'backup');
315316
t.assert.strictEqual(backup.length, 2);
316317
});
318+
319+
test('source database is kept alive while a backup is in flight', async (t) => {
320+
// Regression test: previously, BackupJob stored a raw DatabaseSync* and the
321+
// source could be garbage-collected while the backup was still running,
322+
// leading to a use-after-free when BackupJob::Finalize() dereferenced the
323+
// stale pointer via source_->RemoveBackup(this).
324+
const destDb = nextDb();
325+
326+
let database = makeSourceDb();
327+
// Insert enough rows to ensure the backup takes multiple steps.
328+
const insert = database.prepare('INSERT INTO data (key, value) VALUES (?, ?)');
329+
for (let i = 3; i <= 500; i++) {
330+
insert.run(i, 'A'.repeat(1024) + i);
331+
}
332+
333+
const p = backup(database, destDb, {
334+
rate: 1,
335+
progress() {},
336+
});
337+
// Drop the last strong JS reference to the source database. With the bug,
338+
// the DatabaseSync could be collected here and the in-flight backup would
339+
// later crash while accessing the freed source.
340+
database = null;
341+
342+
// Nudge the GC aggressively, but the backup must keep the source alive
343+
// regardless. Without the fix, the source DatabaseSync would be collected
344+
// and BackupJob::Finalize() would crash the process.
345+
for (let i = 0; i < 5; i++) {
346+
global.gc();
347+
await new Promise((resolve) => setImmediate(resolve));
348+
}
349+
350+
const totalPages = await p;
351+
t.assert.ok(totalPages > 0);
352+
353+
const backupDb = new DatabaseSync(destDb);
354+
t.after(() => { backupDb.close(); });
355+
const rows = backupDb.prepare('SELECT COUNT(*) AS n FROM data').get();
356+
t.assert.strictEqual(rows.n, 500);
357+
});

0 commit comments

Comments
 (0)