SQLite

Check-in [5b9f272113]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Fix handling of attempts to modify the database schema, application_id or user_version within an UNLOCKED transaction.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | begin-concurrent
Files: files | file ages | folders
SHA1: 5b9f272113d21fd606903509d6f830fe60fac039
User & Date: dan 2015-08-24 19:08:10.037
Context
2015-08-24
19:56
Change "BEGIN UNLOCKED" to "BEGIN CONCURRENT". (check-in: ba1ab858e2 user: dan tags: begin-concurrent)
19:08
Fix handling of attempts to modify the database schema, application_id or user_version within an UNLOCKED transaction. (check-in: 5b9f272113 user: dan tags: begin-concurrent)
16:00
Fix compilation without SQLITE_ENABLE_UNLOCKED. Also other code organization issues. (check-in: 0411355754 user: dan tags: begin-concurrent)
Changes
Side-by-Side Diff Ignore Whitespace Patch
Changes to src/vdbe.c.
3223
3224
3225
3226
3227
3228
3229











3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241




3242
3243
3244
3245
3246
3247
3248
3249
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243









3244
3245
3246
3247

3248
3249
3250
3251
3252
3253
3254







+
+
+
+
+
+
+
+
+
+
+



-
-
-
-
-
-
-
-
-
+
+
+
+
-







  assert( DbMaskTest(p->btreeMask, pOp->p1) );
  assert( p->readOnly==0 );
  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 ){
    /* Invalidate all prepared statements whenever the TEMP database
    ** schema is changed.  Ticket #1644 */
6106
6107
6108
6109
6110
6111
6112









6113
6114
6115
6116
6117
6118
6119
6111
6112
6113
6114
6115
6116
6117
6118
6119
6120
6121
6122
6123
6124
6125
6126
6127
6128
6129
6130
6131
6132
6133







+
+
+
+
+
+
+
+
+







** P2 contains the root-page of the table to lock.
**
** 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 && p1<db->nDb );
    assert( DbMaskTest(p->btreeMask, p1) );
    assert( isWriteLock==0 || isWriteLock==1 );
    rc = sqlite3BtreeLockTable(db->aDb[p1].pBt, pOp->p2, isWriteLock);
    if( (rc&0xFF)==SQLITE_LOCKED ){
Changes to src/wal.c.
1944
1945
1946
1947
1948
1949
1950











































1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971

1972
1973
1974
1975
1976

1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993

1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012


2013

2014



2015

















2016







2017
2018
2019
2020
2021
2022
2023







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



















-
-
+
-

-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-







    }
    WALTRACE(("WAL%p: closed\n", pWal));
    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.
**
** The wal-index is in shared memory.  Another thread or process might
** be writing the header at the same time this procedure is trying to
** read it, which might result in inconsistency.  A dirty read is detected
** by verifying that both copies of the header are the same and also by
** a checksum on the header.
**
** If and only if the read is consistent and the header is different from
** pWal->hdr, then pWal->hdr is updated to the content of the new header
** and *pChanged is set to 1.
**
** 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 h1;                 /* 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] );

  if( walIndexLoadHdr(pWal, &h1) ){
  /* 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 */
    return 1;
  }  
  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 */
  }

  if( memcmp(&pWal->hdr, &h1, sizeof(WalIndexHdr)) ){
    *pChanged = 1;
    memcpy(&pWal->hdr, &h1, sizeof(WalIndexHdr));
    pWal->szPage = (pWal->hdr.szPage&0xfe00) + ((pWal->hdr.szPage&0x0001)<<16);
    testcase( pWal->szPage<=32768 );
2632
2633
2634
2635
2636
2637
2638
2639
2640

2641
2642
2643
2644







2645
2646

2647
2648

2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663

2664
2665
2666
2667
2668
2669
2670
2648
2649
2650
2651
2652
2653
2654


2655
2656



2657
2658
2659
2660
2661
2662
2663
2664

2665
2666

2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681

2682
2683
2684
2685
2686
2687
2688
2689







-
-
+

-
-
-
+
+
+
+
+
+
+

-
+

-
+














-
+







  **   a) None of the pages in pList have been modified since the 
  **      transaction opened, and
  **
  **   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;
        volatile u32 *aPgno;
        u32 iZero;

        rc = walHashGet(pWal, iHash, &aHash, &aPgno, &iZero);
        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. */
              u8 aNew[4];
              u8 *aOld = &((u8*)pPage1->pData)[40];
Changes to test/unlocked.test.
106
107
108
109
110
111
112



113
114
115
116
117

118





119

120
121
122
123
124
125
126
106
107
108
109
110
111
112
113
114
115
116
117
118
119

120
121
122
123
124
125
126

127
128
129
130
131
132
133
134







+
+
+




-
+

+
+
+
+
+
-
+








#-------------------------------------------------------------------------
# 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.
#
reset_db
404
405
406
407
408
409
410


411





























412
413

412
413
414
415
416
417
418
419
420

421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452







+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


+
  do_test 2.$tn.7.3 {
    list [catch { sql1 { COMMIT } } msg] $msg [sqlite3_errcode db]
  } {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