Shared file access: FTell(), FEof() and all RDD record positioning use the wrong file offset on .NET 6 and later
Summary
XsWin32FileStream never overrides Position. Since the .NET 6 FileStream rewrite that makes FTell(), FEof(), FReadLine() and — worse — the whole RDD record positioning path (SafeReadAt() / SafeWriteAt() / SafeSetPos()) read and write at the wrong file offset, silently and without an exception, for every file opened with shared access.
XsWin32FileStream does all of its I/O with the Win32 API on the OS file pointer (SetFilePointerEx + ReadFile / WriteFile), but Position falls through to the base class. On .NET Framework that worked: touching SafeFileHandle set _exposedHandle, and FileStream then re-synced its cached _pos from the OS handle on every access (VerifyOSHandlePosition). The .NET 6 FileStream strategy rewrite removed that re-sync, so FileStream.Position is now a purely in-memory counter that the overrides in XsWin32FileStream never touch. It stays at 0 forever while the real file pointer moves.
Affected
Any file opened with share != FileShare.None — that is FO_SHARED, FO_DENYWRITE or FO_DENYREAD — on Windows. That is the branch in XsFileStream.CreateFileStream() which returns CreateWin32FileStream(). It includes the DBF, memo and index files opened by the RDDs.
| Runtime |
Result |
| .NET Framework 4.8 |
OK |
| .NET Core 3.1 |
OK |
| .NET 5 |
OK |
| .NET 6 |
broken |
| .NET 8 |
broken |
| .NET 10 |
broken |
So this is not a .NET 10 regression. It started with .NET 6 and is still present in .NET 10.
Reproduction
FUNCTION Start() AS VOID
LOCAL cFile := "fstest.txt" AS STRING
MemoWrit(cFile, "Hello World line1" + CRLF + "second line here" + CRLF)
VAR h := FOpen(cFile, FO_READWRITE + FO_SHARED) // -> XsWin32FileStream
FSeek3(h, 6, FS_SET)
? FTell(h) // expected 6 .NET 6+: 0
? FReadStr(h, 5) // expected "World" ok
? FTell(h) // expected 11 .NET 6+: 0
FSeek3(h, 0, FS_SET)
? FReadLine(h, 100) // expected "Hello World line1" ok
? FReadLine(h, 100) // expected "second line here" .NET 6+: ""
? FEof(h) // expected TRUE at end .NET 6+: FALSE
FClose(h)
RETURN
Measured against the same XSharp.Core sources, only the host runtime differs:
.NET Framework 4.8 .NET 10.0.11
OK FTell after FSeek(6) = 6 FAIL FTell after FSeek(6) = 0
OK FTell after FRead = 11 FAIL FTell after FRead = 0
OK FReadLine #2 = "second line here" FAIL FReadLine #2 = ""
OK FEof at end = TRUE FAIL FEof at end = FALSE
The serious part: silent data corruption in the RDDs
SafeReadAt(), SafeWriteAt() and SafeSetPos() in FileStreamExensions position by assigning oStream:Position and then calling Read() / Write(). That is how the entire RDD layer addresses records (DbtMemo, FlexArea, DBF headers). With a broken Position setter the assignment updates only the base class counter, while Read() / Write() still operate at the untouched OS file pointer:
.NET Framework 4.8 .NET 10.0.11
OK SafeReadAt(6,5) = "World" FAIL SafeReadAt(6,5) = "Hello"
OK SafeSetPos(0)+SafeRead = "Hello" FAIL SafeSetPos(0)+SafeRead = " Worl"
No exception is raised. Reads and writes simply land at the wrong offset.
Further defects found in the same class
All in src/Runtime/XSharp.Core/Types/SharedFileStream.prg:
ReadByte() is not overridden. .NET Core's FileStream overrides it and goes straight to its own strategy, bypassing the Read() override, so it returns the byte from the wrong position. The same applies to Read(Span<byte>), Write(ReadOnlySpan<byte>) and the async overloads, which .NET Core also overrides.
Read() with offset != 0 copies count bytes instead of the number of bytes actually read. On a short read that overwrites bytes in the caller's buffer past the end of the file with zeroes from the temporary buffer, and it throws when the target buffer is only large enough for offset + bytesRead.
Read() returns -1 on failure, which violates the Stream.Read contract (the result must be between 0 and count) and breaks generic consumers such as Stream.CopyTo and StreamReader.
- The base stream is constructed with a 64 KB buffer (
bufferSize 0xFFFF). On .NET 6+ that wraps it in a BufferedFileStreamStrategy. The overrides never use that buffer, but every code path that is not overridden reads through it and therefore sees stale data, and Flush(lCommit) never calls SUPER:Flush(), so anything sitting in the buffer is silently dropped. A stream for shared access should not be buffered at all.
Shared file access: FTell(), FEof() and all RDD record positioning use the wrong file offset on .NET 6 and later
Summary
XsWin32FileStreamnever overridesPosition. Since the .NET 6FileStreamrewrite that makesFTell(),FEof(),FReadLine()and — worse — the whole RDD record positioning path (SafeReadAt()/SafeWriteAt()/SafeSetPos()) read and write at the wrong file offset, silently and without an exception, for every file opened with shared access.XsWin32FileStreamdoes all of its I/O with the Win32 API on the OS file pointer (SetFilePointerEx+ReadFile/WriteFile), butPositionfalls through to the base class. On .NET Framework that worked: touchingSafeFileHandleset_exposedHandle, andFileStreamthen re-synced its cached_posfrom the OS handle on every access (VerifyOSHandlePosition). The .NET 6FileStreamstrategy rewrite removed that re-sync, soFileStream.Positionis now a purely in-memory counter that the overrides inXsWin32FileStreamnever touch. It stays at 0 forever while the real file pointer moves.Affected
Any file opened with
share != FileShare.None— that isFO_SHARED,FO_DENYWRITEorFO_DENYREAD— on Windows. That is the branch inXsFileStream.CreateFileStream()which returnsCreateWin32FileStream(). It includes the DBF, memo and index files opened by the RDDs.So this is not a .NET 10 regression. It started with .NET 6 and is still present in .NET 10.
Reproduction
Measured against the same
XSharp.Coresources, only the host runtime differs:The serious part: silent data corruption in the RDDs
SafeReadAt(),SafeWriteAt()andSafeSetPos()inFileStreamExensionsposition by assigningoStream:Positionand then callingRead()/Write(). That is how the entire RDD layer addresses records (DbtMemo,FlexArea, DBF headers). With a brokenPositionsetter the assignment updates only the base class counter, whileRead()/Write()still operate at the untouched OS file pointer:No exception is raised. Reads and writes simply land at the wrong offset.
Further defects found in the same class
All in
src/Runtime/XSharp.Core/Types/SharedFileStream.prg:ReadByte()is not overridden. .NET Core'sFileStreamoverrides it and goes straight to its own strategy, bypassing theRead()override, so it returns the byte from the wrong position. The same applies toRead(Span<byte>),Write(ReadOnlySpan<byte>)and the async overloads, which .NET Core also overrides.Read()withoffset != 0copiescountbytes instead of the number of bytes actually read. On a short read that overwrites bytes in the caller's buffer past the end of the file with zeroes from the temporary buffer, and it throws when the target buffer is only large enough foroffset + bytesRead.Read()returns-1on failure, which violates theStream.Readcontract (the result must be between 0 andcount) and breaks generic consumers such asStream.CopyToandStreamReader.bufferSize0xFFFF). On .NET 6+ that wraps it in aBufferedFileStreamStrategy. The overrides never use that buffer, but every code path that is not overridden reads through it and therefore sees stale data, andFlush(lCommit)never callsSUPER:Flush(), so anything sitting in the buffer is silently dropped. A stream for shared access should not be buffered at all.