
SQLite 3.53.4 - High - Local - UAF - rtree last_insert_rowid 0 node hash
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · sqlite-rtree-rowid0
Class: UAF Reach: Local
SQLite 3.53.4 (b09c88c14082339b66c7b7158d609a771e64ca69, SOURCE_ID 2026-07-24 19:02:57 bf7c7f30031888f4e796e429ab3978879485813aaca6f641c7b33e4e09459bcc) - Hwaci
nodeWrite hashes a new rtree node with iNode==0 when sqlite3_last_insert_rowid() stays 0 after an INSERT into %_node that still reports OK. nodeHashDelete skips 0. nodeRelease after SplitNode frees the RtreeNode while the hash still holds it. Later nodeHashLookup is a heap use-after-free. Same nodeWrite is on the geopoly path.
Not demonstrated RCE. ASan READ in nodeHashLookup. The later branch-3.53 fix is SQLITE_CORRUPT_VTAB when the assigned node number is still 0. That check is not on this GA.
| ID | no CVE yet |
| Class | UAF (heap use-after-free of RtreeNode; not demonstrated RCE) |
| Reach | Local (untrusted SQL or untrusted .db on an rtree/geopoly table) |
| CWE | CWE-416 |
| CVSS | High: 8.1 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Product | SQLite rtree / geopoly (SQLITE_ENABLE_RTREE) |
| Affected | 3.53.4 (b09c88c) |
| Auth | attacker SQL or a planted shadow-table trigger / WITHOUT ROWID %_node |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:18890 only. ASan witness. No exploit payload. |
On an rtree or geopoly table, make node INSERT report OK while last_insert_rowid() stays 0. Documented ways: replace %_node with WITHOUT ROWID, or plant BEFORE INSERT RAISE(IGNORE) on the shadow table, then reopen so the connection's last rowid is 0. INSERT until a node split. The right sibling is hashed as node 0, released, and still findable.
The host process then uses a freed RtreeNode. Lab is AddressSanitizer heap-use-after-free. Instruction-pointer control is not in this pack. The crash is.
SQLITE_VTAB_INNOCUOUS is set, so rtree still runs with trusted_schema off. Default compile does not set SQLITE_DEFAULT_DEFENSIVE. A pre-planted trigger in an untrusted DB file still runs when rtree's own pWriteNode INSERT executes.
Twenty source hunts of https://github.com/sqlite/sqlite tag https://github.com/sqlite/sqlite/tree/version-3.53.4. Canonical tree is https://sqlite.org/src. GitHub is a Fossil mirror. 3.54.0 is still a draft. This leftover is typical extra -DSQLITE_ENABLE_RTREE (official recommended compile, Windows nmake OPTS, autoconf fallback, and fuzzcheck all set it).
if( pNode->iNode==0 && rc==SQLITE_OK ){
pNode->iNode = sqlite3_last_insert_rowid(pRtree->db);
nodeHashInsert(pRtree, pNode);
}
static void nodeHashDelete(Rtree *pRtree, RtreeNode *pNode){
RtreeNode **pp;
if( pNode->iNode!=0 ){
pp = &pRtree->aHash[nodeHash(pNode->iNode)];
for( ; (*pp)!=pNode; pp = &(*pp)->pNext){ assert(*pp); }
*pp = pNode->pNext;
pNode->pNext = 0;
}
}
nodeAcquire looks up the hash before rejecting iNode<=0. A hit on the freed zero entry is the UAF. A miss on iNode<=0 is SQLITE_CORRUPT_VTAB, which is a different path.
Wrong turns already recorded: treating %_rowid last_insert_rowid 0 as this bug (that assigns cell rowid 0, not the hash UAF); expecting SQLITE_CORRUPT_VTAB on this pin (that is the later fix); filling 50 cells so there is no split; planting the trigger and not reopening; lookaside hiding the free from ASan; calling the crash RCE.
cd lab
./run.sh
Amalgamation 3.53.4 with -DSQLITE_ENABLE_RTREE=1 and -fsanitize=address. Vendor rtreeI.test 2.0-2.2: fill 51 rows (RTREE_MAXCELLS), BEFORE INSERT RAISE(IGNORE) on %_node, reopen last_insert_rowid()==0, INSERT the 52nd row (split), then SELECT *. Control split without the trigger is OK. Bind 127.0.0.1:18890. Harness is lab/harness.c.
control-split=ok
reopen-last-rowid=0
witness-before-split=SQLITE-RTREE-ROWID0-WITNESS
ERROR: AddressSanitizer: heap-use-after-free
READ of size 8 ... nodeHashLookup
freed by ... nodeRelease / SplitNode
SUCCESS sqlite-rtree-rowid0 how=asan-uaf-nodehash SQLITE-RTREE-ROWID0-WITNESS
After sqlite3_last_insert_rowid(), if iNode is still 0, return SQLITE_CORRUPT_VTAB and do not nodeHashInsert. branch-3.53 11ce330557 / Git 63b6d705 (2026-09-24) does that. The test expects "database disk image is malformed". This GA does not.
b09c88c14082339b66c7b7158d609a771e64ca69)2026-07-24 19:02:57 bf7c7f30031888f4e796e429ab3978879485813aaca6f641c7b33e4e09459bccnodeWrite, nodeHashDelete, nodeRelease, nodeAcquire, SplitNode11ce330557 (2026-09-24, Claude / Ada Logics)