
SQLite 3.53.4 - 高 - ローカル - UAF - rtree last_insert_rowid 0 ノードハッシュ
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · sqlite-rtree-rowid0
解説記事: https://abraxaslabs.tech/research/sqlite-rtree-rowid0
クラス: UAF 到達範囲: ローカル
SQLite 3.53.4 (b09c88c14082339b66c7b7158d609a771e64ca69、SOURCE_ID 2026-07-24 19:02:57 bf7c7f30031888f4e796e429ab3978879485813aaca6f641c7b33e4e09459bcc) - Hwaci
nodeWrite は、%_node への INSERT が OK を返した後も sqlite3_last_insert_rowid() が 0 のままである場合、iNode==0 で新しい rtree ノードをハッシュ化します。nodeHashDelete は 0 をスキップします。SplitNode 後の nodeRelease は、ハッシュがまだ保持している RtreeNode を解放します。その後の nodeHashLookup はヒープの use-after-free となります。同じ nodeWrite は geopoly の経路にも存在します。
RCE は実証されていません。ASan は nodeHashLookup で READ を報告します。後の 3.53 系の修正では、割り当てられたノード番号が依然として 0 の場合に SQLITE_CORRUPT_VTAB を返します。このチェックは本 GA には含まれていません。
| ID | CVE 未割り当て |
| クラス | UAF (RtreeNode のヒープ use-after-free。RCE は未実証) |
| 到達範囲 | ローカル (rtree/geopoly テーブルに対する信頼できない SQL または信頼できない .db) |
| 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 |
| 製品 | SQLite rtree / geopoly (SQLITE_ENABLE_RTREE) |
| 影響を受けるバージョン | 3.53.4 (b09c88c) |
| 認証 | 攻撃者の SQL、または仕込まれたシャドウテーブルトリガー / WITHOUT ROWID の %_node |
| ライセンス | GNU Affero GPL v3.0 |
| ラボ | 127.0.0.1:18890 のみ。ASan による証跡。エクスプロイトペイロードなし。 |
rtree または geopoly テーブル上で、ノードの INSERT が OK を報告しつつ last_insert_rowid() が 0 のままになるようにします。文書化されている方法は、%_node を WITHOUT ROWID に置き換えるか、シャドウテーブルに BEFORE INSERT RAISE(IGNORE) を仕込み、その後再接続して接続の last rowid を 0 にする、というものです。ノードが分割されるまで INSERT を続けます。右側の兄弟ノードはノード 0 としてハッシュ化され、解放された後もなお検索可能です。
その後、ホストプロセスは解放済みの RtreeNode を使用します。ラボでは AddressSanitizer の heap-use-after-free が発生します。命令ポインタの制御は本パックには含まれていません。クラッシュは発生します。
SQLITE_VTAB_INNOCUOUS が設定されているため、trusted_schema がオフでも rtree は動作します。デフォルトのコンパイルでは SQLITE_DEFAULT_DEFENSIVE は設定されません。信頼できない DB ファイルに事前に仕込まれたトリガーは、rtree 自身の pWriteNode INSERT が実行されるときにも依然として実行されます。
https://github.com/sqlite/sqlite のタグ https://github.com/sqlite/sqlite/tree/version-3.53.4 を対象に、20 回のソース調査を行いました。正規のツリーは https://sqlite.org/src です。GitHub は Fossil のミラーです。3.54.0 はまだドラフトです。この見落としは、追加の -DSQLITE_ENABLE_RTREE に典型的なものです (公式推奨のコンパイル、Windows nmake OPTS、autoconf フォールバック、fuzzcheck のいずれもこれを設定しています)。
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 は iNode<=0 を拒否する前にハッシュを参照する。解放済みのゼロエントリにヒットするのが UAF である。iNode<=0 でのミスは SQLITE_CORRUPT_VTAB であり、これは別の経路である。
すでに記録済みの誤った道筋:%_rowid の last_insert_rowid 0 をこのバグとみなすこと(それはセルの rowid に 0 を割り当てるのであって、ハッシュの UAF ではない);このピンで SQLITE_CORRUPT_VTAB を期待すること(それは後の修正である);分割が起きないように 50 セルを埋めること;トリガーを仕込んで再度開かないこと;lookaside が ASan から解放を隠すこと;クラッシュを RCE と呼ぶこと。
cd lab
./run.sh
Amalgamation 3.53.4 を -DSQLITE_ENABLE_RTREE=1 と -fsanitize=address 付きで使用。ベンダーの rtreeI.test 2.0-2.2: 51 行を埋め(RTREE_MAXCELLS)、%_node に BEFORE INSERT RAISE(IGNORE) を設定し、last_insert_rowid()==0 で再オープンし、52 行目を INSERT(分割)してから SELECT * を実行。トリガーなしの制御分割は問題なし。127.0.0.1:18890 にバインド。ハーネスは 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
sqlite3_last_insert_rowid() の後、iNode がまだ 0 の場合、SQLITE_CORRUPT_VTAB を返し、nodeHashInsert を実行しない。branch-3.53 11ce330557 / Git 63b6d705 (2026-09-24) はそうしている。テストは「database disk image is malformed」を期待する。この GA はそうなっていない。
b09c88c14082339b66c7b7158d609a771e64ca69)2026-07-24 19:02:57 bf7c7f30031888f4e796e429ab3978879485813aaca6f641c7b33e4e09459bccnodeWrite、nodeHashDelete、nodeRelease、nodeAcquire、SplitNode11ce330557 (2026-09-24、Claude / Ada Logics)