Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions cpp/OPDatabase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,21 @@ void OPDatabase::on_commit() {
if (alive != nullptr && !alive->load()) {
return;
}
invoker->invokeAsync([this](jsi::Runtime &rt) {
commit_hook_callback->asObject(rt).asFunction(rt).call(rt);
invoker->invokeAsync([callback = commit_hook_callback](jsi::Runtime &rt) {
if (callback != nullptr) {
callback->asObject(rt).asFunction(rt).call(rt);
}
});
}

void OPDatabase::on_rollback() {
if (alive != nullptr && !alive->load()) {
return;
}
invoker->invokeAsync([this](jsi::Runtime &rt) {
rollback_hook_callback->asObject(rt).asFunction(rt).call(rt);
invoker->invokeAsync([callback = rollback_hook_callback](jsi::Runtime &rt) {
if (callback != nullptr) {
callback->asObject(rt).asFunction(rt).call(rt);
}
});
}

Expand Down
76 changes: 76 additions & 0 deletions example/src/tests/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,80 @@ describe("Hooks", () => {

expect(hookRes.length).toEqual(1);
});

// Regression test for a use-after-free: on_commit()/on_rollback() used to
// capture raw `this` in the invoker->invokeAsync lambda and read
// commit_hook_callback/rollback_hook_callback (members of OPDatabase) when
// the lambda finally ran on the JS thread. close()/delete() only drain the
// native thread pool, not the invoker queue, so a db freed right after the
// hook fires but before the queued callback runs would leave that lambda
// dereferencing a dangling OPDatabase. Closing/deleting immediately after
// triggering the hook - without waiting for its callback to run - recreates
// that race; the test passing (instead of crashing the process) is the
// assertion.
it("does not crash when db is closed immediately after commit hook fires", async () => {
const raceDb = open({ name: "commitHookTeardownRace.sqlite" });

raceDb.executeSync(
"CREATE TABLE IF NOT EXISTS User (id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL) STRICT;",
);

raceDb.commitHook(() => {});

raceDb.executeSync(
'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)',
[
chance.integer(),
chance.name(),
chance.integer(),
chance.floating(),
],
);

// The commit hook callback above is now queued on the JS invoker but
// has not run yet.
raceDb.close();
raceDb.delete();

// Give the JS thread a chance to run the still-queued callback.
await sleep(50);

expect(true).toEqual(true);
});

it("does not crash when db is closed immediately after rollback hook fires", async () => {
const raceDb = open({ name: "rollbackHookTeardownRace.sqlite" });

raceDb.executeSync(
"CREATE TABLE IF NOT EXISTS User (id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL) STRICT;",
);

raceDb.rollbackHook(() => {});

// Drive BEGIN/INSERT/ROLLBACK directly through executeSync (the same
// statements db.transaction() issues internally) so the rollback hook
// fires synchronously, in-line, with no `await` giving the JS thread a
// chance to drain the invoker queue before close()/delete() below.
raceDb.executeSync("BEGIN TRANSACTION;");
raceDb.executeSync(
'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)',
[
chance.integer(),
chance.name(),
chance.integer(),
chance.floating(),
],
);
raceDb.executeSync("ROLLBACK;");

// The rollback hook callback above is now queued on the JS invoker but
// has not run yet.
raceDb.close();
raceDb.delete();

// Give the JS thread a chance to run the still-queued callback.
await sleep(50);

expect(true).toEqual(true);
});
});
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@
"build:node": "yarn workspace node build",
"build:turso": "./scripts/build-turso-binaries.sh",
"pods": "cd example && yarn pods",
"clang-format-check": "clang-format -i cpp/*.cpp cpp/*.h"
"clang-format-check": "clang-format -i cpp/*.cpp cpp/*.h",
"lint": "oxlint",
"format": "oxfmt ."
},
"keywords": [
"react-native",
Expand All @@ -73,7 +75,6 @@
"registry": "https://registry.npmjs.org/"
},
"devDependencies": {
"@biomejs/biome": "^2.4.10",
"@sqlite.org/sqlite-wasm": "^3.51.2-build8",
"@types/better-sqlite3": "^7.6.13",
"@types/jest": "^30.0.0",
Expand Down
92 changes: 0 additions & 92 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1523,97 +1523,6 @@ __metadata:
languageName: node
linkType: hard

"@biomejs/biome@npm:^2.4.10":
version: 2.4.10
resolution: "@biomejs/biome@npm:2.4.10"
dependencies:
"@biomejs/cli-darwin-arm64": "npm:2.4.10"
"@biomejs/cli-darwin-x64": "npm:2.4.10"
"@biomejs/cli-linux-arm64": "npm:2.4.10"
"@biomejs/cli-linux-arm64-musl": "npm:2.4.10"
"@biomejs/cli-linux-x64": "npm:2.4.10"
"@biomejs/cli-linux-x64-musl": "npm:2.4.10"
"@biomejs/cli-win32-arm64": "npm:2.4.10"
"@biomejs/cli-win32-x64": "npm:2.4.10"
dependenciesMeta:
"@biomejs/cli-darwin-arm64":
optional: true
"@biomejs/cli-darwin-x64":
optional: true
"@biomejs/cli-linux-arm64":
optional: true
"@biomejs/cli-linux-arm64-musl":
optional: true
"@biomejs/cli-linux-x64":
optional: true
"@biomejs/cli-linux-x64-musl":
optional: true
"@biomejs/cli-win32-arm64":
optional: true
"@biomejs/cli-win32-x64":
optional: true
bin:
biome: bin/biome
checksum: 10c0/80d10d5e6fa41a24efb9020ee73b79b0aca46942b55ea96e880c3bb45ea14c71e49fb1be9f134bee23b2d940bb8cad51a70351ca051e09a43613018dba693bd6
languageName: node
linkType: hard

"@biomejs/cli-darwin-arm64@npm:2.4.10":
version: 2.4.10
resolution: "@biomejs/cli-darwin-arm64@npm:2.4.10"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard

"@biomejs/cli-darwin-x64@npm:2.4.10":
version: 2.4.10
resolution: "@biomejs/cli-darwin-x64@npm:2.4.10"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard

"@biomejs/cli-linux-arm64-musl@npm:2.4.10":
version: 2.4.10
resolution: "@biomejs/cli-linux-arm64-musl@npm:2.4.10"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard

"@biomejs/cli-linux-arm64@npm:2.4.10":
version: 2.4.10
resolution: "@biomejs/cli-linux-arm64@npm:2.4.10"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard

"@biomejs/cli-linux-x64-musl@npm:2.4.10":
version: 2.4.10
resolution: "@biomejs/cli-linux-x64-musl@npm:2.4.10"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard

"@biomejs/cli-linux-x64@npm:2.4.10":
version: 2.4.10
resolution: "@biomejs/cli-linux-x64@npm:2.4.10"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard

"@biomejs/cli-win32-arm64@npm:2.4.10":
version: 2.4.10
resolution: "@biomejs/cli-win32-arm64@npm:2.4.10"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard

"@biomejs/cli-win32-x64@npm:2.4.10":
version: 2.4.10
resolution: "@biomejs/cli-win32-x64@npm:2.4.10"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard

"@esbuild/aix-ppc64@npm:0.27.7":
version: 0.27.7
resolution: "@esbuild/aix-ppc64@npm:0.27.7"
Expand Down Expand Up @@ -2272,7 +2181,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "@op-engineering/op-sqlite@workspace:."
dependencies:
"@biomejs/biome": "npm:^2.4.10"
"@sqlite.org/sqlite-wasm": "npm:^3.51.2-build8"
"@types/better-sqlite3": "npm:^7.6.13"
"@types/jest": "npm:^30.0.0"
Expand Down
Loading