diff --git a/.github/workflows/ci4sqlite3mc.yml b/.github/workflows/ci4sqlite3mc.yml index 8c7fc99d..634aa23f 100644 --- a/.github/workflows/ci4sqlite3mc.yml +++ b/.github/workflows/ci4sqlite3mc.yml @@ -48,6 +48,14 @@ jobs: make cryptotest ./cryptotest + - name: Temporary file tests + run: | + make tempfiletest + ./tempfiletest 20000 + ./tempfiletest 20000 1 faults + ./tempfiletest 20000 1 db4096 + ./tempfiletest 20000 1 chunk faults + android_build: name: Android (API ${{ matrix.api-level }}) runs-on: ubuntu-latest diff --git a/Makefile.am b/Makefile.am index 3ab094e0..fae8fa40 100644 --- a/Makefile.am +++ b/Makefile.am @@ -165,8 +165,12 @@ sqlite3shell_LDFLAGS += -no-install endif # Tests for the cryptographic primitives (not installed, built like the shell). -check_PROGRAMS = cryptotest +check_PROGRAMS = cryptotest tempfiletest cryptotest_SOURCES = test/cryptotest.c cryptotest_CFLAGS = $(sqlite3shell_CFLAGS) cryptotest_LDADD = $(sqlite3shell_LDADD) cryptotest_LDFLAGS = $(sqlite3shell_LDFLAGS) +tempfiletest_SOURCES = test/tempfiletest.c +tempfiletest_CFLAGS = $(sqlite3shell_CFLAGS) +tempfiletest_LDADD = $(sqlite3shell_LDADD) +tempfiletest_LDFLAGS = $(sqlite3shell_LDFLAGS) diff --git a/src/sqlite3mc_vfs.c b/src/sqlite3mc_vfs.c index 9ad35f4e..d13ec1b7 100644 --- a/src/sqlite3mc_vfs.c +++ b/src/sqlite3mc_vfs.c @@ -20,6 +20,7 @@ typedef struct sqlite3mc_file sqlite3mc_file; typedef struct sqlite3mc_vfs sqlite3mc_vfs; +typedef struct mcTempCipher mcTempCipher; /* ** SQLite3 Multiple Ciphers file structure @@ -36,6 +37,7 @@ struct sqlite3mc_file sqlite3mc_file* pMainDb; /* Main database to which this one is attached */ Codec* codec; /* Codec if encrypted */ int pageNo; /* Page number (in case of journal files) */ + mcTempCipher* tempCipher; /* Cipher of a temporary file opened without a name */ }; /* @@ -96,6 +98,459 @@ static int mcIoShmUnmap(sqlite3_file* pFile, int deleteFlag); static int mcIoFetch(sqlite3_file* pFile, sqlite3_int64 iOfst, int iAmt, void** pp); static int mcIoUnfetch(sqlite3_file* pFile, sqlite3_int64 iOfst, void* p); +/* +** Encryption of temporary files +** +** SQLite opens temporary files without a name: temporary and transient +** databases, temporary journals, sorter files and statement journals. Such a +** file is private and deleted on close (see xOpen in sqlite3.h), and it can't +** be linked to the codec of a database. Therefore every file opened without a +** name is encrypted with its own random key, which exists only in memory and +** is wiped when the file is closed. +** +** A file is encrypted in blocks with ChaCha20. Each block is stored with the +** value of a counter in front of it, which the file increments for every block +** it writes, so the offsets within the file move by 8 bytes per block. The +** counter is the nonce of the block and is read back from the file, so no +** state per block has to be kept in memory. A write that appends to the last +** block continues its keystream, every other write re-encrypts the block with +** the next counter value. A keystream is therefore never used twice. +** +** Blocks below the last one are always complete, so the size of the file tells +** how many bytes of the last block are valid. A block that was never written +** holds the counter value 0 and reads as zeros. +** +** The encryption protects the confidentiality of temporary files, not their +** integrity. +*/ + +#ifndef SQLITE3MC_ENCRYPT_TEMP_FILES +#define SQLITE3MC_ENCRYPT_TEMP_FILES 1 +#endif + +#define MC_TEMP_BLOCK 4096 /* Block size unless the database page size is known */ +#define MC_TEMP_MAX_BLOCK 65536 /* Largest database page size SQLite supports */ +#define MC_TEMP_HEADER 8 /* Size of the counter in front of every block */ +#define MC_TEMP_MAX_BROKEN 8 /* Blocks whose failed write is remembered individually */ + +struct mcTempCipher +{ + uint8_t key[32]; /* Random key */ + int blockSize; /* Payload bytes per block */ + sqlite3_uint64 counter; /* Last counter value used; 0 if nothing was written yet */ + sqlite3_int64 size; /* Size of the file as SQLite sees it */ + sqlite3_int64 iTail; /* Block whose keystream may be continued, or -1 */ + sqlite3_uint64 tailCounter; /* Counter value of that block */ + int nTailUsed; /* Bytes of its keystream that are already used */ + sqlite3_int64 aBroken[MC_TEMP_MAX_BROKEN]; + /* Blocks left unreadable by a failed write */ + int nBroken; /* Entries used in aBroken */ + int allBroken; /* More blocks were broken than aBroken can hold */ + int tailStale; /* Data of a failed write may still sit behind the end */ + uint8_t* buffer; /* Work buffer for one block, allocated when the block size is + ** known; holds only ciphertext between calls, and SQLite uses a + ** file from one thread at a time */ +}; + +/* +** Create the cipher of a temporary file with a fresh random key +*/ +static mcTempCipher* mcTempCreate(void) +{ + mcTempCipher* pTemp = (mcTempCipher*) sqlite3_malloc(sizeof(mcTempCipher)); + if (pTemp != NULL) + { + memset(pTemp, 0, sizeof(mcTempCipher)); + pTemp->blockSize = MC_TEMP_BLOCK; + pTemp->iTail = -1; + chacha20_rng(pTemp->key, sizeof(pTemp->key)); + } + return pTemp; +} + +/* +** Wipe and release the cipher of a temporary file +*/ +static void mcTempFree(mcTempCipher* pTemp) +{ + if (pTemp != NULL) + { + if (pTemp->buffer != NULL) + { + sqlite3mcSecureZeroMemory(pTemp->buffer, (size_t) (MC_TEMP_HEADER + pTemp->blockSize)); + sqlite3_free(pTemp->buffer); + } + sqlite3mcSecureZeroMemory(pTemp, sizeof(mcTempCipher)); + sqlite3_free(pTemp); + } +} + +/* +** The work buffer is allocated when the first write has settled the block size +*/ +static int mcTempBuffer(mcTempCipher* pTemp) +{ + if (pTemp->buffer == NULL) + { + pTemp->buffer = (uint8_t*) sqlite3_malloc(MC_TEMP_HEADER + pTemp->blockSize); + if (pTemp->buffer == NULL) return SQLITE_NOMEM; + } + return SQLITE_OK; +} + +/* +** Position of a block in the file +*/ +static sqlite3_int64 mcTempSlot(mcTempCipher* pTemp, sqlite3_int64 iBlock) +{ + return iBlock * (MC_TEMP_HEADER + pTemp->blockSize); +} + +/* +** Payload bytes of a block that belong to the file +*/ +static int mcTempValid(mcTempCipher* pTemp, sqlite3_int64 iBlock) +{ + sqlite3_int64 start = iBlock * pTemp->blockSize; + if (pTemp->size <= start) return 0; + return (pTemp->size - start < pTemp->blockSize) ? (int) (pTemp->size - start) : pTemp->blockSize; +} + +/* +** Blocks whose rewrite failed are unreadable; if there are more of them than +** can be remembered, the whole file is treated as unreadable +*/ +static int mcTempIsBroken(mcTempCipher* pTemp, sqlite3_int64 iBlock) +{ + int i; + if (pTemp->allBroken) return 1; + for (i = 0; i < pTemp->nBroken; i++) + { + if (pTemp->aBroken[i] == iBlock) return 1; + } + return 0; +} + +static void mcTempMarkBroken(mcTempCipher* pTemp, sqlite3_int64 iBlock) +{ + if (mcTempIsBroken(pTemp, iBlock)) return; + if (pTemp->nBroken < MC_TEMP_MAX_BROKEN) pTemp->aBroken[pTemp->nBroken++] = iBlock; + else pTemp->allBroken = 1; +} + +static void mcTempClearBroken(mcTempCipher* pTemp, sqlite3_int64 iBlock) +{ + int i; + for (i = 0; i < pTemp->nBroken; i++) + { + if (pTemp->aBroken[i] == iBlock) pTemp->aBroken[i] = pTemp->aBroken[--pTemp->nBroken]; + } +} + +/* +** Blocks that a truncate removed are no longer unreadable +*/ +static void mcTempClearBrokenFrom(mcTempCipher* pTemp, sqlite3_int64 iFirst) +{ + int i; + for (i = 0; i < pTemp->nBroken; ) + { + if (pTemp->aBroken[i] >= iFirst) pTemp->aBroken[i] = pTemp->aBroken[--pTemp->nBroken]; + else i++; + } +} + +/* +** XOR the keystream of a counter value into data, which starts at byte iOff of +** the block +*/ +static void mcTempXorKeystream(mcTempCipher* pTemp, sqlite3_uint64 counter, + uint8_t* data, int iOff, int n) +{ + uint8_t nonce[12]; + uint32_t block = (uint32_t) (iOff / 64); + int skip = iOff % 64; + int i; + memset(nonce, 0, sizeof(nonce)); + for (i = 0; i < 8; i++) nonce[i] = (uint8_t) (counter >> (8 * i)); + if (skip > 0) + { + uint8_t ks[64]; + int m = (n < 64 - skip) ? n : 64 - skip; + memset(ks, 0, sizeof(ks)); + chacha20_xor(ks, sizeof(ks), pTemp->key, nonce, block++); + for (i = 0; i < m; i++) data[i] ^= ks[skip + i]; + sqlite3mcSecureZeroMemory(ks, sizeof(ks)); + data += m; + n -= m; + } + if (n > 0) + { + chacha20_xor(data, (size_t) n, pTemp->key, nonce, block); + } +} + +/* +** Read the counter value in front of a block; 0 means the block was never +** written +*/ +static sqlite3_uint64 mcTempCounterOf(const uint8_t* header) +{ + sqlite3_uint64 counter = 0; + int i; + for (i = 0; i < 8; i++) counter |= ((sqlite3_uint64) header[i]) << (8 * i); + return counter; +} + +static void mcTempPutCounter(uint8_t* header, sqlite3_uint64 counter) +{ + int i; + for (i = 0; i < 8; i++) header[i] = (uint8_t) (counter >> (8 * i)); +} + +/* +** Remove what sits behind the end of the file: the rest of a failed write, or +** the blocks that a truncate kept because the file has a chunk size. Their +** counter value is cleared, so that they read as never written. +*/ +static void mcTempDropTail(sqlite3mc_file* mcFile) +{ + static const uint8_t zero[MC_TEMP_HEADER] = { 0 }; + mcTempCipher* pTemp = mcFile->tempCipher; + sqlite3_file* pReal = REALFILE(mcFile); + sqlite3_int64 slot = MC_TEMP_HEADER + pTemp->blockSize; + sqlite3_int64 iBlock = pTemp->size / pTemp->blockSize; + int iOff = (int) (pTemp->size % pTemp->blockSize); + sqlite3_int64 physical = iBlock * slot + (iOff > 0 ? MC_TEMP_HEADER + iOff : 0); + sqlite3_int64 kept = 0; + sqlite3_int64 b = (pTemp->size + pTemp->blockSize - 1) / pTemp->blockSize; + + pTemp->tailStale = (pReal->pMethods->xTruncate(pReal, physical) != SQLITE_OK); + if (pReal->pMethods->xFileSize(pReal, &kept) != SQLITE_OK) { pTemp->tailStale = 1; return; } + while (b * slot + MC_TEMP_HEADER <= kept) + { + if (pReal->pMethods->xWrite(pReal, zero, MC_TEMP_HEADER, b * slot) != SQLITE_OK) + { + mcTempMarkBroken(pTemp, b); + pTemp->tailStale = 1; + } + b++; + } +} + +/* +** Read from a temporary file. Everything that was never written, and +** everything behind the end of the file, is returned as zeros. +*/ +static int mcTempRead(sqlite3mc_file* mcFile, void* buffer, int count, sqlite3_int64 offset) +{ + mcTempCipher* pTemp = mcFile->tempCipher; + sqlite3_file* pReal = REALFILE(mcFile); + sqlite3_int64 pos, end = offset + count; + int rc = SQLITE_OK; + assert(offset >= 0 && count >= 0); + if (end > pTemp->size) rc = SQLITE_IOERR_SHORT_READ; + for (pos = offset; pos < end; ) + { + sqlite3_int64 iBlock = pos / pTemp->blockSize; + int iOff = (int) (pos % pTemp->blockSize); + int n = (pTemp->blockSize - iOff < end - pos) ? pTemp->blockSize - iOff : (int) (end - pos); + int nValid = mcTempValid(pTemp, iBlock); + int nDecrypt = 0; + uint8_t* data = (uint8_t*) buffer + (pos - offset); + + if (mcTempIsBroken(pTemp, iBlock)) return SQLITE_IOERR_READ; + if (iOff < nValid) + { + int nRead = MC_TEMP_HEADER + iOff + n; + sqlite3_uint64 counter; + if (mcTempBuffer(pTemp) != SQLITE_OK) return SQLITE_NOMEM; + int rcRead = pReal->pMethods->xRead(pReal, pTemp->buffer, nRead, mcTempSlot(pTemp, iBlock)); + if (rcRead != SQLITE_OK && rcRead != SQLITE_IOERR_SHORT_READ) return rcRead; + counter = mcTempCounterOf(pTemp->buffer); + if (counter != 0) + { + nDecrypt = (iOff + n < nValid) ? n : nValid - iOff; + memcpy(data, pTemp->buffer + MC_TEMP_HEADER + iOff, (size_t) nDecrypt); + mcTempXorKeystream(pTemp, counter, data, iOff, nDecrypt); + } + } + if (nDecrypt < n) memset(data + nDecrypt, 0, (size_t) (n - nDecrypt)); + pos += n; + } + return rc; +} + +/* +** Write one block: the counter value and the payload from the work buffer, or +** only the new bytes when the keystream of the block is continued +*/ +static int mcTempWriteBlock(sqlite3mc_file* mcFile, sqlite3_int64 iBlock, int iStart, int nEnd, + sqlite3_uint64 counter) +{ + mcTempCipher* pTemp = mcFile->tempCipher; + sqlite3_file* pReal = REALFILE(mcFile); + mcTempXorKeystream(pTemp, counter, pTemp->buffer + MC_TEMP_HEADER + iStart, iStart, nEnd - iStart); + if (iStart == 0) + { + mcTempPutCounter(pTemp->buffer, counter); + return pReal->pMethods->xWrite(pReal, pTemp->buffer, MC_TEMP_HEADER + nEnd, mcTempSlot(pTemp, iBlock)); + } + return pReal->pMethods->xWrite(pReal, pTemp->buffer + MC_TEMP_HEADER + iStart, nEnd - iStart, + mcTempSlot(pTemp, iBlock) + MC_TEMP_HEADER + iStart); +} + +/* +** Fill up the last, incomplete block of the file with zeros, so that a write +** behind it never leaves a partly written block in the middle of the file +*/ +static int mcTempWrite(sqlite3mc_file* mcFile, const void* buffer, int count, sqlite3_int64 offset); + +static int mcTempFillTail(sqlite3mc_file* mcFile, sqlite3_int64 iBlock) +{ + static const uint8_t zeros[MC_TEMP_BLOCK] = { 0 }; + mcTempCipher* pTemp = mcFile->tempCipher; + sqlite3_int64 iLast = (pTemp->size > 0) ? (pTemp->size - 1) / pTemp->blockSize : -1; + int nValid, nFill; + if (iLast < 0 || iLast >= iBlock) return SQLITE_OK; + nValid = mcTempValid(pTemp, iLast); + if (nValid == 0 || nValid == pTemp->blockSize) return SQLITE_OK; + for (nFill = pTemp->blockSize - nValid; nFill > 0; ) + { + int n = (nFill < MC_TEMP_BLOCK) ? nFill : MC_TEMP_BLOCK; + int rc = mcTempWrite(mcFile, zeros, n, pTemp->size); + if (rc != SQLITE_OK) return rc; + nFill -= n; + } + return SQLITE_OK; +} + +/* +** Encrypt and write to a temporary file, block by block +*/ +static int mcTempWrite(sqlite3mc_file* mcFile, const void* buffer, int count, sqlite3_int64 offset) +{ + mcTempCipher* pTemp = mcFile->tempCipher; + sqlite3_file* pReal = REALFILE(mcFile); + sqlite3_int64 pos, end = offset + count; + int rc; + assert(offset >= 0 && count >= 0); + if (pTemp->buffer == NULL && (mcFile->openFlags & (SQLITE_OPEN_TEMP_DB | SQLITE_OPEN_TRANSIENT_DB)) && + count >= 512 && count <= MC_TEMP_MAX_BLOCK && (count & (count - 1)) == 0 && offset % count == 0) + { + /* A database uses its page size as block size, so that writing a page + ** never rewrites another; the first write tells the size */ + pTemp->blockSize = count; + } + if (mcTempBuffer(pTemp) != SQLITE_OK) return SQLITE_NOMEM; + if (offset > pTemp->size) + { + /* Only the last block of a file may be incomplete, and nothing that a + ** failed write left behind its end may look like a block */ + rc = mcTempFillTail(mcFile, offset / pTemp->blockSize); + if (rc != SQLITE_OK) return rc; + if (pTemp->tailStale) mcTempDropTail(mcFile); + } + for (pos = offset; pos < end; ) + { + sqlite3_int64 iBlock = pos / pTemp->blockSize; + int iOff = (int) (pos % pTemp->blockSize); + int n = (pTemp->blockSize - iOff < end - pos) ? pTemp->blockSize - iOff : (int) (end - pos); + int nValid = mcTempValid(pTemp, iBlock); + sqlite3_uint64 counter; + int iStart; /* First byte of the block to encrypt and write */ + int nKeep; /* Bytes at the start of the block whose content is kept */ + int nEnd; /* Bytes of the block that are valid afterwards */ + + if (iBlock == pTemp->iTail && iOff == pTemp->nTailUsed && nValid == iOff) + { + /* Append: the keystream of the block behind its valid part is unused */ + counter = pTemp->tailCounter; + iStart = nKeep = iOff; + } + else + { + /* Rewrite the block with the next counter value, keeping the bytes + ** before and behind the new data */ + counter = ++pTemp->counter; + iStart = nKeep = 0; + if (nValid > 0 && (iOff > 0 || iOff + n < nValid)) + { + sqlite3_uint64 old; + if (mcTempIsBroken(pTemp, iBlock)) return SQLITE_IOERR_WRITE; + nKeep = nValid; + rc = pReal->pMethods->xRead(pReal, pTemp->buffer, MC_TEMP_HEADER + nKeep, mcTempSlot(pTemp, iBlock)); + if (rc != SQLITE_OK) return (rc == SQLITE_IOERR_SHORT_READ) ? SQLITE_IOERR_READ : rc; + old = mcTempCounterOf(pTemp->buffer); + if (old != 0) mcTempXorKeystream(pTemp, old, pTemp->buffer + MC_TEMP_HEADER, 0, nKeep); + else memset(pTemp->buffer + MC_TEMP_HEADER, 0, (size_t) nKeep); + } + } + /* A gap before the new data reads as zeros */ + if (nKeep < iOff) memset(pTemp->buffer + MC_TEMP_HEADER + nKeep, 0, (size_t) (iOff - nKeep)); + memcpy(pTemp->buffer + MC_TEMP_HEADER + iOff, (const uint8_t*) buffer + (pos - offset), (size_t) n); + + nEnd = (iOff + n > nKeep) ? iOff + n : nKeep; + rc = mcTempWriteBlock(mcFile, iBlock, iStart, nEnd, counter); + pTemp->iTail = iBlock; + pTemp->tailCounter = counter; + pTemp->nTailUsed = nEnd; + if (rc != SQLITE_OK) + { + /* Bytes behind the end of the file read as zeros, as if the write had + ** not happened. If the write reached into the part of the block that was + ** already valid, reading the block fails until it is written completely + ** again, because half of it may still be encrypted with the old counter + ** value. */ + if (iStart < nValid) mcTempMarkBroken(pTemp, iBlock); + mcTempDropTail(mcFile); + return rc; + } + if (iStart == 0) mcTempClearBroken(pTemp, iBlock); + if (iBlock * pTemp->blockSize + pTemp->nTailUsed > pTemp->size) + { + pTemp->size = iBlock * pTemp->blockSize + pTemp->nTailUsed; + } + pos += n; + } + return SQLITE_OK; +} + +/* +** Truncate a temporary file. The counter values are kept, so no keystream is +** used twice when the file grows again. +*/ +static int mcTempTruncate(sqlite3mc_file* mcFile, sqlite3_int64 size) +{ + mcTempCipher* pTemp = mcFile->tempCipher; + sqlite3_int64 iBlock = size / pTemp->blockSize; + int iOff = (int) (size % pTemp->blockSize); + sqlite3_int64 physical = mcTempSlot(pTemp, iBlock) + (iOff > 0 ? MC_TEMP_HEADER + iOff : 0); + int rc; + if (size > pTemp->size) + { + rc = mcTempFillTail(mcFile, iBlock); + if (rc != SQLITE_OK) return rc; + } + rc = REALFILE(mcFile)->pMethods->xTruncate(REALFILE(mcFile), physical); + if (rc == SQLITE_OK) + { + pTemp->size = size; + mcTempClearBrokenFrom(pTemp, (size + pTemp->blockSize - 1) / pTemp->blockSize); + mcTempDropTail(mcFile); + } + return rc; +} + +/* +** Size of a temporary file as SQLite sees it, without the counter values +*/ +static int mcTempFileSize(sqlite3mc_file* mcFile, sqlite3_int64* pSize) +{ + *pSize = mcFile->tempCipher->size; + return SQLITE_OK; +} + #define SQLITE3MC_VFS_NAME ("multipleciphers") #define SQLITE3MC_FCNTL_PVFS 0x3f98c078 @@ -415,6 +870,13 @@ static int mcVfsOpen(sqlite3_vfs* pVfs, const char* zName, sqlite3_file* pFile, mcFile->pMainDb = 0; mcFile->pMainNext = 0; mcFile->pageNo = 0; + mcFile->tempCipher = 0; + + if (SQLITE3MC_ENCRYPT_TEMP_FILES && zName == NULL) + { + mcFile->tempCipher = mcTempCreate(); + if (mcFile->tempCipher == NULL) return SQLITE_NOMEM; + } if (zName) { @@ -497,6 +959,11 @@ static int mcVfsOpen(sqlite3_vfs* pVfs, const char* zName, sqlite3_file* pFile, mcMainListAdd(mcFile); } } + else + { + mcTempFree(mcFile->tempCipher); + mcFile->tempCipher = 0; + } return rc; } @@ -600,6 +1067,8 @@ static int mcIoClose(sqlite3_file* pFile) sqlite3mcCodecFree(p->codec); p->codec = 0; } + mcTempFree(p->tempCipher); + p->tempCipher = 0; assert(p->pMainNext == 0 && p->pVfsMC->pMain != p); rc = REALFILE(pFile)->pMethods->xClose(REALFILE(pFile)); @@ -815,7 +1284,12 @@ static int mcReadWal(sqlite3_file* pFile, const void* buffer, int count, sqlite3 static int mcIoRead(sqlite3_file* pFile, void* buffer, int count, sqlite3_int64 offset) { sqlite3mc_file* mcFile = (sqlite3mc_file*) pFile; - int rc = REALFILE(pFile)->pMethods->xRead(REALFILE(pFile), buffer, count, offset); + int rc; + if (mcFile->tempCipher) + { + return mcTempRead(mcFile, buffer, count, offset); + } + rc = REALFILE(pFile)->pMethods->xRead(REALFILE(pFile), buffer, count, offset); if (rc != SQLITE_OK) { return rc; @@ -1118,7 +1592,11 @@ static int mcIoWrite(sqlite3_file* pFile, const void* buffer, int count, sqlite3 int doDefault = 1; sqlite3mc_file* mcFile = (sqlite3mc_file*) pFile; - if (mcFile->openFlags & SQLITE_OPEN_MAIN_DB) + if (mcFile->tempCipher) + { + rc = mcTempWrite(mcFile, buffer, count, offset); + } + else if (mcFile->openFlags & SQLITE_OPEN_MAIN_DB) { rc = mcWriteMainDb(pFile, buffer, count, offset); } @@ -1181,6 +1659,11 @@ static int mcIoWrite(sqlite3_file* pFile, const void* buffer, int count, sqlite3 static int mcIoTruncate(sqlite3_file* pFile, sqlite3_int64 size) { + sqlite3mc_file* mcFile = (sqlite3mc_file*) pFile; + if (mcFile->tempCipher) + { + return mcTempTruncate(mcFile, size); + } return REALFILE(pFile)->pMethods->xTruncate(REALFILE(pFile), size); } @@ -1191,6 +1674,11 @@ static int mcIoSync(sqlite3_file* pFile, int flags) static int mcIoFileSize(sqlite3_file* pFile, sqlite3_int64* pSize) { + sqlite3mc_file* mcFile = (sqlite3mc_file*) pFile; + if (mcFile->tempCipher) + { + return mcTempFileSize(mcFile, pSize); + } return REALFILE(pFile)->pMethods->xFileSize(REALFILE(pFile), pSize); } @@ -1322,6 +1810,12 @@ static int mcIoShmUnmap(sqlite3_file* pFile, int deleteFlag) static int mcIoFetch(sqlite3_file* pFile, sqlite3_int64 iOfst, int iAmt, void** pp) { + if (((sqlite3mc_file*) pFile)->tempCipher) + { + /* No memory mapping for encrypted temporary files; SQLite falls back to xRead */ + *pp = 0; + return SQLITE_OK; + } return REALFILE(pFile)->pMethods->xFetch(REALFILE(pFile), iOfst, iAmt, pp); } diff --git a/test/tempfiletest.c b/test/tempfiletest.c new file mode 100644 index 00000000..17e22dab --- /dev/null +++ b/test/tempfiletest.c @@ -0,0 +1,492 @@ +/* +** Test for the encryption of temporary files: an anonymous temporary file is +** opened through the SQLite3 Multiple Ciphers VFS and gets random writes, +** appends, reads and truncates, which are compared with a plain in-memory +** model of the file. +** +** A second VFS below checks every write that reaches the disk, independently +** of the read path: the blocks carry their counter value in front of them, +** every byte must be the model's plaintext XOR the keystream of that counter, +** a counter value must never be smaller than one used before, and no keystream +** byte may be used twice, not even after a failed write. +** +** Usage: tempfiletest [operations] [seed] [chunk] [faults] [db[N]] [anon] +** +** chunk ask for SQLITE_FCNTL_CHUNK_SIZE first; the file then keeps blocks +** behind its end, which must still read as zeros +** faults let disk writes fail after a random part and disk reads fail; a +** wrong value without an error is a failure +** db[N] open the file as a temporary database whose first write is a page +** of N bytes (default 1024), so blocks are that size +** anon open the file as a main journal without a name: every file opened +** without a name is encrypted, whatever its type +** +** In a build without the encryption of temporary files, only the model is +** checked. +*/ + +#include "sqlite3mc.c" +#include +#include +#include + +#define MAX_FILE_SIZE (3 * 1024 * 1024) +#define MAX_BLOCKS (MAX_FILE_SIZE / 512 + 1) +#define MAX_BLOCK_SIZE 16384 /* largest block size the checks are sized for */ +#define MAX_DISK_WRITES 256 /* one write covers at most 64 KiB, that is 128 blocks of 512 bytes */ + +/* +** The model: what the file should contain +*/ +static unsigned char model[MAX_FILE_SIZE]; /* expected content */ +static unsigned char undefined[MAX_FILE_SIZE]; /* bytes of a failed write, any value is allowed */ +static unsigned char writeBuf[200000]; +static unsigned char readBuf[200000]; +static sqlite3_int64 modelSize; +static unsigned long long rngState; +static int chunkFile; /* the file was given a chunk size */ +static int blockSize = MC_TEMP_BLOCK; /* block size of the file under test */ +static sqlite3_file* file; /* the temporary file under test */ +static int encrypted; /* the build encrypts temporary files */ +static long nWrite, nRead, nTrunc, nShort, nWriteErrors, nReadErrors; + +/* +** Without encryption the size of a one byte file is reported as 0 (SQLite +** ticket #3260), and with a chunk size a truncate rounds the size up +*/ +static int sizeIsPlausible(sqlite3_int64 fsz) +{ + if (chunkFile) return fsz >= modelSize && fsz <= (modelSize + 65535) / 65536 * 65536; + return fsz == modelSize || (modelSize == 1 && fsz == 0); +} + +static unsigned long long rnd(void) +{ + rngState ^= rngState << 13; rngState ^= rngState >> 7; rngState ^= rngState << 17; + return rngState; +} + +/* Mostly small lengths and offsets near block boundaries, sometimes large ones */ +static int randLen(void) +{ + switch (rnd() % 6) + { + case 0: return 1 + (int) (rnd() % 8); + case 1: return 4; + case 2: return 4096; + case 3: return 1 + (int) (rnd() % 5000); + case 4: return 4096 * (1 + (int) (rnd() % 4)) + (int) (rnd() % 3) - 1; + default: return 1 + (int) (rnd() % 65536); /* SQLite writes at most 64 KiB at once; the unix VFS rejects 128 KiB and more */ + } +} +static sqlite3_int64 randOffset(void) +{ + sqlite3_int64 base = (sqlite3_int64) (rnd() % 600) * 4096, off; + switch (rnd() % 6) + { + case 0: off = base; break; + case 1: off = base + (sqlite3_int64) (rnd() % 64); break; + case 2: { sqlite3_int64 back = base - 1 - (sqlite3_int64) (rnd() % 64); off = back > 0 ? back : 0; } break; + case 3: off = modelSize; break; /* append */ + case 4: off = modelSize + (sqlite3_int64) (rnd() % 100); break; /* append after a gap */ + default: off = (sqlite3_int64) (rnd() % (MAX_FILE_SIZE - 200000)); break; + } + return off < MAX_FILE_SIZE ? off : MAX_FILE_SIZE - 1; +} + +/* +** The VFS below the one under test: it records the writes of one operation on +** their way to the disk, and injects errors +*/ +static sqlite3_vfs* realVfs; +static sqlite3_vfs spyVfs; +static struct +{ + sqlite3_int64 off; /* where it went */ + int n; /* how many bytes */ + int failed; /* the write was made to fail */ + unsigned char data[MC_TEMP_HEADER + MAX_BLOCK_SIZE]; +} diskWrite[MAX_DISK_WRITES]; +static int nDiskWrite; /* recorded writes of the current operation */ +static int diskWriteOverflow; /* more writes than the record can hold */ +static int faultMode; /* inject errors */ +static int faultInjected; /* an error was injected during the current operation */ +static long nFaultWrites, nFaultReads; + +/* +** What the checks remember about the file +*/ +static sqlite3_uint64 lastCounter[MAX_BLOCKS]; /* counter value each block was written with */ +static sqlite3_uint64 maxCounter; /* largest counter value seen so far */ +static unsigned char keystreamUsed[MAX_BLOCKS][MAX_BLOCK_SIZE / 8]; +static unsigned char blockFailed[MAX_BLOCKS]; /* a write failed here, so reads and writes may report an error */ +static sqlite3_int64 firstStaleBlock = MAX_BLOCKS; + /* from here on, cleaning up behind the end of the file may have failed */ +static long nWritesChecked, nAppendWrites; + +typedef struct SpyFile { sqlite3_file base; sqlite3_file* real; } SpyFile; +#define REAL(f) (((SpyFile*) (f))->real) +static int spyClose(sqlite3_file* f) { return REAL(f)->pMethods ? REAL(f)->pMethods->xClose(REAL(f)) : SQLITE_OK; } +static int spyRead(sqlite3_file* f, void* b, int n, sqlite3_int64 o) +{ + if (faultMode && rnd() % 80 == 0) { nFaultReads++; faultInjected = 1; return SQLITE_IOERR_READ; } + return REAL(f)->pMethods->xRead(REAL(f), b, n, o); +} +static int spyWrite(sqlite3_file* f, const void* b, int n, sqlite3_int64 o) +{ + int failed = faultMode && rnd() % 40 == 0; + if (nDiskWrite < MAX_DISK_WRITES && n <= MC_TEMP_HEADER + MAX_BLOCK_SIZE) { diskWrite[nDiskWrite].off = o; diskWrite[nDiskWrite].n = n; diskWrite[nDiskWrite].failed = failed; memcpy(diskWrite[nDiskWrite].data, b, n); nDiskWrite++; } + else diskWriteOverflow = 1; + if (failed) + { + int k = (int) (rnd() % (unsigned) n); /* bytes that reach the disk before the error */ + if (k > 0) REAL(f)->pMethods->xWrite(REAL(f), b, k, o); + nFaultWrites++; + faultInjected = 1; + return SQLITE_FULL; + } + return REAL(f)->pMethods->xWrite(REAL(f), b, n, o); +} +static int spyTruncate(sqlite3_file* f, sqlite3_int64 s) { return REAL(f)->pMethods->xTruncate(REAL(f), s); } +static int spySync(sqlite3_file* f, int fl) { return REAL(f)->pMethods->xSync(REAL(f), fl); } +static int spyFileSize(sqlite3_file* f, sqlite3_int64* s) { return REAL(f)->pMethods->xFileSize(REAL(f), s); } +static int spyLock(sqlite3_file* f, int l) { return REAL(f)->pMethods->xLock(REAL(f), l); } +static int spyUnlock(sqlite3_file* f, int l) { return REAL(f)->pMethods->xUnlock(REAL(f), l); } +static int spyCheckReserved(sqlite3_file* f, int* r) { return REAL(f)->pMethods->xCheckReservedLock(REAL(f), r); } +static int spyFileControl(sqlite3_file* f, int op, void* a) { return REAL(f)->pMethods->xFileControl(REAL(f), op, a); } +static int spySectorSize(sqlite3_file* f) { return REAL(f)->pMethods->xSectorSize(REAL(f)); } +static int spyDevChar(sqlite3_file* f) { return REAL(f)->pMethods->xDeviceCharacteristics(REAL(f)); } +static const sqlite3_io_methods spyIo = { + 1, spyClose, spyRead, spyWrite, spyTruncate, spySync, spyFileSize, spyLock, spyUnlock, + spyCheckReserved, spyFileControl, spySectorSize, spyDevChar +}; +static int spyOpen(sqlite3_vfs* v, sqlite3_filename name, sqlite3_file* f, int flags, int* outFlags) +{ + SpyFile* p = (SpyFile*) f; + int rc; + (void) v; + p->real = (sqlite3_file*) &p[1]; + rc = realVfs->xOpen(realVfs, name, p->real, flags, outFlags); + p->base.pMethods = (rc == SQLITE_OK && p->real->pMethods) ? &spyIo : NULL; + return rc; +} +static int spyDelete(sqlite3_vfs* v, const char* n, int s) { (void) v; return realVfs->xDelete(realVfs, n, s); } +static int spyAccess(sqlite3_vfs* v, const char* n, int fl, int* r) { (void) v; return realVfs->xAccess(realVfs, n, fl, r); } +static int spyFullPath(sqlite3_vfs* v, const char* n, int nOut, char* out) { (void) v; return realVfs->xFullPathname(realVfs, n, nOut, out); } +static int spyRandomness(sqlite3_vfs* v, int n, char* out) { (void) v; return realVfs->xRandomness(realVfs, n, out); } +static int spySleep(sqlite3_vfs* v, int us) { (void) v; return realVfs->xSleep(realVfs, us); } +static int spyCurrentTime(sqlite3_vfs* v, double* t) { (void) v; return realVfs->xCurrentTime(realVfs, t); } +static int spyLastError(sqlite3_vfs* v, int n, char* e) { (void) v; return realVfs->xGetLastError(realVfs, n, e); } + +static void installSpy(void) +{ + realVfs = sqlite3_vfs_find(NULL); + memset(&spyVfs, 0, sizeof(spyVfs)); + spyVfs.iVersion = 1; spyVfs.szOsFile = (int) sizeof(SpyFile) + realVfs->szOsFile; spyVfs.mxPathname = realVfs->mxPathname; + spyVfs.zName = "spy"; spyVfs.xOpen = spyOpen; spyVfs.xDelete = spyDelete; spyVfs.xAccess = spyAccess; + spyVfs.xFullPathname = spyFullPath; spyVfs.xRandomness = spyRandomness; spyVfs.xSleep = spySleep; + spyVfs.xCurrentTime = spyCurrentTime; spyVfs.xGetLastError = spyLastError; + sqlite3_vfs_register(&spyVfs, 0); + sqlite3mc_vfs_create("spy", 1); +} + +/* +** Check the recorded disk writes of one xWrite (the model is already updated): +** counter values never go backwards, keystream never reused, also by failed +** writes; successful writes carry the encrypted plaintext +*/ +static int checkDiskWrites(long op) +{ + mcTempCipher* pTemp = ((sqlite3mc_file*) file)->tempCipher; + int slotSize = MC_TEMP_HEADER + blockSize; + int i, k; + if (!encrypted) { nDiskWrite = 0; return 0; } + if (diskWriteOverflow) { printf(" op %ld: more disk writes than the test records\n", op); return 1; } + for (i = 0; i < nDiskWrite; i++) + { + sqlite3_int64 iBlock = diskWrite[i].off / slotSize; + int rel = (int) (diskWrite[i].off % slotSize); /* position inside the block on disk */ + int iOff, n; + const unsigned char* payload; + sqlite3_uint64 counter; + unsigned char ks[MAX_BLOCK_SIZE], nonce[12]; + + if (iBlock >= MAX_BLOCKS) { printf(" op %ld: write beyond the model\n", op); return 1; } + if (rel == 0) + { + /* A block written from its start carries the counter value in front */ + if (diskWrite[i].n < MC_TEMP_HEADER) { printf(" op %ld: write shorter than the counter\n", op); return 1; } + counter = 0; + for (k = 0; k < 8; k++) counter |= ((sqlite3_uint64) diskWrite[i].data[k]) << (8 * k); + payload = diskWrite[i].data + MC_TEMP_HEADER; + iOff = 0; + n = diskWrite[i].n - MC_TEMP_HEADER; + } + else if (rel >= MC_TEMP_HEADER) + { + /* An append continues the block that is already on disk */ + counter = lastCounter[iBlock]; + payload = diskWrite[i].data; + iOff = rel - MC_TEMP_HEADER; + n = diskWrite[i].n; + if (counter == 0) { printf(" op %ld: append to block %lld without a counter on disk\n", op, (long long) iBlock); return 1; } + nAppendWrites++; + } + else { printf(" op %ld: write starts inside the counter\n", op); return 1; } + + if (iOff + n > blockSize) { printf(" op %ld: write crosses a block boundary\n", op); return 1; } + if (counter == 0 && rel == 0 && diskWrite[i].n == MC_TEMP_HEADER) + { + /* A truncate clears the counter value of a block behind the new end */ + lastCounter[iBlock] = 0; + memset(keystreamUsed[iBlock], 0, sizeof(keystreamUsed[iBlock])); + continue; + } + if (counter == 0) { printf(" op %ld: counter value 0 written\n", op); return 1; } + if (counter != lastCounter[iBlock]) + { + if (counter <= maxCounter) + { + printf(" op %ld: counter value %llu for block %lld was used before\n", op, (unsigned long long) counter, (long long) iBlock); + return 1; + } + maxCounter = counter; + lastCounter[iBlock] = counter; + memset(keystreamUsed[iBlock], 0, sizeof(keystreamUsed[iBlock])); + } + for (k = iOff; k < iOff + n; k++) + { + if (keystreamUsed[iBlock][k >> 3] & (1 << (k & 7))) + { + printf(" op %ld: keystream reused, block %lld counter %llu byte %d\n", op, (long long) iBlock, (unsigned long long) counter, k); + return 1; + } + keystreamUsed[iBlock][k >> 3] |= (unsigned char) (1 << (k & 7)); + } + if (diskWrite[i].failed) continue; + memset(nonce, 0, sizeof(nonce)); + for (k = 0; k < 8; k++) nonce[k] = (unsigned char) (counter >> (8 * k)); + memset(ks, 0, sizeof(ks)); + chacha20_xor(ks, sizeof(ks), pTemp->key, nonce, 0); + for (k = iOff; k < iOff + n; k++) + { + sqlite3_int64 pos = iBlock * blockSize + k; + unsigned char plain = (pos < modelSize) ? model[pos] : 0; + if (pos < MAX_FILE_SIZE && undefined[pos]) continue; + if (payload[k - iOff] != (unsigned char) (plain ^ ks[k])) + { + printf(" op %ld: byte %lld on disk is not the encrypted plaintext\n", op, (long long) pos); + return 1; + } + } + nWritesChecked++; + } + return 0; +} + +/* Does [off, off + n) touch a block that had a failed write? A write behind +** the end of the file also fills up the last block, so that one counts too. */ +static int mayReportError(sqlite3_int64 off, int n) +{ + sqlite3_int64 b; + if (!encrypted) return 0; + for (b = off / blockSize; b <= (off + n - 1) / blockSize && b < MAX_BLOCKS; b++) + if (blockFailed[b]) return 1; + if (off > modelSize && modelSize > 0 && blockFailed[(modelSize - 1) / blockSize]) return 1; + if ((off + n - 1) / blockSize >= firstStaleBlock) return 1; + return 0; +} + +/* +** One random write, possibly behind the end of the file or across a gap +*/ +static int doWrite(long op) +{ + sqlite3_int64 off = randOffset(), fsz = -1, b; + int n = randLen(), k, rc, failures = 0; + if (off + n > MAX_FILE_SIZE) n = (int) (MAX_FILE_SIZE - off); + for (k = 0; k < n; k++) writeBuf[k] = (unsigned char) rnd(); + nDiskWrite = 0; diskWriteOverflow = 0; + rc = file->pMethods->xWrite(file, writeBuf, n, off); + + if (rc != SQLITE_OK && faultMode && (faultInjected || (rc == SQLITE_IOERR_WRITE && mayReportError(off, n)))) + { + /* The range of a failed write is undefined, and the bytes between the old + ** and the new end of the file read as zeros */ + file->pMethods->xFileSize(file, &fsz); + if (fsz > modelSize) { memset(model + modelSize, 0, (size_t) (fsz - modelSize)); memset(undefined + modelSize, 0, (size_t) (fsz - modelSize)); } + if (off > modelSize) { memset(model + modelSize, 0, (size_t) (off - modelSize)); memset(undefined + modelSize, 0, (size_t) (off - modelSize)); } + memcpy(model + off, writeBuf, n); + memset(undefined + off, 1, n); + if (fsz > modelSize) modelSize = fsz; + for (b = off / blockSize; b <= (off + n - 1) / blockSize; b++) blockFailed[b] = 1; + if (off > modelSize && modelSize > 0) blockFailed[(modelSize - 1) / blockSize] = 1; /* filling up the last block may have failed */ + if (fsz / blockSize < firstStaleBlock) firstStaleBlock = fsz / blockSize; /* cleaning up behind the end may have failed */ + nWriteErrors++; + return checkDiskWrites(op); + } + if (rc != SQLITE_OK) + { + int lastErrno = 0; + file->pMethods->xFileControl(file, SQLITE_FCNTL_LAST_ERRNO, &lastErrno); + printf(" write failed at op %ld: rc %d, errno %d, offset %lld, length %d, file size %lld\n", + op, rc, lastErrno, (long long) off, n, (long long) modelSize); + return 1; + } + + if (off > modelSize) { memset(model + modelSize, 0, (size_t) (off - modelSize)); memset(undefined + modelSize, 0, (size_t) (off - modelSize)); } + memcpy(model + off, writeBuf, n); + memset(undefined + off, 0, n); + if (off + n > modelSize) modelSize = off + n; + for (b = (off + blockSize - 1) / blockSize; (b + 1) * blockSize <= off + n; b++) blockFailed[b] = 0; /* written completely, readable again */ + + file->pMethods->xFileSize(file, &fsz); + if (!sizeIsPlausible(fsz)) + { + printf(" op %ld: size %lld after write at %lld length %d, expected %lld\n", + op, (long long) fsz, (long long) off, n, (long long) modelSize); + failures++; + } + nWrite++; + return failures + checkDiskWrites(op); +} + +/* +** One random read, possibly behind the end of the file +*/ +static int doRead(long op) +{ + sqlite3_int64 off = randOffset(); + int n = randLen(), k, rc, expectShort; + if (off + n > MAX_FILE_SIZE) n = (int) (MAX_FILE_SIZE - off); + memset(readBuf, 0xAA, n); + rc = file->pMethods->xRead(file, readBuf, n, off); + + if (rc == SQLITE_IOERR_READ && faultMode && + (faultInjected || mayReportError(off, n) || + (encrypted && ((sqlite3mc_file*) file)->tempCipher->allBroken))) + { + nReadErrors++; + return 0; + } + expectShort = off + n > modelSize; + if ((expectShort && rc != SQLITE_IOERR_SHORT_READ && !(chunkFile && rc == SQLITE_OK)) || (!expectShort && rc != SQLITE_OK)) + { + printf(" read rc %d at op %ld (off %lld, n %d, size %lld)\n", rc, op, (long long) off, n, (long long) modelSize); + return 1; + } + for (k = 0; k < n; k++) + { + unsigned char want = (off + k < modelSize) ? model[off + k] : 0; + if (off + k < MAX_FILE_SIZE && off + k < modelSize && undefined[off + k]) continue; + if (readBuf[k] != want) + { + printf(" WRONG DATA WITHOUT ERROR at op %ld: offset %lld (read off %lld n %d, size %lld)\n", + op, (long long) (off + k), (long long) off, n, (long long) modelSize); + return 1; + } + } + nRead++; + if (expectShort) nShort++; + return 0; +} + +/* +** One truncate; SQLite only ever shortens a temporary file +*/ +static int doTruncate(long op) +{ + sqlite3_int64 size = modelSize > 0 ? (sqlite3_int64) (rnd() % (modelSize + 1)) : 0, fsz = -1, b; + int rc, failures = 0; + nDiskWrite = 0; diskWriteOverflow = 0; + rc = file->pMethods->xTruncate(file, size); + if (rc != SQLITE_OK) { printf(" truncate failed at op %ld: %d\n", op, rc); return 1; } + modelSize = size; + memset(undefined + size, 0, (size_t) (MAX_FILE_SIZE - size)); + if (faultMode && size / blockSize < firstStaleBlock) firstStaleBlock = size / blockSize; /* the cleanup behind the new end can fail too */ + for (b = (size + blockSize - 1) / blockSize; b < MAX_BLOCKS; b++) blockFailed[b] = 0; + + file->pMethods->xFileSize(file, &fsz); + if (!sizeIsPlausible(fsz)) { printf(" size %lld after truncate, expected %lld\n", (long long) fsz, (long long) modelSize); failures++; } + nTrunc++; + return failures + checkDiskWrites(op); +} + +int main(int argc, char** argv) +{ + long ops = argc > 1 ? atol(argv[1]) : 100000, i; + sqlite3_vfs* vfs; + int outFlags, rc, failures = 0; + int pageSize = 0, fileType = SQLITE_OPEN_TEMP_JOURNAL; + rngState = argc > 2 ? strtoull(argv[2], NULL, 10) : 0x9E3779B97F4A7C15ULL; + + sqlite3_initialize(); + installSpy(); + vfs = sqlite3_vfs_find(NULL); + file = (sqlite3_file*) sqlite3_malloc(vfs->szOsFile); + memset(file, 0, vfs->szOsFile); + for (i = 3; i < argc; i++) + { + if (strncmp(argv[i], "db", 2) == 0) { pageSize = atoi(argv[i] + 2); if (pageSize == 0) pageSize = 1024; fileType = SQLITE_OPEN_TEMP_DB; } + if (strcmp(argv[i], "anon") == 0) fileType = SQLITE_OPEN_MAIN_JOURNAL; + } + printf(" file opened without a name as %s\n", fileType == SQLITE_OPEN_TEMP_DB ? "temp db" : + fileType == SQLITE_OPEN_MAIN_JOURNAL ? "main journal" : "temp journal"); + rc = vfs->xOpen(vfs, NULL, file, fileType | SQLITE_OPEN_READWRITE | + SQLITE_OPEN_CREATE | SQLITE_OPEN_EXCLUSIVE | SQLITE_OPEN_DELETEONCLOSE, &outFlags); + if (rc != SQLITE_OK) { printf("open failed: %d\n", rc); return 1; } + for (i = 3; i < argc; i++) + { + if (strcmp(argv[i], "chunk") == 0) + { + int chunkSize = 65536; + file->pMethods->xFileControl(file, SQLITE_FCNTL_CHUNK_SIZE, &chunkSize); + chunkFile = 1; + printf(" SQLITE_FCNTL_CHUNK_SIZE 65536 requested\n"); + } + } + if (pageSize) + { + /* The first write of a database is a whole page */ + int k; + for (k = 0; k < pageSize; k++) model[k] = writeBuf[k] = (unsigned char) rnd(); + rc = file->pMethods->xWrite(file, writeBuf, pageSize, 0); + if (rc != SQLITE_OK) { printf("first page write failed: %d\n", rc); return 1; } + modelSize = pageSize; + } + encrypted = ((sqlite3mc_file*) file)->tempCipher != NULL; + printf(" VFS %s, %s encryption of the temporary file\n", vfs->zName, encrypted ? "with" : "without"); + if (encrypted) + { + blockSize = ((sqlite3mc_file*) file)->tempCipher->blockSize; + printf(" block size %d\n", blockSize); + if (pageSize) { nDiskWrite = 0; failures += checkDiskWrites(-1); } + } + for (i = 3; i < argc; i++) + { + if (strcmp(argv[i], "faults") == 0) + { + faultMode = 1; + printf(" injecting disk write and read faults\n"); + } + } + + for (i = 0; i < ops && failures < 5; i++) + { + int op = (int) (rnd() % 10); + faultInjected = 0; + if (op < 5) failures += doWrite(i); + else if (op < 9) failures += doRead(i); + else failures += doTruncate(i); + } + file->pMethods->xClose(file); + sqlite3_free(file); + printf(" %ld operations: %ld writes, %ld reads (%ld beyond the end), %ld truncates\n", i, nWrite, nRead, nShort, nTrunc); + if (faultMode) + printf(" faults: %ld disk writes and %ld disk reads failed; %ld writes and %ld reads reported an error\n", + nFaultWrites, nFaultReads, nWriteErrors, nReadErrors); + printf(" %ld disk writes checked against ChaCha20 and for keystream reuse, %ld of them appends with the same counter value\n", + nWritesChecked, nAppendWrites); + printf(failures ? "FAILED: %d failure(s)\n" : "OK: %d failure(s)\n", failures); + return failures != 0; +}