Package version
5.0.3
Affected package
BLite.Core (storage engine)
.NET version
10.0.201
Description
Summary
When DocumentDbContext fails during construction (while reading pages and initializing collections), BLite does not release the underlying page file and memory-mapped resources. The database file (and related WAL sidecar files) remain locked by the process even though no usable DocumentDbContext instance is returned to the caller.
This report is not about the root cause of the exception that triggers the failure. The trigger can be any error during page read or collection bootstrap inside the constructor. In our case, the visible exception is an AES-GCM decryption failure when the configured ICryptoProvider password does not match the encrypted store. The bug we are reporting is the missing cleanup on constructor failure, which prevents deleting or replacing the database file afterward.
Configuration
var config = new PageFileConfig
{
PageSize = PageFileConfig.Default.PageSize,
GrowthBlockSize = PageFileConfig.Default.GrowthBlockSize,
Access = MemoryMappedFileAccess.ReadWrite,
AllowMultiProcessAccess = true,
LockTimeout = LockTimeout.From(
(int)TimeSpan.FromSeconds(15).TotalMilliseconds,
(int)TimeSpan.FromSeconds(30).TotalMilliseconds
),
CryptoProvider = new AesGcmCryptoProvider(new CryptoOptions(password))
};
// Throws during construction; no instance is returned
var context = new DocumentDbContext(databasePath, config);
Impact
| Area |
Effect |
| Startup recovery |
Host applications that delete and recreate the store after a failed open cannot do so reliably in-process. |
| Multi-process access |
With AllowMultiProcessAccess = true, a failed open in one process can leave files locked and affect sibling processes sharing the same path. |
| Consumer workarounds |
Callers are forced to use GC.Collect(), GC.WaitForPendingFinalizers(), rename-to-quarantine fallbacks, or process restart — none of which should be required for correct library behavior. |
Minimal reproduction
- Open or create an encrypted database with
AesGcmCryptoProvider and password A.
- Insert at least one document and call
CheckpointAsync so collection metadata pages are written.
- Dispose the context normally (
await DisposeAsync()).
- Attempt to open the same database path with password B (incorrect password).
- Observe that
new DocumentDbContext(path, config) throws before the constructor completes.
- Without terminating the process, try to delete the database file (and
.wal / -shm sidecars if present).
Expected behavior
DocumentDbContext construction should follow a fail-safe pattern:
- If any step after
PageFile / StorageEngine initialization throws, all already-acquired native and managed resources (PageFile, memory-mapped views, WAL handles, StorageEngine, collection managers, etc.) must be disposed before the exception propagates to the caller.
- After the constructor throws, the OS should not retain an exclusive lock on the database file from that failed attempt.
- Callers should be able to delete or replace the database file in the same process without relying on the finalizer thread or
GC.Collect().
This is analogous to the standard IDisposable / IAsyncDisposable contract: a half-initialized object must not leak handles when construction fails.
Actual behavior
Actual: File.Delete fails with IOException — the file is still in use by the current process. The lock persists until the process exits (or until a non-deterministic GC/finalizer cycle eventually releases handles).
Expected: The file can be deleted immediately after the constructor throws, because no open database instance exists.
Additional context
Observed exception (trigger only — not the reported bug)
The following stack trace shows where construction fails in our scenario. Any similar failure during InitializeCollections() / ReadPage would exhibit the same resource leak.
System.Security.Cryptography.AuthenticationTagMismatchException:
The computed authentication tag did not match the input authentication tag.
at System.Security.Cryptography.AeadCommon.Decrypt(...)
at System.Security.Cryptography.AesGcm.Decrypt(...)
at BLite.Core.Encryption.AesGcmCryptoProvider.Decrypt(
UInt32 pageId, ReadOnlySpan`1 ciphertext, Span`1 plaintext)
at BLite.Core.Storage.PageFile.ReadPageCore(UInt32 pageId, Span`1 destination)
at BLite.Core.Storage.PageFile.ReadPage(UInt32 pageId, Span`1 destination)
at BLite.Core.Storage.StorageEngine.ReadPage(
UInt32 pageId, Nullable`1 transactionId, Span`1 destination)
at BLite.Core.Storage.StorageEngine.GetCollectionMetadata(String name)
at BLite.Core.Indexing.CollectionIndexManager`2..ctor(
StorageEngine storage, IDocumentMapper`2 mapper, String collectionName)
at BLite.Core.Collections.DocumentCollection`2..ctor(
StorageEngine storage, ITransactionHolder transactionHolder,
IDocumentMapper`2 mapper, String collectionName, FreeSpaceIndex freeSpaceIndex)
at BLite.Core.DocumentDbContext.CreateCollection[TId,T](IDocumentMapper`2 mapper)
at BLite.Core.DocumentDbContext.InitializeCollections()
at BLite.Core.DocumentDbContext..ctor(
String databasePath, PageFileConfig config, BLiteKvOptions kvOptions)
at BLite.Core.DocumentDbContext..ctor(String databasePath, PageFileConfig config)
Suggested fix direction
- Wrap
DocumentDbContext constructor body (page file open → storage engine → InitializeCollections()) in try/finally or use a private initialization helper that disposes partial state on any exception.
- Ensure
PageFile.Dispose() / DisposeAsync() releases the memory-mapped file even when opened but not fully initialized.
- Add a regression test: open encrypted store with wrong password → assert constructor throws → assert database file is not locked (e.g.
File.Delete succeeds in the same process).
Package version
5.0.3
Affected package
BLite.Core (storage engine)
.NET version
10.0.201
Description
Summary
When
DocumentDbContextfails during construction (while reading pages and initializing collections), BLite does not release the underlying page file and memory-mapped resources. The database file (and related WAL sidecar files) remain locked by the process even though no usableDocumentDbContextinstance is returned to the caller.This report is not about the root cause of the exception that triggers the failure. The trigger can be any error during page read or collection bootstrap inside the constructor. In our case, the visible exception is an AES-GCM decryption failure when the configured
ICryptoProviderpassword does not match the encrypted store. The bug we are reporting is the missing cleanup on constructor failure, which prevents deleting or replacing the database file afterward.Configuration
Impact
AllowMultiProcessAccess = true, a failed open in one process can leave files locked and affect sibling processes sharing the same path.GC.Collect(),GC.WaitForPendingFinalizers(), rename-to-quarantine fallbacks, or process restart — none of which should be required for correct library behavior.Minimal reproduction
AesGcmCryptoProviderand password A.CheckpointAsyncso collection metadata pages are written.await DisposeAsync()).new DocumentDbContext(path, config)throws before the constructor completes..wal/-shmsidecars if present).Expected behavior
DocumentDbContextconstruction should follow a fail-safe pattern:PageFile/StorageEngineinitialization throws, all already-acquired native and managed resources (PageFile, memory-mapped views, WAL handles,StorageEngine, collection managers, etc.) must be disposed before the exception propagates to the caller.GC.Collect().This is analogous to the standard IDisposable / IAsyncDisposable contract: a half-initialized object must not leak handles when construction fails.
Actual behavior
Actual:
File.Deletefails withIOException— the file is still in use by the current process. The lock persists until the process exits (or until a non-deterministic GC/finalizer cycle eventually releases handles).Expected: The file can be deleted immediately after the constructor throws, because no open database instance exists.
Additional context
Observed exception (trigger only — not the reported bug)
The following stack trace shows where construction fails in our scenario. Any similar failure during
InitializeCollections()/ReadPagewould exhibit the same resource leak.Suggested fix direction
DocumentDbContextconstructor body (page file open → storage engine →InitializeCollections()) intry/finallyor use a private initialization helper that disposes partial state on any exception.PageFile.Dispose()/DisposeAsync()releases the memory-mapped file even when opened but not fully initialized.File.Deletesucceeds in the same process).