
SQLite 3.53.4 — высокий — локальный — UAF — rtree last_insert_rowid 0 node hash
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 хеширует новый узел rtree с iNode==0, когда sqlite3_last_insert_rowid() остаётся равным 0 после INSERT в %_node, который при этом всё равно сообщает об успехе. nodeHashDelete пропускает 0. nodeRelease после SplitNode освобождает RtreeNode, пока хеш всё ещё хранит его. Последующий nodeHashLookup — это use-after-free в куче. Тот же nodeWrite присутствует и на пути geopoly.
RCE не продемонстрирован. ASan READ в nodeHashLookup. Более позднее исправление в ветке 3.53 — это SQLITE_CORRUPT_VTAB, когда назначенный номер узла всё ещё равен 0. Этой проверки нет в данной GA-версии.
| ID | CVE пока нет |
| Класс | UAF (use-after-free в куче для RtreeNode; RCE не продемонстрирован) |
| Доступ | Локальный (недоверенный SQL или недоверенный .db на таблице rtree/geopoly) |
| CWE | CWE-416 |
| CVSS | Высокий: 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 узла сообщать об успехе, пока last_insert_rowid() остаётся равным 0. Документированные способы: заменить %_node на WITHOUT ROWID или внедрить BEFORE INSERT RAISE(IGNORE) на теневой таблице, затем переоткрыть соединение, чтобы last rowid соединения стал равен 0. Выполнять INSERT до разделения узла. Правый сосед хешируется как узел 0, освобождается и всё ещё может быть найден.
Затем процесс-хост использует освобождённый RtreeNode. В лаборатории — AddressSanitizer heap-use-after-free. Контроль над instruction pointer в этот пакет не входит. А падение — входит.
SQLITE_VTAB_INNOCUOUS установлен, поэтому rtree всё равно работает при выключенном trusted_schema. Компиляция по умолчанию не задаёт SQLITE_DEFAULT_DEFENSIVE. Заранее внедрённый триггер в недоверенном файле БД всё равно срабатывает, когда выполняется собственный INSERT pWriteNode в rtree.
Двадцать поисков по исходникам https://github.com/sqlite/sqlite тега https://github.com/sqlite/sqlite/tree/version-3.53.4. Каноническое дерево — 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 ячеек, чтобы не было разделения; установка триггера без повторного открытия; сокрытие освобождения от ASan через lookaside; называние падения RCE.
cd lab
./run.sh
Amalgamation 3.53.4 с -DSQLITE_ENABLE_RTREE=1 и -fsanitize=address. Вендорный rtreeI.test 2.0-2.2: заполнить 51 строку (RTREE_MAXCELLS), BEFORE INSERT RAISE(IGNORE) на %_node, переоткрыть last_insert_rowid()==0, INSERT 52-й строки (split), затем SELECT *. Контрольный split без триггера — OK. Привязка 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)