Repository navigation
macOS: cross-process WAL writer lock (flock/fcntl) fails on FileStream-derived fd #134
Description
Activity
Still failing on 5.1.1, i.e. with the
flock()change from 2026-08-22 in place. macOS 27 arm64, .NET 10.0.12,PageFileConfig { AllowMultiProcessAccess = true }.Setup: two processes start at the same time. Each opens the same database and writes 20 batches of 50 documents, one explicit transaction per batch (
InsertAsync(entity, txn)+CommitAsync()).1. Database file does not exist yet: one process always crashes while opening. 18 of 18 rounds. The other process commits all 1,000 documents, and no committed document is lost. The crash comes in one of three forms:
Fatal error. System.AccessViolationException: Attempted to read or write protected memory. at System.SpanHelpers.Memmove(Byte ByRef, Byte ByRef, UIntPtr) at BLite.Core.Storage.PageFile.ReadPageCore(UInt32, System.Span`1<Byte>) at BLite.Core.Storage.PageFile.ReadPage(UInt32, System.Span`1<Byte>) at BLite.Core.Storage.StorageEngine.ReadPage(UInt32, System.Nullable`1<UInt64>, System.Span`1<Byte>) at BLite.Core.Storage.StorageEngine.CollectSchemaPageBytes(UInt32) at BLite.Core.Storage.StorageEngine.GetSchemas(UInt32) at BLite.Core.Collections.DocumentCollection`2..EnsureSchema() at BLite.Core.DocumentDbContext.CreateCollection(...)System.UnauthorizedAccessException: Attempted to perform an unauthorized operation. at System.IO.MemoryMappedFiles.MemoryMappedFile.CreateViewAccessor(Int64 offset, Int64 size, MemoryMappedFileAccess access) at BLite.Core.Storage.PageFile.WritePageCore(UInt32 pageId, ReadOnlySpan`1 source) at BLite.Core.Storage.StorageEngine.WritePageImmediate(UInt32 pageId, ReadOnlySpan`1 data) at BLite.Core.Storage.StorageEngine.InitializeDictionary() at BLite.Core.Storage.StorageEngine..ctor(String databasePath, PageFileConfig config)System.IndexOutOfRangeException: Index was outside the bounds of the array.The
AccessViolationExceptioncannot be caught and terminates the process. Creating and initialising a new file does not seem to be covered by the cross-process lock at all.2. Database file exists (created by an earlier single-process run): neither process commits anything. 3 of 3 rounds. Both fail on their first
CommitAsync():System.TimeoutException: Timed out acquiring cross-process WAL writer lock (.wal-shm). at BLite.Core.Storage.StorageEngine.CommitTransactionAsync(UInt64 transactionId, CancellationToken ct) at BLite.Core.Transactions.Transaction.CommitAsync(CancellationToken ct)A single process with the same settings on the same file commits normally. Two writers never make progress, so this looks like either the lock not being released between the two processes or both of them waiting on each other.
Repro harness (single file, ~200 lines, synthetic data) available on request.
Reacted by MrDevRobot- added a commit that references this issue
on Oct 4, 2026
Summary
3 tests in
MultiProcessWalSharedMemoryTestsfail on macOS (confirmed on Darwin 25.5.0 / arm64) with a locking failure that is not present on Linux CI. Root cause is deeper than a single wrong constant and isn't fully resolved yet.What's already fixed (see commit ccda67b)
WalShmFcntl.csused the Linux value ofF_WRLCK(1) unchanged on macOS, where1is actuallyF_RDLCK(macOSF_WRLCKis3, verified against the SDK'ssys/fcntl.h). Every "write" lock request on macOS was silently taking a read lock instead — a real, independent bug, now fixed.Separately,
fcntl(F_SETLK)was replaced withflock()on macOS after finding thatfcntl(F_SETLK)reliably returnsEINVALwhen called on a file descriptor obtained from a .NETFileStreamon this platform — reproduced with a bare P/Invoke +FileStreamprobe, independent of BLite entirely. A rawlibcopen()-backed descriptor is unaffected; onlyFileStream-derived descriptors trigger it, and it happens regardless of the lock type/offset passed.What's still broken
Switching to
flock()fixed 10 of the 13 originally-failing tests, but exposed a different problem: callingflock()manually on aFileStream's underlying fd appears to conflict with something in .NET's own UnixFileStreamimplementation. Isolated with a minimal probe:FileStreamopens on the same path (FileShare.ReadWrite, no manual locking at all) succeed concurrently — fine.flock(fdA, LOCK_EX | LOCK_NB)is taken on the firstFileStream's fd, a second, independentFileStream.Openon the same path then throwsIOException: ... because it is being used by another process— even though the second open only needsFileShare.ReadWriteand isn't attempting to lock anything itself.This suggests .NET's Unix
FileStreamopen path consults/participates in the file'sflock()state on macOS, and a lock taken manually on one fd is visible to (and blocks) an unrelatedFileStream.Openon the same path. That's a different, more invasive problem than the wrong constant — it looks likeWalSharedMemory's current design (callfcntl/flockdirectly on the fd owned by itsFileStream) may not be safe on macOS at all, regardless of which syscall is used.Suspect: OS build
This machine runs Darwin 25.5.0 (arm64) — a very recent, likely pre-release build as of August 2026. Not yet confirmed whether this reproduces on a stable, publicly-released macOS version. If it doesn't, this may be specific to this beta build rather than a general macOS issue.
Failing tests (as of commit ccda67b)
MultiProcessWalSharedMemoryTests.WriterLock_IsMutualExclusion_AcrossInstancesMultiProcessWalSharedMemoryTests.BeginTransaction_Phase7_ReplaysCrossProcessCommitsdotnet test --filter FullyQualifiedName~MultiProcessWalSharedMemoryTests)Practical impact
Only affects
AllowMultiProcessAccess = true(opt-in, defaults tofalse). Not exercised by CI (ubuntu-latestonly) — this is why the existing 5.0.6 release shipped despite the (pre-fix) failures never being caught.Possible fix direction (not yet implemented)
Open a separate, raw native fd via
open()(bypassingFileStreamentirely) dedicated to holding the advisory lock, while keeping the existingFileStreamfor actual I/O on the.wal-shmfile. Confirmed via probe that a rawopen()-backed fd does not hit either thefcntlEINVALor theflock/FileStream.Openconflict. This would require restructuring howWalSharedMemoryopens its backing file on macOS, not justWalShmFcntl.cs.