Index: src/btree.c ================================================================== --- src/btree.c +++ src/btree.c @@ -7,11 +7,11 @@ ** May you do good and not evil. ** May you find forgiveness for yourself and forgive others. ** May you share freely, never taking more than you give. ** ************************************************************************* -** $Id: btree.c,v 1.535 2008/11/13 14:28:29 danielk1977 Exp $ +** $Id: btree.c,v 1.536 2008/11/13 18:29:51 shane Exp $ ** ** This file implements a external (disk-based) database using BTrees. ** See the header comment on "btreeInt.h" for additional information. ** Including a description of file format and an overview of operation. */ @@ -732,11 +732,11 @@ cbrk = usableSize; for(i=0; i=pPage->pBt->usableSize ){ + if( pc>=usableSize ){ return SQLITE_CORRUPT_BKPT; } size = cellSizePtr(pPage, &temp[pc]); cbrk -= size; if( cbrkusableSize ){ @@ -833,11 +833,11 @@ ** and the size of the block is "size" bytes. ** ** Most of the effort here is involved in coalesing adjacent ** free blocks into a single big free block. */ -static void freeSpace(MemPage *pPage, int start, int size){ +static int freeSpace(MemPage *pPage, int start, int size){ int addr, pbegin, hdr; unsigned char *data = pPage->aData; assert( pPage->pBt!=0 ); assert( sqlite3PagerIswriteable(pPage->pDbPage) ); @@ -855,14 +855,18 @@ /* Add the space back into the linked list of freeblocks */ hdr = pPage->hdrOffset; addr = hdr + 1; while( (pbegin = get2byte(&data[addr]))0 ){ assert( pbegin<=pPage->pBt->usableSize-4 ); - assert( pbegin>addr ); + if( pbegin<=addr ) { + return SQLITE_CORRUPT_BKPT; + } addr = pbegin; } - assert( pbegin<=pPage->pBt->usableSize-4 ); + if ( pbegin>pPage->pBt->usableSize-4 ) { + return SQLITE_CORRUPT_BKPT; + } assert( pbegin>addr || pbegin==0 ); put2byte(&data[addr], start); put2byte(&data[start], pbegin); put2byte(&data[start+2], size); pPage->nFree += size; @@ -875,11 +879,13 @@ assert( pbegin<=pPage->pBt->usableSize-4 ); pnext = get2byte(&data[pbegin]); psize = get2byte(&data[pbegin+2]); if( pbegin + psize + 3 >= pnext && pnext>0 ){ int frag = pnext - (pbegin+psize); - assert( frag<=data[pPage->hdrOffset+7] ); + if( (frag<0) || (frag>data[pPage->hdrOffset+7]) ){ + return SQLITE_CORRUPT_BKPT; + } data[pPage->hdrOffset+7] -= frag; put2byte(&data[pbegin], get2byte(&data[pnext])); put2byte(&data[pbegin+2], pnext+get2byte(&data[pnext+2])-pbegin); }else{ addr = pbegin; @@ -892,10 +898,11 @@ pbegin = get2byte(&data[hdr+1]); memcpy(&data[hdr+1], &data[pbegin], 2); top = get2byte(&data[hdr+5]); put2byte(&data[hdr+5], top + get2byte(&data[pbegin+2])); } + return SQLITE_OK; } /* ** Decode the flags byte (the first byte of the header) for a page ** and initialize fields of the MemPage structure accordingly. @@ -4577,22 +4584,26 @@ static int dropCell(MemPage *pPage, int idx, int sz){ int i; /* Loop counter */ int pc; /* Offset to cell content of cell being deleted */ u8 *data; /* pPage->aData */ u8 *ptr; /* Used to move bytes around within data[] */ + int rc; /* The return code */ assert( idx>=0 && idxnCell ); assert( sz==cellSize(pPage, idx) ); assert( sqlite3PagerIswriteable(pPage->pDbPage) ); assert( sqlite3_mutex_held(pPage->pBt->mutex) ); data = pPage->aData; ptr = &data[pPage->cellOffset + 2*idx]; pc = get2byte(ptr); - if ( pc<=10 || pc+sz>pPage->pBt->usableSize ) { + if ( (pchdrOffset+6+(pPage->leaf?0:4)) || (pc+sz>pPage->pBt->usableSize) ) { return SQLITE_CORRUPT_BKPT; } - freeSpace(pPage, pc, sz); + rc = freeSpace(pPage, pc, sz); + if( rc!=SQLITE_OK ){ + return rc; + } for(i=idx+1; inCell; i++, ptr+=2){ ptr[0] = ptr[2]; ptr[1] = ptr[3]; } pPage->nCell--; @@ -6049,12 +6060,14 @@ } sqlite3BtreeReleaseTempCursor(&leafCur); }else{ TRACE(("DELETE: table=%d delete from leaf %d\n", pCur->pgnoRoot, pPage->pgno)); - dropCell(pPage, idx, cellSizePtr(pPage, pCell)); - rc = balance(pCur, 0); + rc = dropCell(pPage, idx, cellSizePtr(pPage, pCell)); + if( rc==SQLITE_OK ){ + rc = balance(pCur, 0); + } } if( rc==SQLITE_OK ){ moveToRoot(pCur); } return rc; Index: src/build.c ================================================================== --- src/build.c +++ src/build.c @@ -20,11 +20,11 @@ ** creating ID lists ** BEGIN TRANSACTION ** COMMIT ** ROLLBACK ** -** $Id: build.c,v 1.501 2008/11/11 18:28:59 drh Exp $ +** $Id: build.c,v 1.502 2008/11/13 18:29:51 shane Exp $ */ #include "sqliteInt.h" #include /* @@ -668,11 +668,15 @@ ){ int iDb; /* Database holding the object */ sqlite3 *db = pParse->db; if( pName2 && pName2->n>0 ){ - assert( !db->init.busy ); + if( db->init.busy ) { + sqlite3ErrorMsg(pParse, "corrupt database"); + pParse->nErr++; + return -1; + } *pUnqual = pName2; iDb = sqlite3FindDb(db, pName1); if( iDb<0 ){ sqlite3ErrorMsg(pParse, "unknown database %T", pName1); pParse->nErr++; Index: src/vdbe.c ================================================================== --- src/vdbe.c +++ src/vdbe.c @@ -41,11 +41,11 @@ ** documentation, headers files, or other derived files. The formatting ** of the code in this file is, therefore, important. See other comments ** in this file for details. If in doubt, do not deviate from existing ** commenting and indentation practices when changing or adding code. ** -** $Id: vdbe.c,v 1.786 2008/11/05 16:37:35 drh Exp $ +** $Id: vdbe.c,v 1.787 2008/11/13 18:29:51 shane Exp $ */ #include "sqliteInt.h" #include #include "vdbeInt.h" @@ -2696,11 +2696,14 @@ assert( p2>0 ); assert( p2<=p->nMem ); pIn2 = &p->aMem[p2]; sqlite3VdbeMemIntegerify(pIn2); p2 = pIn2->u.i; - assert( p2>=2 ); + if( p2<2 ) { + rc = SQLITE_CORRUPT_BKPT; + goto abort_due_to_error; + } } assert( i>=0 ); pCur = allocateCursor(p, i, &pOp[-1], iDb, 1); if( pCur==0 ) goto no_mem; pCur->nullRow = 1; Index: test/corruptC.test ================================================================== --- test/corruptC.test +++ test/corruptC.test @@ -13,20 +13,17 @@ # This file implements tests to make sure SQLite does not crash or # segfault if it sees a corrupt database file. It creates a base # data base file, then tests that single byte corruptions in # increasingly larger quantities are handled gracefully. # -# $Id: corruptC.test,v 1.8 2008/11/12 18:21:36 danielk1977 Exp $ +# $Id: corruptC.test,v 1.9 2008/11/13 18:29:51 shane Exp $ catch {file delete -force test.db test.db-journal test.bu} set testdir [file dirname $argv0] source $testdir/tester.tcl -# Set a uniform random seed -expr srand(0) - # Construct a compact, dense database for testing. # do_test corruptC-1.1 { execsql { PRAGMA auto_vacuum = 0; @@ -50,34 +47,42 @@ } # Generate random integer # proc random {range} { - return [expr {round(rand()*$range)}] + return [expr {round(rand()*$range)}] } # Copy file $from into $to # proc copy_file {from to} { - set f [open $from] - fconfigure $f -translation binary - set t [open $to w] - fconfigure $t -translation binary - puts -nonewline $t [read $f [file size $from]] - close $t - close $f + file copy -force $from $to } # Setup for the tests. Make a backup copy of the good database in test.bu. # db close copy_file test.db test.bu sqlite3 db test.db set fsize [file size test.db] +# Set a quasi-random random seed. +if {[info exists SOAKTEST]} { + # If we are doing SOAK tests, we want a different + # random seed for each run. Ideally we would like + # to use [clock clicks] or something like that here. + set qseed [file mtime test.db] +} else { + # If we are not doing soak tests, + # make it repeatable. + set qseed 0 +} +expr srand($qseed) + # -# first test some specific corruption tests found from earlier runs +# First test some specific corruption tests found from earlier runs +# with specific seeds. # # test that a corrupt content offset size is handled (seed 5577) do_test corruptC-2.1 { db close @@ -163,18 +168,103 @@ hexio_write test.db 3150 [format %02x 0xa8] sqlite3 db test.db catchsql {BEGIN; UPDATE t2 SET y='abcdef-uvwxyz'; ROLLBACK;} } {1 {database disk image is malformed}} + +# corruption (seed 178692) +do_test corruptC-2.7 { + db close + copy_file test.bu test.db + + # insert corrupt byte(s) + hexio_write test.db 3074 [format %02x 0xa0] + + sqlite3 db test.db + catchsql {BEGIN; UPDATE t2 SET y='abcdef-uvwxyz'; ROLLBACK;} +} {1 {database disk image is malformed}} + +# corruption (seed 179069) +do_test corruptC-2.8 { + db close + copy_file test.bu test.db + + # insert corrupt byte(s) + hexio_write test.db 1393 [format %02x 0x7d] + hexio_write test.db 84 [format %02x 0x19] + hexio_write test.db 3287 [format %02x 0x3b] + hexio_write test.db 2564 [format %02x 0xed] + hexio_write test.db 2139 [format %02x 0x55] + + sqlite3 db test.db + catchsql {BEGIN; DELETE FROM t1 WHERE x>13; ROLLBACK;} +} {1 {database disk image is malformed}} + +# corruption (seed 170434) +do_test corruptC-2.9 { + db close + copy_file test.bu test.db + + # insert corrupt byte(s) + hexio_write test.db 2095 [format %02x 0xd6] + + sqlite3 db test.db + catchsql {BEGIN; DELETE FROM t1 WHERE x>13; ROLLBACK;} +} {1 {database disk image is malformed}} + +# corruption (seed 186504) +do_test corruptC-2.10 { + db close + copy_file test.bu test.db + + # insert corrupt byte(s) + hexio_write test.db 3130 [format %02x 0x02] + + sqlite3 db test.db + catchsql {BEGIN; UPDATE t2 SET y='abcdef-uvwxyz'; ROLLBACK;} +} {1 {database disk image is malformed}} + +# corruption (seed 1589) +do_test corruptC-2.11 { + db close + copy_file test.bu test.db + + # insert corrupt byte(s) + hexio_write test.db 55 [format %02x 0xa7] + + sqlite3 db test.db + catchsql {BEGIN; CREATE TABLE t3 AS SELECT x,3 as y FROM t2 WHERE rowid%5!=0; ROLLBACK;} +} {1 {database disk image is malformed}} + +# corruption (seed 14166) +do_test corruptC-2.12 { + db close + copy_file test.bu test.db + + # insert corrupt byte(s) + hexio_write test.db 974 [format %02x 0x2e] + + sqlite3 db test.db + catchsql {SELECT count(*) FROM sqlite_master;} +} {1 {malformed database schema (t1i1) - corrupt database}} + +# corruption (seed 218803) +do_test corruptC-2.13 { + db close + copy_file test.bu test.db + + # insert corrupt byte(s) + hexio_write test.db 102 [format %02x 0x12] + + sqlite3 db test.db + catchsql {BEGIN; CREATE TABLE t3 AS SELECT x,3 as y FROM t2 WHERE rowid%5!=0; ROLLBACK;} +} {1 {database disk image is malformed}} + # # now test for a series of quasi-random seeds -# -for {set tn 0} {$tn<=1024} {incr tn 1} { - - # Set a quasi-random random seed - expr srand($tn) +for {set tn 0} {$tn<1024} {incr tn 1} { # setup for test db close copy_file test.bu test.db sqlite3 db test.db @@ -182,46 +272,68 @@ # Seek to a random location in the file, and write a random single byte # value. Then do various operations on the file to make sure that # the database engine can handle the corruption gracefully. # set last 0 - for {set i 1} {$i<=1024 && !$last} {incr i 1} { + for {set i 1} {$i<=512 && !$last} {incr i 1} { # insert random byte at random location db close - hexio_write test.db [random $fsize] [format %02x [random 255]] + set roffset [random $fsize] + set rbyte [format %02x [random 255]] + + # You can uncomment the following to have it trace + # exactly how it's corrupting the file. This is + # useful for generating the "seed specific" tests + # above. + # set rline "$roffset $rbyte" + # puts stdout $rline + + hexio_write test.db $roffset $rbyte sqlite3 db test.db # do a few random operations to make sure that if # they error, they error gracefully instead of crashing. - do_test corruptC-3.$tn.$i.1 { + do_test corruptC-3.$tn.($qseed).$i.1 { catchsql {SELECT count(*) FROM sqlite_master} set x {} } {} - do_test corruptC-3.$tn.$i.2 { + do_test corruptC-3.$tn.($qseed).$i.2 { catchsql {SELECT count(*) FROM t1} set x {} } {} - do_test corruptC-3.$tn.$i.3 { + do_test corruptC-3.$tn.($qseed).$i.3 { catchsql {SELECT count(*) FROM t1 WHERE x>13} set x {} } {} - do_test corruptC-3.$tn.$i.4 { + do_test corruptC-3.$tn.($qseed).$i.4 { catchsql {SELECT count(*) FROM t2} set x {} } {} - do_test corruptC-3.$tn.$i.5 { + do_test corruptC-3.$tn.($qseed).$i.5 { catchsql {SELECT count(*) FROM t2 WHERE x<13} set x {} } {} - do_test corruptC-3.$tn.$i.6 { + do_test corruptC-3.$tn.($qseed).$i.6 { catchsql {BEGIN; UPDATE t1 SET y=1; ROLLBACK;} set x {} } {} - do_test corruptC-3.$tn.$i.7 { + do_test corruptC-3.$tn.($qseed).$i.7 { catchsql {BEGIN; UPDATE t2 SET y='abcdef-uvwxyz'; ROLLBACK;} set x {} + } {} + do_test corruptC-3.$tn.($qseed).$i.8 { + catchsql {BEGIN; DELETE FROM t1 WHERE x>13; ROLLBACK;} + set x {} + } {} + do_test corruptC-3.$tn.($qseed).$i.9 { + catchsql {BEGIN; DELETE FROM t2 WHERE x<13; ROLLBACK;} + set x {} + } {} + do_test corruptC-3.$tn.($qseed).$i.10 { + catchsql {BEGIN; CREATE TABLE t3 AS SELECT x,3 as y FROM t2 WHERE rowid%5!=0; ROLLBACK;} + set x {} } {} # check the integrity of the database. # once the corruption is detected, we can stop. ifcapable {integrityck} { @@ -241,11 +353,11 @@ # Check that no page references were leaked. # TBD: need to figure out why this doesn't work # work with ROLLBACKs... if {0} { - do_test corruptC-3.$tn.$i.8 { + do_test corruptC-3.$tn.($qseed).$i.11 { set bt [btree_from_db db] db_enter db array set stats [btree_pager_stats $bt] db_leave db set stats(ref) Index: test/soak.test ================================================================== --- test/soak.test +++ test/soak.test @@ -9,11 +9,11 @@ # #*********************************************************************** # This file is the driver for the "soak" tests. It is a peer of the # quick.test and all.test scripts. # -# $Id: soak.test,v 1.3 2008/07/12 14:52:20 drh Exp $ +# $Id: soak.test,v 1.4 2008/11/13 18:29:51 shane Exp $ set testdir [file dirname $argv0] source $testdir/tester.tcl rename finish_test really_finish_test proc finish_test {} {} @@ -45,14 +45,14 @@ # pseudo-random data in some way over and over again for a very # long time. The number of tests run depends on the value of # global variable $TIMEOUT - tests are run for at least $TIMEOUT # seconds. # -# fuzz.test (pseudo-random SQL statements) -# trans.test (pseudo-random changes to a database followed by rollbacks) -# -# fuzzy malloc? +# fuzz.test (pseudo-random SQL statements) +# trans.test (pseudo-random changes to a database followed by rollbacks) +# fuzz_malloc.test +# corruptC.test (pseudo-random corruption to a database) # # Many database changes maintaining some kind of invariant. # Storing checksums etc. # @@ -60,10 +60,11 @@ # set SOAKTESTS { fuzz.test fuzz_malloc.test trans.test + corruptC.test } set ISQUICK 1 set soak_starttime [clock seconds]