Skip to content

Commit e193a84

Browse files
mcollinajuanarbol
authored andcommitted
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 b7faac4 commit e193a84

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
@@ -509,8 +509,10 @@ class BackupJob : public ThreadPoolWork {
509509
TryCatch try_catch(env()->isolate());
510510
USE(fn->Call(env()->context(), Null(env()->isolate()), 1, argv));
511511
if (try_catch.HasCaught()) {
512+
Local<Value> exception = try_catch.Exception();
512513
Finalize();
513-
resolver->Reject(env()->context(), try_catch.Exception()).ToChecked();
514+
resolver->Reject(env()->context(), exception).ToChecked();
515+
delete this;
514516
return;
515517
}
516518
}
@@ -529,11 +531,15 @@ class BackupJob : public ThreadPoolWork {
529531
resolver
530532
->Resolve(env()->context(), Integer::New(env()->isolate(), total_pages))
531533
.ToChecked();
534+
delete this;
532535
}
533536

534537
void Finalize() {
535538
Cleanup();
536-
source_->RemoveBackup(this);
539+
if (source_) {
540+
source_->RemoveBackup(this);
541+
source_.reset();
542+
}
537543
}
538544

539545
void Cleanup() {
@@ -554,28 +560,32 @@ class BackupJob : public ThreadPoolWork {
554560
Local<Object> e;
555561
if (!CreateSQLiteError(env()->isolate(), dest_).ToLocal(&e)) {
556562
Finalize();
563+
delete this;
557564
return;
558565
}
559566

560567
Finalize();
561568
resolver->Reject(env()->context(), e).ToChecked();
569+
delete this;
562570
}
563571

564572
void HandleBackupError(Local<Promise::Resolver> resolver, int errcode) {
565573
Local<Object> e;
566574
if (!CreateSQLiteError(env()->isolate(), errcode).ToLocal(&e)) {
567575
Finalize();
576+
delete this;
568577
return;
569578
}
570579

571580
Finalize();
572581
resolver->Reject(env()->context(), e).ToChecked();
582+
delete this;
573583
}
574584

575585
Environment* env() const { return env_; }
576586

577587
Environment* env_;
578-
DatabaseSync* source_;
588+
BaseObjectPtr<DatabaseSync> source_;
579589
Global<Promise::Resolver> resolver_;
580590
Global<Function> progressFunc_;
581591
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)