The query socket (src/daemon.rs) is meant to be read-only, but it enforces that with a lexical guard (is_write_query) that rejects a statement carrying a write keyword. A blocklist is the wrong layer. A same-uid client can reach mutating Kuzu statements that carry none of the listed keywords, for example ALTER, COPY FROM, IMPORT DATABASE, ATTACH DATABASE, CREATE SEQUENCE, CREATE TYPE, LOAD EXTENSION, and non-read-only call functions. That permits schema drift, data import or corruption, database attach, or native extension loading in the daemon process.
The robust fix is engine-level read-only. lbug exposes SystemConfig::read_only(true) at database open, so the query path should execute against a read-only database handle. Two things to resolve first:
- lbug has no per-statement read-only classification (only
prepared_statement_error_message), and there is no read-only mode on an individual connection. So this means a separate read-only Database handle for the query path, not a per-query check.
- The daemon currently uses one read-write handle for both promotion writes and socket reads. Opening a second read-only handle on the same database files alongside the writer needs Kuzu's read-write plus read-only concurrency behaviour confirmed (file locking) before relying on it.
This affects the legacy text query mode and the typed-row mode equally. It is pre-existing and was not introduced by the typed-row work, which only tightened the lexical guard to catch a write hidden after a MATCH. The same-uid residual is consistent with the other same-uid items in the security model (closed properly only by installd inode-keyed identity), but a truly read-only socket is the right enforcement regardless of that.
The query socket (
src/daemon.rs) is meant to be read-only, but it enforces that with a lexical guard (is_write_query) that rejects a statement carrying a write keyword. A blocklist is the wrong layer. A same-uid client can reach mutating Kuzu statements that carry none of the listed keywords, for exampleALTER,COPY FROM,IMPORT DATABASE,ATTACH DATABASE,CREATE SEQUENCE,CREATE TYPE,LOAD EXTENSION, and non-read-only call functions. That permits schema drift, data import or corruption, database attach, or native extension loading in the daemon process.The robust fix is engine-level read-only. lbug exposes
SystemConfig::read_only(true)at database open, so the query path should execute against a read-only database handle. Two things to resolve first:prepared_statement_error_message), and there is no read-only mode on an individual connection. So this means a separate read-onlyDatabasehandle for the query path, not a per-query check.This affects the legacy text query mode and the typed-row mode equally. It is pre-existing and was not introduced by the typed-row work, which only tightened the lexical guard to catch a write hidden after a
MATCH. The same-uid residual is consistent with the other same-uid items in the security model (closed properly only by installd inode-keyed identity), but a truly read-only socket is the right enforcement regardless of that.