From d66ac81a0065de07bb79c437eed350c8b2fb5082 Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Fri, 18 Sep 2026 08:59:51 -0400 Subject: [PATCH 1/2] Fix use after free in commit hook callback --- cpp/OPDatabase.cpp | 12 ++++-- example/src/tests/hooks.ts | 76 ++++++++++++++++++++++++++++++++++++++ package.json | 5 ++- 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/cpp/OPDatabase.cpp b/cpp/OPDatabase.cpp index ac098ca2..23da58ea 100644 --- a/cpp/OPDatabase.cpp +++ b/cpp/OPDatabase.cpp @@ -79,8 +79,10 @@ 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); + } }); } @@ -88,8 +90,10 @@ 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); + } }); } diff --git a/example/src/tests/hooks.ts b/example/src/tests/hooks.ts index 4e6cfd97..1b623dc2 100644 --- a/example/src/tests/hooks.ts +++ b/example/src/tests/hooks.ts @@ -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); + }); }); diff --git a/package.json b/package.json index 23a1fec9..12343d18 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", From 4d5434becb4094eb270628bcff1ea77d53e9edec Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Fri, 18 Sep 2026 09:00:58 -0400 Subject: [PATCH 2/2] yarn.lock --- yarn.lock | 92 ------------------------------------------------------- 1 file changed, 92 deletions(-) diff --git a/yarn.lock b/yarn.lock index 140d0e69..030d54d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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"