Skip to content

Commit 49930c3

Browse files
adamsitnikNicoAvanzDevam11stephentoubMichalPetryka
authored
[release/9.0-staging] Fix few RandomAccess.Write edge case bugs (#109646)
Co-authored-by: NicoAvanzDev <[email protected]> Co-authored-by: Adeel Mujahid <[email protected]> Co-authored-by: Stephen Toub <[email protected]> Co-authored-by: Michał Petryka <[email protected]>
1 parent 3dd878b commit 49930c3

File tree

4 files changed

+254
-33
lines changed

4 files changed

+254
-33
lines changed

src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -168,37 +168,30 @@ internal static unsafe void WriteGatherAtOffset(SafeFileHandle handle, IReadOnly
168168

169169
var handles = new MemoryHandle[buffersCount];
170170
Span<Interop.Sys.IOVector> vectors = buffersCount <= IovStackThreshold ?
171-
stackalloc Interop.Sys.IOVector[IovStackThreshold] :
171+
stackalloc Interop.Sys.IOVector[IovStackThreshold].Slice(0, buffersCount) :
172172
new Interop.Sys.IOVector[buffersCount];
173173

174174
try
175175
{
176-
int buffersOffset = 0, firstBufferOffset = 0;
177-
while (true)
176+
long totalBytesToWrite = 0;
177+
for (int i = 0; i < buffersCount; i++)
178178
{
179-
long totalBytesToWrite = 0;
180-
181-
for (int i = buffersOffset; i < buffersCount; i++)
182-
{
183-
ReadOnlyMemory<byte> buffer = buffers[i];
184-
totalBytesToWrite += buffer.Length;
185-
186-
MemoryHandle memoryHandle = buffer.Pin();
187-
vectors[i] = new Interop.Sys.IOVector { Base = firstBufferOffset + (byte*)memoryHandle.Pointer, Count = (UIntPtr)(buffer.Length - firstBufferOffset) };
188-
handles[i] = memoryHandle;
189-
190-
firstBufferOffset = 0;
191-
}
179+
ReadOnlyMemory<byte> buffer = buffers[i];
180+
totalBytesToWrite += buffer.Length;
192181

193-
if (totalBytesToWrite == 0)
194-
{
195-
break;
196-
}
182+
MemoryHandle memoryHandle = buffer.Pin();
183+
vectors[i] = new Interop.Sys.IOVector { Base = (byte*)memoryHandle.Pointer, Count = (UIntPtr)buffer.Length };
184+
handles[i] = memoryHandle;
185+
}
197186

187+
int buffersOffset = 0;
188+
while (totalBytesToWrite > 0)
189+
{
198190
long bytesWritten;
199-
fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(vectors))
191+
Span<Interop.Sys.IOVector> left = vectors.Slice(buffersOffset);
192+
fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(left))
200193
{
201-
bytesWritten = Interop.Sys.PWriteV(handle, pinnedVectors, buffersCount, fileOffset);
194+
bytesWritten = Interop.Sys.PWriteV(handle, pinnedVectors, left.Length, fileOffset);
202195
}
203196

204197
FileStreamHelpers.CheckFileCall(bytesWritten, handle.Path);
@@ -208,22 +201,31 @@ internal static unsafe void WriteGatherAtOffset(SafeFileHandle handle, IReadOnly
208201
}
209202

210203
// The write completed successfully but for fewer bytes than requested.
204+
// We need to perform next write where the previous one has finished.
205+
fileOffset += bytesWritten;
206+
totalBytesToWrite -= bytesWritten;
211207
// We need to try again for the remainder.
212-
for (int i = 0; i < buffersCount; i++)
208+
while (buffersOffset < buffersCount && bytesWritten > 0)
213209
{
214-
int n = buffers[i].Length;
210+
int n = (int)vectors[buffersOffset].Count;
215211
if (n <= bytesWritten)
216212
{
217-
buffersOffset++;
218213
bytesWritten -= n;
219-
if (bytesWritten == 0)
220-
{
221-
break;
222-
}
214+
buffersOffset++;
223215
}
224216
else
225217
{
226-
firstBufferOffset = (int)(bytesWritten - n);
218+
// A partial read: the vector needs to point to the new offset.
219+
// But that offset needs to be relative to the previous attempt.
220+
// Example: we have a single buffer with 30 bytes and the first read returned 10.
221+
// The next read should try to read the remaining 20 bytes, but in case it also reads just 10,
222+
// the third attempt should read last 10 bytes (not 20 again).
223+
Interop.Sys.IOVector current = vectors[buffersOffset];
224+
vectors[buffersOffset] = new Interop.Sys.IOVector
225+
{
226+
Base = current.Base + (int)(bytesWritten),
227+
Count = current.Count - (UIntPtr)(bytesWritten)
228+
};
227229
break;
228230
}
229231
}

src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ internal static long ReadScatterAtOffset(SafeFileHandle handle, IReadOnlyList<Me
435435
internal static void WriteGatherAtOffset(SafeFileHandle handle, IReadOnlyList<ReadOnlyMemory<byte>> buffers, long fileOffset)
436436
{
437437
// WriteFileGather does not support sync handles, so we just call WriteFile in a loop
438-
int bytesWritten = 0;
438+
long bytesWritten = 0;
439439
int buffersCount = buffers.Count;
440440
for (int i = 0; i < buffersCount; i++)
441441
{

src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/WriteGatherAsync.cs

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Buffers;
45
using System.Collections.Generic;
56
using System.Linq;
67
using System.Security.Cryptography;
78
using System.Threading;
89
using System.Threading.Tasks;
10+
using Microsoft.DotNet.XUnitExtensions;
911
using Microsoft.Win32.SafeHandles;
1012
using Xunit;
1113

1214
namespace System.IO.Tests
1315
{
1416
[SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")]
17+
[Collection(nameof(DisableParallelization))] // don't run in parallel, as some of these tests use a LOT of resources
1518
public class RandomAccess_WriteGatherAsync : RandomAccess_Base<ValueTask>
1619
{
1720
protected override ValueTask MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset)
@@ -133,5 +136,172 @@ public async Task DuplicatedBufferDuplicatesContentAsync(FileOptions options)
133136
Assert.Equal(repeatCount, actualContent.Length);
134137
Assert.All(actualContent, actual => Assert.Equal(value, actual));
135138
}
139+
140+
[OuterLoop("It consumes a lot of resources (disk space and memory).")]
141+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.Is64BitProcess), nameof(PlatformDetection.IsReleaseRuntime))]
142+
[InlineData(false, false)]
143+
[InlineData(false, true)]
144+
[InlineData(true, true)]
145+
[InlineData(true, false)]
146+
public async Task NoInt32OverflowForLargeInputs(bool asyncFile, bool asyncMethod)
147+
{
148+
// We need to write more than Int32.MaxValue bytes to the disk to reproduce the problem.
149+
// To reduce the number of used memory, we allocate only one write buffer and simply repeat it multiple times.
150+
// For reading, we need unique buffers to ensure that all of them are getting populated with the right data.
151+
152+
const int BufferCount = 1002;
153+
const int BufferSize = int.MaxValue / 1000;
154+
const long FileSize = (long)BufferCount * BufferSize;
155+
string filePath = GetTestFilePath();
156+
157+
FileOptions options = asyncFile ? FileOptions.Asynchronous : FileOptions.None; // we need to test both code paths
158+
options |= FileOptions.DeleteOnClose;
159+
160+
SafeFileHandle? sfh;
161+
try
162+
{
163+
sfh = File.OpenHandle(filePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, options, preallocationSize: FileSize);
164+
}
165+
catch (IOException)
166+
{
167+
throw new SkipTestException("Not enough disk space.");
168+
}
169+
170+
using (sfh)
171+
{
172+
ReadOnlyMemory<byte> writeBuffer = RandomNumberGenerator.GetBytes(BufferSize);
173+
List<ReadOnlyMemory<byte>> writeBuffers = Enumerable.Repeat(writeBuffer, BufferCount).ToList();
174+
175+
List<NativeMemoryManager> memoryManagers = new List<NativeMemoryManager>(BufferCount);
176+
List<Memory<byte>> readBuffers = new List<Memory<byte>>(BufferCount);
177+
178+
try
179+
{
180+
try
181+
{
182+
for (int i = 0; i < BufferCount; i++)
183+
{
184+
// We are using native memory here to get OOM as soon as possible.
185+
NativeMemoryManager nativeMemoryManager = new(BufferSize);
186+
memoryManagers.Add(nativeMemoryManager);
187+
readBuffers.Add(nativeMemoryManager.Memory);
188+
}
189+
}
190+
catch (OutOfMemoryException)
191+
{
192+
throw new SkipTestException("Not enough memory.");
193+
}
194+
195+
await Verify(asyncMethod, FileSize, sfh, writeBuffer, writeBuffers, readBuffers);
196+
}
197+
finally
198+
{
199+
foreach (IDisposable memoryManager in memoryManagers)
200+
{
201+
memoryManager.Dispose();
202+
}
203+
}
204+
}
205+
206+
static async Task Verify(bool asyncMethod, long FileSize, SafeFileHandle sfh, ReadOnlyMemory<byte> writeBuffer, List<ReadOnlyMemory<byte>> writeBuffers, List<Memory<byte>> readBuffers)
207+
{
208+
if (asyncMethod)
209+
{
210+
await RandomAccess.WriteAsync(sfh, writeBuffers, 0);
211+
}
212+
else
213+
{
214+
RandomAccess.Write(sfh, writeBuffers, 0);
215+
}
216+
217+
Assert.Equal(FileSize, RandomAccess.GetLength(sfh));
218+
219+
long fileOffset = 0;
220+
while (fileOffset < FileSize)
221+
{
222+
long bytesRead = asyncMethod
223+
? await RandomAccess.ReadAsync(sfh, readBuffers, fileOffset)
224+
: RandomAccess.Read(sfh, readBuffers, fileOffset);
225+
226+
Assert.InRange(bytesRead, 0, FileSize);
227+
228+
while (bytesRead > 0)
229+
{
230+
Memory<byte> readBuffer = readBuffers[0];
231+
if (bytesRead >= readBuffer.Length)
232+
{
233+
AssertExtensions.SequenceEqual(writeBuffer.Span, readBuffer.Span);
234+
235+
bytesRead -= readBuffer.Length;
236+
fileOffset += readBuffer.Length;
237+
238+
readBuffers.RemoveAt(0);
239+
}
240+
else
241+
{
242+
// A read has finished somewhere in the middle of one of the read buffers.
243+
// Example: buffer had 30 bytes and only 10 were read.
244+
// We don't read the missing part, but try to read the whole buffer again.
245+
// It's not optimal from performance perspective, but it keeps the test logic simple.
246+
break;
247+
}
248+
}
249+
}
250+
}
251+
}
252+
253+
[Theory]
254+
[InlineData(false, false)]
255+
[InlineData(false, true)]
256+
[InlineData(true, true)]
257+
[InlineData(true, false)]
258+
public async Task IovLimitsAreRespected(bool asyncFile, bool asyncMethod)
259+
{
260+
// We need to write and read more than IOV_MAX buffers at a time.
261+
// IOV_MAX typical value is 1024.
262+
const int BufferCount = 1026;
263+
const int BufferSize = 1; // the less resources we use, the better
264+
const int FileSize = BufferCount * BufferSize;
265+
266+
ReadOnlyMemory<byte> writeBuffer = RandomNumberGenerator.GetBytes(BufferSize);
267+
ReadOnlyMemory<byte>[] writeBuffers = Enumerable.Repeat(writeBuffer, BufferCount).ToArray();
268+
Memory<byte>[] readBuffers = Enumerable.Range(0, BufferCount).Select(_ => new byte[BufferSize].AsMemory()).ToArray();
269+
270+
FileOptions options = asyncFile ? FileOptions.Asynchronous : FileOptions.None; // we need to test both code paths
271+
options |= FileOptions.DeleteOnClose;
272+
273+
using SafeFileHandle sfh = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, options);
274+
275+
if (asyncMethod)
276+
{
277+
await RandomAccess.WriteAsync(sfh, writeBuffers, 0);
278+
}
279+
else
280+
{
281+
RandomAccess.Write(sfh, writeBuffers, 0);
282+
}
283+
284+
Assert.Equal(FileSize, RandomAccess.GetLength(sfh));
285+
286+
long fileOffset = 0;
287+
int bufferOffset = 0;
288+
while (fileOffset < FileSize)
289+
{
290+
ArraySegment<Memory<byte>> left = new ArraySegment<Memory<byte>>(readBuffers, bufferOffset, readBuffers.Length - bufferOffset);
291+
292+
long bytesRead = asyncMethod
293+
? await RandomAccess.ReadAsync(sfh, left, fileOffset)
294+
: RandomAccess.Read(sfh, left, fileOffset);
295+
296+
fileOffset += bytesRead;
297+
// The following operation is correct only because the BufferSize is 1.
298+
bufferOffset += (int)bytesRead;
299+
}
300+
301+
for (int i = 0; i < BufferCount; ++i)
302+
{
303+
Assert.Equal(writeBuffers[i], readBuffers[i]);
304+
}
305+
}
136306
}
137307
}

src/native/libs/System.Native/pal_io.c

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1883,6 +1883,53 @@ int32_t SystemNative_PWrite(intptr_t fd, void* buffer, int32_t bufferSize, int64
18831883
return (int32_t)count;
18841884
}
18851885

1886+
#if (HAVE_PREADV || HAVE_PWRITEV) && !defined(TARGET_WASM)
1887+
static int GetAllowedVectorCount(IOVector* vectors, int32_t vectorCount)
1888+
{
1889+
#if defined(IOV_MAX)
1890+
const int IovMax = IOV_MAX;
1891+
#else
1892+
// In theory all the platforms that we support define IOV_MAX,
1893+
// but we want to be extra safe and provde a fallback
1894+
// in case it turns out to not be true.
1895+
// 16 is low, but supported on every platform.
1896+
const int IovMax = 16;
1897+
#endif
1898+
1899+
int allowedCount = (int)vectorCount;
1900+
1901+
// We need to respect the limit of items that can be passed in iov.
1902+
// In case of writes, the managed code is responsible for handling incomplete writes.
1903+
// In case of reads, we simply returns the number of bytes read and it's up to the users.
1904+
if (IovMax < allowedCount)
1905+
{
1906+
allowedCount = IovMax;
1907+
}
1908+
1909+
#if defined(TARGET_APPLE)
1910+
// For macOS preadv and pwritev can fail with EINVAL when the total length
1911+
// of all vectors overflows a 32-bit integer.
1912+
size_t totalLength = 0;
1913+
for (int i = 0; i < allowedCount; i++)
1914+
{
1915+
assert(INT_MAX >= vectors[i].Count);
1916+
1917+
totalLength += vectors[i].Count;
1918+
1919+
if (totalLength > INT_MAX)
1920+
{
1921+
allowedCount = i;
1922+
break;
1923+
}
1924+
}
1925+
#else
1926+
(void)vectors;
1927+
#endif
1928+
1929+
return allowedCount;
1930+
}
1931+
#endif // (HAVE_PREADV || HAVE_PWRITEV) && !defined(TARGET_WASM)
1932+
18861933
int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset)
18871934
{
18881935
assert(vectors != NULL);
@@ -1891,7 +1938,8 @@ int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount,
18911938
int64_t count = 0;
18921939
int fileDescriptor = ToFileDescriptor(fd);
18931940
#if HAVE_PREADV && !defined(TARGET_WASM) // preadv is buggy on WASM
1894-
while ((count = preadv(fileDescriptor, (struct iovec*)vectors, (int)vectorCount, (off_t)fileOffset)) < 0 && errno == EINTR);
1941+
int allowedVectorCount = GetAllowedVectorCount(vectors, vectorCount);
1942+
while ((count = preadv(fileDescriptor, (struct iovec*)vectors, allowedVectorCount, (off_t)fileOffset)) < 0 && errno == EINTR);
18951943
#else
18961944
int64_t current;
18971945
for (int i = 0; i < vectorCount; i++)
@@ -1931,7 +1979,8 @@ int64_t SystemNative_PWriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount
19311979
int64_t count = 0;
19321980
int fileDescriptor = ToFileDescriptor(fd);
19331981
#if HAVE_PWRITEV && !defined(TARGET_WASM) // pwritev is buggy on WASM
1934-
while ((count = pwritev(fileDescriptor, (struct iovec*)vectors, (int)vectorCount, (off_t)fileOffset)) < 0 && errno == EINTR);
1982+
int allowedVectorCount = GetAllowedVectorCount(vectors, vectorCount);
1983+
while ((count = pwritev(fileDescriptor, (struct iovec*)vectors, allowedVectorCount, (off_t)fileOffset)) < 0 && errno == EINTR);
19351984
#else
19361985
int64_t current;
19371986
for (int i = 0; i < vectorCount; i++)

0 commit comments

Comments
 (0)