From f25f25aad60b59209b95be80a481268e0eae6ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Tro=C3=9Fbach?= Date: Fri, 18 Sep 2026 08:18:35 +0200 Subject: [PATCH 1/2] Encrypt temporary files with a random key per file Temporary files (temporary and transient databases, their journals, sorter files and statement journals) are opened without a name. They can't be linked to the codec of a database, so they were written in plaintext whenever temporary data went to disk. Encrypt every file opened without a name with ChaCha20 and a random key that exists only in memory while the file is open. The file is split into blocks, and every block is stored with the value of a counter in front of it. That counter is the nonce of the block and is read back from the file, so no state per block is kept in memory and a keystream is never used twice. Appends continue the keystream of the last block, so journals need no read-modify-write. A failed write never turns into wrong data: its bytes read as zeros, or the block reports an I/O error until it is written again. The compile-time option SQLITE3MC_ENCRYPT_TEMP_FILES turns this on or off. --- src/sqlite3mc_vfs.c | 498 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 496 insertions(+), 2 deletions(-) diff --git a/src/sqlite3mc_vfs.c b/src/sqlite3mc_vfs.c index 9ad35f4..d13ec1b 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); } From 2f767c5828588c4b2e68b017f00d890b5c091ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Tro=C3=9Fbach?= Date: Mon, 21 Sep 2026 10:06:55 +0200 Subject: [PATCH 2/2] Add a test for the encryption of temporary files The test writes to, reads from and truncates an anonymous temporary file at random and compares every result with a model of the file. A second VFS below checks the writes on their way to the disk: every byte must be the plaintext XOR the keystream of the counter value that is stored in front of its block, counter values never go backwards, and no keystream byte is used twice, not even after a failed write. Disk errors can be injected in the middle of a write, and a wrong value without an error is a failure. In a build without the encryption of temporary files, the test checks the model only. --- .github/workflows/ci4sqlite3mc.yml | 8 + Makefile.am | 6 +- test/tempfiletest.c | 492 +++++++++++++++++++++++++++++ 3 files changed, 505 insertions(+), 1 deletion(-) create mode 100644 test/tempfiletest.c diff --git a/.github/workflows/ci4sqlite3mc.yml b/.github/workflows/ci4sqlite3mc.yml index 8c7fc99..634aa23 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 3ab094e..fae8fa4 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/test/tempfiletest.c b/test/tempfiletest.c new file mode 100644 index 0000000..17e22da --- /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; +}