Skip to content

Commit b9c80c9

Browse files
committed
sqlite: keep source database alive during backup
1 parent 449a93a commit b9c80c9

File tree

2 files changed

+60
-3
lines changed

2 files changed

+60
-3
lines changed

src/node_sqlite.cc

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -546,8 +546,10 @@ class BackupJob : public ThreadPoolWork {
546546
TryCatch try_catch(env()->isolate());
547547
USE(fn->Call(env()->context(), Null(env()->isolate()), 1, argv));
548548
if (try_catch.HasCaught()) {
549+
Local<Value> exception = try_catch.Exception();
549550
Finalize();
550-
resolver->Reject(env()->context(), try_catch.Exception()).ToChecked();
551+
resolver->Reject(env()->context(), exception).ToChecked();
552+
delete this;
551553
return;
552554
}
553555
}
@@ -566,11 +568,15 @@ class BackupJob : public ThreadPoolWork {
566568
resolver
567569
->Resolve(env()->context(), Integer::New(env()->isolate(), total_pages))
568570
.ToChecked();
571+
delete this;
569572
}
570573

571574
void Finalize() {
572575
Cleanup();
573-
source_->RemoveBackup(this);
576+
if (source_) {
577+
source_->RemoveBackup(this);
578+
source_.reset();
579+
}
574580
}
575581

576582
void Cleanup() {
@@ -591,28 +597,37 @@ class BackupJob : public ThreadPoolWork {
591597
Local<Object> e;
592598
if (!CreateSQLiteError(env()->isolate(), dest_).ToLocal(&e)) {
593599
Finalize();
600+
delete this;
594601
return;
595602
}
596603

597604
Finalize();
598605
resolver->Reject(env()->context(), e).ToChecked();
606+
delete this;
599607
}
600608

601609
void HandleBackupError(Local<Promise::Resolver> resolver, int errcode) {
602610
Local<Object> e;
603611
if (!CreateSQLiteError(env()->isolate(), errcode).ToLocal(&e)) {
604612
Finalize();
613+
delete this;
605614
return;
606615
}
607616

608617
Finalize();
609618
resolver->Reject(env()->context(), e).ToChecked();
619+
delete this;
610620
}
611621

612622
Environment* env() const { return env_; }
613623

614624
Environment* env_;
615-
DatabaseSync* source_;
625+
// Keep a strong reference to the source DatabaseSync so that it cannot be
626+
// garbage-collected while this backup job is still in flight. Without this,
627+
// the source object may be destroyed before Finalize() runs, leaving
628+
// source_ dangling and causing a use-after-free when Finalize() attempts
629+
// to dereference it.
630+
BaseObjectPtr<DatabaseSync> source_;
616631
Global<Promise::Resolver> resolver_;
617632
Global<Function> progressFunc_;
618633
sqlite3* dest_ = nullptr;

test/parallel/test-sqlite-backup.mjs

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

0 commit comments

Comments
 (0)