Skip to content

Commit 0e4415c

Browse files
authored
fix(rivetkit): harden sqlite-vfs lifecycle, remove getSqliteVfs (#4277)
Fix SQL injection vulnerability and improve SQLite VFS lifecycle management
1 parent 8eb31d9 commit 0e4415c

6 files changed

Lines changed: 205 additions & 86 deletions

File tree

rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/db-lifecycle.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ export const dbLifecycle = actor({
9090
ping: () => "pong",
9191
insertValue: async (c, value: string) => {
9292
await c.db.execute(
93-
`INSERT INTO lifecycle_data (value, created_at) VALUES ('${value}', ${Date.now()})`,
93+
"INSERT INTO lifecycle_data (value, created_at) VALUES (?, ?)",
94+
value,
95+
Date.now(),
9496
);
9597
},
9698
getCount: async (c) => {

rivetkit-typescript/packages/rivetkit/src/actor/driver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export interface ActorDriver {
6969
): Promise<BaseSQLiteDatabase<any,any,any,any> | undefined>;
7070

7171
/**
72-
* Returns a SQLite VFS instance for creating KV-backed databases.
72+
* Creates a SQLite VFS instance for creating KV-backed databases.
7373
* If not provided, the database provider will need an override.
7474
*
7575
* @rivetkit/sqlite's async build is not re-entrant per module instance. Drivers
@@ -78,7 +78,7 @@ export interface ActorDriver {
7878
* This is a method (not a property) so drivers can use dynamic imports,
7979
* keeping the core driver tree-shakeable from @rivetkit/sqlite.
8080
*/
81-
getSqliteVfs?(): SqliteVfs | Promise<SqliteVfs>;
81+
createSqliteVfs?(): SqliteVfs | Promise<SqliteVfs>;
8282

8383
/**
8484
* Requests the actor to go to sleep.

rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,8 +1412,8 @@ export class ActorInstance<
14121412
// Every actor gets its own SqliteVfs/@rivetkit/sqlite instance. The async
14131413
// @rivetkit/sqlite build is not re-entrant, and sharing one instance across
14141414
// actors can cause cross-actor contention and runtime corruption.
1415-
if (!this.#sqliteVfs && this.driver.getSqliteVfs) {
1416-
this.#sqliteVfs = await this.driver.getSqliteVfs();
1415+
if (!this.#sqliteVfs && this.driver.createSqliteVfs) {
1416+
this.#sqliteVfs = await this.driver.createSqliteVfs();
14171417
}
14181418

14191419
client = await this.#config.db.createClient({
@@ -1446,6 +1446,16 @@ export class ActorInstance<
14461446
});
14471447
}
14481448
}
1449+
if (this.#sqliteVfs) {
1450+
try {
1451+
await this.#sqliteVfs.destroy();
1452+
} catch (cleanupError) {
1453+
this.#rLog.error({
1454+
msg: "sqlite vfs teardown after setup failure failed",
1455+
error: stringifyError(cleanupError),
1456+
});
1457+
}
1458+
}
14491459
this.#sqliteVfs = undefined;
14501460
if (error instanceof Error) {
14511461
this.#rLog.error({
@@ -1466,23 +1476,31 @@ export class ActorInstance<
14661476

14671477
async #cleanupDatabase() {
14681478
const client = this.#db;
1479+
const sqliteVfs = this.#sqliteVfs;
1480+
const dbConfig = "db" in this.#config ? this.#config.db : undefined;
14691481
this.#db = undefined;
14701482
this.#sqliteVfs = undefined;
14711483

1472-
if (!client) {
1473-
return;
1474-
}
1475-
if (!("db" in this.#config) || !this.#config.db) {
1476-
return;
1484+
if (client && dbConfig) {
1485+
try {
1486+
await dbConfig.onDestroy?.(client);
1487+
} catch (error) {
1488+
this.#rLog.error({
1489+
msg: "database cleanup failed",
1490+
error: stringifyError(error),
1491+
});
1492+
}
14771493
}
14781494

1479-
try {
1480-
await this.#config.db.onDestroy?.(client);
1481-
} catch (error) {
1482-
this.#rLog.error({
1483-
msg: "database cleanup failed",
1484-
error: stringifyError(error),
1485-
});
1495+
if (sqliteVfs) {
1496+
try {
1497+
await sqliteVfs.destroy();
1498+
} catch (error) {
1499+
this.#rLog.error({
1500+
msg: "sqlite vfs cleanup failed",
1501+
error: stringifyError(error),
1502+
});
1503+
}
14861504
}
14871505
}
14881506

rivetkit-typescript/packages/rivetkit/src/drivers/file-system/actor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ export class FileSystemActorDriver implements ActorDriver {
8181
await this.#state.setActorAlarm(actor.id, timestamp);
8282
}
8383

84-
/** SQLite VFS instance for creating KV-backed databases */
85-
async getSqliteVfs(): Promise<SqliteVfs> {
84+
/** Creates a SQLite VFS instance for creating KV-backed databases */
85+
async createSqliteVfs(): Promise<SqliteVfs> {
8686
// Dynamic import keeps @rivetkit/sqlite out of the main entrypoint bundle,
8787
// preserving tree-shakeability for environments that don't use SQLite.
8888
// The async @rivetkit/sqlite build is not re-entrant per module instance.

rivetkit-typescript/packages/sqlite-vfs/src/kv.ts

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,33 +47,17 @@ export function getMetaKey(fileTag: SqliteFileTag): Uint8Array {
4747
return key;
4848
}
4949

50-
/**
51-
* Gets the key for a file chunk
52-
* Format: [SQLITE_PREFIX (1 byte), CHUNK_PREFIX (1 byte), file tag (1 byte), chunk index (4 bytes, big-endian)]
53-
*/
54-
export function createChunkKeyFactory(
55-
fileTag: SqliteFileTag,
56-
): (chunkIndex: number) => Uint8Array {
57-
const prefix = new Uint8Array(3);
58-
prefix[0] = SQLITE_PREFIX;
59-
prefix[1] = CHUNK_PREFIX;
60-
prefix[2] = fileTag;
61-
62-
return (chunkIndex: number): Uint8Array => {
63-
const key = new Uint8Array(prefix.length + 4);
64-
key.set(prefix, 0);
65-
const offset = prefix.length;
66-
key[offset + 0] = (chunkIndex >>> 24) & 0xff;
67-
key[offset + 1] = (chunkIndex >>> 16) & 0xff;
68-
key[offset + 2] = (chunkIndex >>> 8) & 0xff;
69-
key[offset + 3] = chunkIndex & 0xff;
70-
return key;
71-
};
72-
}
73-
7450
export function getChunkKey(
7551
fileTag: SqliteFileTag,
7652
chunkIndex: number,
7753
): Uint8Array {
78-
return createChunkKeyFactory(fileTag)(chunkIndex);
54+
const key = new Uint8Array(7);
55+
key[0] = SQLITE_PREFIX;
56+
key[1] = CHUNK_PREFIX;
57+
key[2] = fileTag;
58+
key[3] = (chunkIndex >>> 24) & 0xff;
59+
key[4] = (chunkIndex >>> 16) & 0xff;
60+
key[5] = (chunkIndex >>> 8) & 0xff;
61+
key[6] = chunkIndex & 0xff;
62+
return key;
7963
}

0 commit comments

Comments
 (0)