Index: src/vdbe.c ================================================================== --- src/vdbe.c +++ src/vdbe.c @@ -3225,23 +3225,28 @@ pDb = &db->aDb[pOp->p1]; assert( pDb->pBt!=0 ); assert( sqlite3SchemaMutexHeld(db, pOp->p1, 0) ); pIn3 = &aMem[pOp->p3]; sqlite3VdbeMemIntegerify(pIn3); +#ifdef SQLITE_ENABLE_UNLOCKED + if( db->bUnlocked + && (pOp->p2==BTREE_USER_VERSION || pOp->p2==BTREE_APPLICATION_ID) + ){ + rc = SQLITE_ERROR; + sqlite3VdbeError(p, "cannot modify %s within UNLOCKED transaction", + pOp->p2==BTREE_USER_VERSION ? "user_version" : "application_id" + ); + break; + } +#endif /* See note about index shifting on OP_ReadCookie */ rc = sqlite3BtreeUpdateMeta(pDb->pBt, pOp->p2, (int)pIn3->u.i); if( pOp->p2==BTREE_SCHEMA_VERSION ){ - if( db->bUnlocked ){ - sqlite3VdbeError(p, "cannot modify database schema - " - "UNLOCKED transaction" - ); - rc = SQLITE_ERROR; - }else{ - /* When the schema cookie changes, record the new cookie internally */ - pDb->pSchema->schema_cookie = (int)pIn3->u.i; - db->flags |= SQLITE_InternChanges; - } + /* When the schema cookie changes, record the new cookie internally */ + assert( db->bUnlocked==0 ); + pDb->pSchema->schema_cookie = (int)pIn3->u.i; + db->flags |= SQLITE_InternChanges; }else if( pOp->p2==BTREE_FILE_FORMAT ){ /* Record changes in the file format */ pDb->pSchema->file_format = (u8)pIn3->u.i; } if( pOp->p1==1 ){ @@ -6108,10 +6113,19 @@ ** P4 contains a pointer to the name of the table being locked. This is only ** used to generate an error message if the lock cannot be obtained. */ case OP_TableLock: { u8 isWriteLock = (u8)pOp->p3; +#ifdef SQLITE_ENABLE_UNLOCKED + if( isWriteLock && db->bUnlocked && pOp->p2==1 ){ + rc = SQLITE_ERROR; + sqlite3VdbeError(p, + "cannot modify database schema within UNLOCKED transaction"); + rc = SQLITE_ERROR; + break; + } +#endif if( isWriteLock || 0==(db->flags&SQLITE_ReadUncommitted) ){ int p1 = pOp->p1; assert( p1>=0 && p1nDb ); assert( DbMaskTest(p->btreeMask, p1) ); assert( isWriteLock==0 || isWriteLock==1 ); Index: src/wal.c ================================================================== --- src/wal.c +++ src/wal.c @@ -1946,10 +1946,53 @@ sqlite3_free((void *)pWal->apWiData); sqlite3_free(pWal); } return rc; } + +/* +** Try to copy the wal-index header from shared-memory into (*pHdr). Return +** zero if successful or non-zero otherwise. If the header is corrupted +** (either because the two copies are inconsistent or because the checksum +** values are incorrect), the read fails and non-zero is returned. +*/ +static int walIndexLoadHdr(Wal *pWal, WalIndexHdr *pHdr){ + u32 aCksum[2]; /* Checksum on the header content */ + WalIndexHdr h2; /* Second copy of the header content */ + WalIndexHdr volatile *aHdr; /* Header in shared memory */ + + /* The first page of the wal-index must be mapped at this point. */ + assert( pWal->nWiData>0 && pWal->apWiData[0] ); + + /* Read the header. This might happen concurrently with a write to the + ** same area of shared memory on a different CPU in a SMP, + ** meaning it is possible that an inconsistent snapshot is read + ** from the file. If this happens, return non-zero. + ** + ** There are two copies of the header at the beginning of the wal-index. + ** When reading, read [0] first then [1]. Writes are in the reverse order. + ** Memory barriers are used to prevent the compiler or the hardware from + ** reordering the reads and writes. + */ + aHdr = walIndexHdr(pWal); + memcpy(pHdr, (void *)&aHdr[0], sizeof(h2)); + walShmBarrier(pWal); + memcpy(&h2, (void *)&aHdr[1], sizeof(h2)); + + if( memcmp(&h2, pHdr, sizeof(h2))!=0 ){ + return 1; /* Dirty read */ + } + if( h2.isInit==0 ){ + return 1; /* Malformed header - probably all zeros */ + } + walChecksumBytes(1, (u8*)&h2, sizeof(h2)-sizeof(h2.aCksum), 0, aCksum); + if( aCksum[0]!=h2.aCksum[0] || aCksum[1]!=h2.aCksum[1] ){ + return 1; /* Checksum does not match */ + } + + return 0; +} /* ** Try to read the wal-index header. Return 0 on success and 1 if ** there is a problem. ** @@ -1965,41 +2008,14 @@ ** ** If the checksum cannot be verified return non-zero. If the header ** is read successfully and the checksum verified, return zero. */ static int walIndexTryHdr(Wal *pWal, int *pChanged){ - u32 aCksum[2]; /* Checksum on the header content */ - WalIndexHdr h1, h2; /* Two copies of the header content */ - WalIndexHdr volatile *aHdr; /* Header in shared memory */ - - /* The first page of the wal-index must be mapped at this point. */ - assert( pWal->nWiData>0 && pWal->apWiData[0] ); - - /* Read the header. This might happen concurrently with a write to the - ** same area of shared memory on a different CPU in a SMP, - ** meaning it is possible that an inconsistent snapshot is read - ** from the file. If this happens, return non-zero. - ** - ** There are two copies of the header at the beginning of the wal-index. - ** When reading, read [0] first then [1]. Writes are in the reverse order. - ** Memory barriers are used to prevent the compiler or the hardware from - ** reordering the reads and writes. - */ - aHdr = walIndexHdr(pWal); - memcpy(&h1, (void *)&aHdr[0], sizeof(h1)); - walShmBarrier(pWal); - memcpy(&h2, (void *)&aHdr[1], sizeof(h2)); - - if( memcmp(&h1, &h2, sizeof(h1))!=0 ){ - return 1; /* Dirty read */ - } - if( h1.isInit==0 ){ - return 1; /* Malformed header - probably all zeros */ - } - walChecksumBytes(1, (u8*)&h1, sizeof(h1)-sizeof(h1.aCksum), 0, aCksum); - if( aCksum[0]!=h1.aCksum[0] || aCksum[1]!=h1.aCksum[1] ){ - return 1; /* Checksum does not match */ + WalIndexHdr h1; /* Copy of the header content */ + + if( walIndexLoadHdr(pWal, &h1) ){ + return 1; } if( memcmp(&pWal->hdr, &h1, sizeof(WalIndexHdr)) ){ *pChanged = 1; memcpy(&pWal->hdr, &h1, sizeof(WalIndexHdr)); @@ -2634,20 +2650,23 @@ ** ** b) The database schema cookie has not been modified since the ** transaction was started. */ if( rc==SQLITE_OK ){ - volatile WalIndexHdr *pHead; /* Head of the wal file */ - pHead = walIndexHdr(pWal); + WalIndexHdr head; - /* TODO: Check header checksum is good here. */ - - if( memcmp(&pWal->hdr, (void*)pHead, sizeof(WalIndexHdr))!=0 ){ + if( walIndexLoadHdr(pWal, &head) ){ + /* This branch is taken if the wal-index header is corrupted. This + ** occurs if some other writer has crashed while committing a + ** transaction to this database since the current unlocked transaction + ** was opened. */ + rc = SQLITE_BUSY_SNAPSHOT; + }else if( memcmp(&pWal->hdr, (void*)&head, sizeof(WalIndexHdr))!=0 ){ int iHash; - int iLastHash = walFramePage(pHead->mxFrame); + int iLastHash = walFramePage(head.mxFrame); u32 iFirst = pWal->hdr.mxFrame+1; /* First wal frame to check */ - if( memcmp(pWal->hdr.aSalt, (u32*)pHead->aSalt, sizeof(u32)*2) ){ + if( memcmp(pWal->hdr.aSalt, (u32*)head.aSalt, sizeof(u32)*2) ){ assert( pWal->readLock==0 ); iFirst = 1; } for(iHash=walFramePage(iFirst); iHash<=iLastHash; iHash++){ volatile ht_slot *aHash; @@ -2658,11 +2677,11 @@ if( rc==SQLITE_OK ){ int i; int iMin = (iFirst - iZero); int iMax = (iHash==0) ? HASHTABLE_NPAGE_ONE : HASHTABLE_NPAGE; if( iMin<1 ) iMin = 1; - if( iMax>pHead->mxFrame ) iMax = pHead->mxFrame; + if( iMax>head.mxFrame ) iMax = head.mxFrame; for(i=iMin; i<=iMax; i++){ PgHdr *pPg; if( aPgno[i]==1 ){ /* Check that the schema cookie has not been modified. If ** it has not, the commit can proceed. */ Index: test/unlocked.test ================================================================== --- test/unlocked.test +++ test/unlocked.test @@ -108,17 +108,25 @@ # UNLOCKED transactions may not modify the db schema. # foreach {tn sql} { 1 { CREATE TABLE xx(a, b) } 2 { DROP TABLE t1 } + 3 { CREATE INDEX i1 ON t1(a) } + 4 { CREATE VIEW v1 AS SELECT * FROM t1 } + 5 { CREATE TEMP TABLE xx(a, b) } } { do_catchsql_test 1.7.$tn.1 " BEGIN UNLOCKED; $sql - " {1 {cannot modify database schema - UNLOCKED transaction}} + " {1 {cannot modify database schema within UNLOCKED transaction}} + + do_execsql_test 1.7.$tn.2 { + SELECT sql FROM sqlite_master; + SELECT sql FROM sqlite_temp_master; + } {{CREATE TABLE t1(a, b)}} - do_execsql_test 1.7.$tn.2 ROLLBACK + do_execsql_test 1.7.$tn.3 COMMIT } #------------------------------------------------------------------------- # If an auto-vacuum database is written within an UNLOCKED transaction, it # is handled in the same way as for a non-UNLOCKED transaction. @@ -406,8 +414,39 @@ } {0 {} SQLITE_OK} do_test 2.$tn.7.4 { sql3 { PRAGMA integrity_check } } ok } - +#------------------------------------------------------------------------- +# Unlocked transactions may not modify the user_version or application_id. +# +reset_db +do_execsql_test 3.0 { + PRAGMA journal_mode = wal; + CREATE TABLE t1(x, y); + INSERT INTO t1 VALUES('a', 'b'); + PRAGMA user_version = 10; +} {wal} +do_execsql_test 3.1 { + BEGIN UNLOCKED; + INSERT INTO t1 VALUES('c', 'd'); + SELECT * FROM t1; +} {a b c d} +do_catchsql_test 3.2 { + PRAGMA user_version = 11; +} {1 {cannot modify user_version within UNLOCKED transaction}} +do_execsql_test 3.3 { + PRAGMA user_version; + SELECT * FROM t1; +} {10 a b c d} +do_catchsql_test 3.4 { + PRAGMA application_id = 11; +} {1 {cannot modify application_id within UNLOCKED transaction}} +do_execsql_test 3.5 { + COMMIT; + PRAGMA user_version; + PRAGMA application_id; + SELECT * FROM t1; +} {10 0 a b c d} finish_test +