An integer overflow exists in the FTS5 extension. It occurs when the size of an array of tombstone pointers is calculated and truncated into a 32-bit integer. A pointer to partially controlled data can then be written out of bounds.
Moderate - The overflow can be triggered by either an attacker who is able to execute arbitrary queries or an attacker that can make an application process a controlled SQLite DB file.
echo "SELECT * FROM articles WHERE articles MATCH 'whatever'" | ./sqlite3 /tmp/poc.sql ================================================================= ==3811642==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5030000012f0 at pc 0x55eafca6599b bp 0x7ffdd1591570 sp 0x7ffdd1591568 READ of size 8 at 0x5030000012f0 thread T0
The vulnerability occurs in fts5SegIterAllocTombstone() when nByte is calculated as the total size for an array of pointers:
static void fts5SegIterAllocTombstone(Fts5Index *p, Fts5SegIter *pIter){ const int nTomb = pIter->pSeg->nPgTombstone; if( nTomb>0 ){ int nByte = SZ_FTS5TOMBSTONEARRAY(nTomb+1); Fts5TombstoneArray *pNew; pNew = (Fts5TombstoneArray*)sqlite3Fts5MallocZero(&p->rc, nByte); if( pNew ){ pNew->nTombstone = nTomb; pNew->nRef = 1; pIter->pTombArray = pNew; } } }
/* Size (in bytes) of an Fts5TombstoneArray holding up to N tombstones */ #define SZ_FTS5TOMBSTONEARRAY(N) \ (offsetof(Fts5TombstoneArray,apTombstone)+(N)*sizeof(Fts5Data*))
nByte
is a signed 32-bit integer. While the multiplication done in the SZ_FTS5TOMBSTONEARRAY
macro is a 64-bit multiplication, the result gets truncated to 32-bit should the resulting value exceed 2^32 - 1.
pIter->pSeg->nPgTombstone
is a fully attacker controlled, 32-bit value that is stored within the metadata tables for a FTS5 table:
i += fts5GetVarint32(&pData[i], pSeg->nPgTombstone);
By setting nPgTombstone
to e.g. 1610612738, nByte
becomes 32. As a result, only 32 bytes are allocated for an array that is supposed to fit 1610612738 entries.
This array is then used in the fts5MultiIterIsDeleted()
function. An array index is calculated using the Rowid
of the current context and is bound checked by the number of Tombstone pointers. Since nTombstone
is the original value that was not overflowed, the iPg
entry can point out of bounds. If the entry at iPg
is 0, a structure is allocated and a pointer to it is written out of bounds:
if( pSeg->pLeaf && pArray ){ /* Figure out which page the rowid might be present on. */ int iPg = ((u64)pSeg->iRowid) % pArray->nTombstone; assert( iPg>=0 ); /* If tombstone hash page iPg has not yet been loaded from the ** database, load it now. */ if( pArray->apTombstone[iPg]==0 ){ pArray->apTombstone[iPg] = fts5DataRead(pIter->pIndex, FTS5_TOMBSTONE_ROWID(pSeg->pSeg->iSegid, iPg) ); if( pArray->apTombstone[iPg]==0 ) return 0; }
Fix can be found here: https://sqlite.org/src/info/63595b74956a9391
Date reported: 07/15/2025
Date fixed: 07/16/2025
Date disclosed: 08/15/2025