Skip to content

Commit 861b80d

Browse files
adamsitnikam11stephentoub
committed
Handle IOV_MAX limit in RandomAccess, add missing test (dotnet#109340)
* add test for Int32 overflow for WriteGather in RandomAccess * add failing test fore more than IOV_MAX buffers * fix both the native and managed parts --------- Co-authored-by: Adeel Mujahid <[email protected]> Co-authored-by: Stephen Toub <[email protected]>
1 parent f3a867b commit 861b80d

File tree

3 files changed

+153
-4
lines changed

3 files changed

+153
-4
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,10 @@ internal static unsafe void WriteGatherAtOffset(SafeFileHandle handle, IReadOnly
196196
}
197197

198198
long bytesWritten;
199-
fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(vectors))
199+
Span<Interop.Sys.IOVector> left = vectors.Slice(buffersOffset);
200+
fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(left))
200201
{
201-
bytesWritten = Interop.Sys.PWriteV(handle, pinnedVectors, buffersCount, fileOffset);
202+
bytesWritten = Interop.Sys.PWriteV(handle, pinnedVectors, buffersCount - buffersOffset, fileOffset);
202203
}
203204

204205
FileStreamHelpers.CheckFileCall(bytesWritten, handle.Path);
@@ -208,6 +209,8 @@ internal static unsafe void WriteGatherAtOffset(SafeFileHandle handle, IReadOnly
208209
}
209210

210211
// The write completed successfully but for fewer bytes than requested.
212+
// We need to perform next write where the previous one has finished.
213+
fileOffset += bytesWritten;
211214
// We need to try again for the remainder.
212215
for (int i = 0; i < buffersCount; i++)
213216
{

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

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Security.Cryptography;
77
using System.Threading;
88
using System.Threading.Tasks;
9+
using Microsoft.DotNet.XUnitExtensions;
910
using Microsoft.Win32.SafeHandles;
1011
using Xunit;
1112

@@ -133,5 +134,130 @@ public async Task DuplicatedBufferDuplicatesContentAsync(FileOptions options)
133134
Assert.Equal(repeatCount, actualContent.Length);
134135
Assert.All(actualContent, actual => Assert.Equal(value, actual));
135136
}
137+
138+
[OuterLoop("It consumes a lot of resources (disk space and memory).")]
139+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.Is64BitProcess), nameof(PlatformDetection.IsReleaseRuntime))]
140+
[InlineData(false, false)]
141+
[InlineData(false, true)]
142+
[InlineData(true, true)]
143+
[InlineData(true, false)]
144+
public async Task NoInt32OverflowForLargeInputs(bool asyncFile, bool asyncMethod)
145+
{
146+
// We need to write more than Int32.MaxValue bytes to the disk to reproduce the problem.
147+
// To reduce the number of used memory, we allocate only one write buffer and simply repeat it multiple times.
148+
// For reading, we need unique buffers to ensure that all of them are getting populated with the right data.
149+
150+
const int BufferCount = 1002;
151+
const int BufferSize = int.MaxValue / 1000;
152+
const long FileSize = (long)BufferCount * BufferSize;
153+
string filePath = GetTestFilePath();
154+
ReadOnlyMemory<byte> writeBuffer = RandomNumberGenerator.GetBytes(BufferSize);
155+
List<ReadOnlyMemory<byte>> writeBuffers = Enumerable.Repeat(writeBuffer, BufferCount).ToList();
156+
List<Memory<byte>> readBuffers = new List<Memory<byte>>(BufferCount);
157+
158+
try
159+
{
160+
for (int i = 0; i < BufferCount; i++)
161+
{
162+
readBuffers.Add(new byte[BufferSize]);
163+
}
164+
}
165+
catch (OutOfMemoryException)
166+
{
167+
throw new SkipTestException("Not enough memory.");
168+
}
169+
170+
FileOptions options = asyncFile ? FileOptions.Asynchronous : FileOptions.None; // we need to test both code paths
171+
options |= FileOptions.DeleteOnClose;
172+
173+
SafeFileHandle? sfh;
174+
try
175+
{
176+
sfh = File.OpenHandle(filePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, options, preallocationSize: FileSize);
177+
}
178+
catch (IOException)
179+
{
180+
throw new SkipTestException("Not enough disk space.");
181+
}
182+
183+
long fileOffset = 0, bytesRead = 0;
184+
try
185+
{
186+
if (asyncMethod)
187+
{
188+
await RandomAccess.WriteAsync(sfh, writeBuffers, fileOffset);
189+
bytesRead = await RandomAccess.ReadAsync(sfh, readBuffers, fileOffset);
190+
}
191+
else
192+
{
193+
RandomAccess.Write(sfh, writeBuffers, fileOffset);
194+
bytesRead = RandomAccess.Read(sfh, readBuffers, fileOffset);
195+
}
196+
}
197+
finally
198+
{
199+
sfh.Dispose(); // delete the file ASAP to avoid running out of resources in CI
200+
}
201+
202+
Assert.Equal(FileSize, bytesRead);
203+
for (int i = 0; i < BufferCount; i++)
204+
{
205+
Assert.Equal(writeBuffer, readBuffers[i]);
206+
}
207+
}
208+
209+
[Theory]
210+
[InlineData(false, false)]
211+
[InlineData(false, true)]
212+
[InlineData(true, true)]
213+
[InlineData(true, false)]
214+
public async Task IovLimitsAreRespected(bool asyncFile, bool asyncMethod)
215+
{
216+
// We need to write and read more than IOV_MAX buffers at a time.
217+
// IOV_MAX typical value is 1024.
218+
const int BufferCount = 1026;
219+
const int BufferSize = 1; // the less resources we use, the better
220+
const int FileSize = BufferCount * BufferSize;
221+
222+
ReadOnlyMemory<byte> writeBuffer = RandomNumberGenerator.GetBytes(BufferSize);
223+
ReadOnlyMemory<byte>[] writeBuffers = Enumerable.Repeat(writeBuffer, BufferCount).ToArray();
224+
Memory<byte>[] readBuffers = Enumerable.Range(0, BufferCount).Select(_ => new byte[BufferSize].AsMemory()).ToArray();
225+
226+
FileOptions options = asyncFile ? FileOptions.Asynchronous : FileOptions.None; // we need to test both code paths
227+
options |= FileOptions.DeleteOnClose;
228+
229+
using SafeFileHandle sfh = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, options);
230+
231+
if (asyncMethod)
232+
{
233+
await RandomAccess.WriteAsync(sfh, writeBuffers, 0);
234+
}
235+
else
236+
{
237+
RandomAccess.Write(sfh, writeBuffers, 0);
238+
}
239+
240+
Assert.Equal(FileSize, RandomAccess.GetLength(sfh));
241+
242+
long fileOffset = 0;
243+
int bufferOffset = 0;
244+
while (fileOffset < FileSize)
245+
{
246+
ArraySegment<Memory<byte>> left = new ArraySegment<Memory<byte>>(readBuffers, bufferOffset, readBuffers.Length - bufferOffset);
247+
248+
long bytesRead = asyncMethod
249+
? await RandomAccess.ReadAsync(sfh, left, fileOffset)
250+
: RandomAccess.Read(sfh, left, fileOffset);
251+
252+
fileOffset += bytesRead;
253+
// The following operation is correct only because the BufferSize is 1.
254+
bufferOffset += (int)bytesRead;
255+
}
256+
257+
for (int i = 0; i < BufferCount; ++i)
258+
{
259+
Assert.Equal(writeBuffers[i], readBuffers[i]);
260+
}
261+
}
136262
}
137263
}

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1883,6 +1883,24 @@ 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(int32_t vectorCount)
1888+
{
1889+
int allowedCount = (int)vectorCount;
1890+
1891+
#if defined(IOV_MAX)
1892+
if (IOV_MAX < allowedCount)
1893+
{
1894+
// We need to respect the limit of items that can be passed in iov.
1895+
// In case of writes, the managed code is reponsible for handling incomplete writes.
1896+
// In case of reads, we simply returns the number of bytes read and it's up to the users.
1897+
allowedCount = IOV_MAX;
1898+
}
1899+
#endif
1900+
return allowedCount;
1901+
}
1902+
#endif // (HAVE_PREADV || HAVE_PWRITEV) && !defined(TARGET_WASM)
1903+
18861904
int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset)
18871905
{
18881906
assert(vectors != NULL);
@@ -1891,7 +1909,8 @@ int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount,
18911909
int64_t count = 0;
18921910
int fileDescriptor = ToFileDescriptor(fd);
18931911
#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);
1912+
int allowedVectorCount = GetAllowedVectorCount(vectorCount);
1913+
while ((count = preadv(fileDescriptor, (struct iovec*)vectors, allowedVectorCount, (off_t)fileOffset)) < 0 && errno == EINTR);
18951914
#else
18961915
int64_t current;
18971916
for (int i = 0; i < vectorCount; i++)
@@ -1931,7 +1950,8 @@ int64_t SystemNative_PWriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount
19311950
int64_t count = 0;
19321951
int fileDescriptor = ToFileDescriptor(fd);
19331952
#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);
1953+
int allowedVectorCount = GetAllowedVectorCount(vectorCount);
1954+
while ((count = pwritev(fileDescriptor, (struct iovec*)vectors, allowedVectorCount, (off_t)fileOffset)) < 0 && errno == EINTR);
19351955
#else
19361956
int64_t current;
19371957
for (int i = 0; i < vectorCount; i++)

0 commit comments

Comments
 (0)