Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 83551fa

Browse files
tmdsstephentoub
authored andcommitted
Eliminate dirent memcpy when using readdir. (#25407)
* Eliminate dirent memcpy when using readdir. * Remove concurrent readdir check * Update references * Add comment about readdir concurrency assumption.
1 parent 439bc50 commit 83551fa

File tree

3 files changed

+55
-36
lines changed

3 files changed

+55
-36
lines changed

src/Common/src/Interop/Unix/System.Native/Interop.ReadDir.cs

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55
using System;
66
using System.Runtime.InteropServices;
7+
using System.Threading;
78
using Microsoft.Win32.SafeHandles;
89

910
internal static partial class Interop
1011
{
1112
internal static partial class Sys
1213
{
13-
private static readonly int s_direntSize = GetDirentSize();
14+
private static readonly int s_readBufferSize = GetReadDirRBufferSize();
1415

1516
internal enum NodeType : int
1617
{
@@ -42,29 +43,51 @@ internal struct DirectoryEntry
4243
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_OpenDir", SetLastError = true)]
4344
internal static extern Microsoft.Win32.SafeHandles.SafeDirectoryHandle OpenDir(string path);
4445

45-
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetDirentSize", SetLastError = false)]
46-
internal static extern int GetDirentSize();
46+
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetReadDirRBufferSize", SetLastError = false)]
47+
internal static extern int GetReadDirRBufferSize();
4748

4849
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_ReadDirR", SetLastError = false)]
49-
private static extern unsafe int ReadDirR(SafeDirectoryHandle dir, byte* buffer, int bufferSize, out InternalDirectoryEntry outputEntry);
50+
private static extern unsafe int ReadDirR(IntPtr dir, byte* buffer, int bufferSize, out InternalDirectoryEntry outputEntry);
5051

5152
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_CloseDir", SetLastError = true)]
5253
internal static extern int CloseDir(IntPtr dir);
5354

5455
// The calling pattern for ReadDir is described in src/Native/System.Native/pal_readdir.cpp
5556
internal static int ReadDir(SafeDirectoryHandle dir, out DirectoryEntry outputEntry)
5657
{
57-
unsafe
58+
bool addedRef = false;
59+
try
5860
{
59-
// To reduce strcpys, alloc a buffer here and get the result from OS, then copy it over for the caller.
60-
byte* buffer = stackalloc byte[s_direntSize];
61-
InternalDirectoryEntry temp;
62-
int ret = ReadDirR(dir, buffer, s_direntSize, out temp);
63-
outputEntry = ret == 0 ?
64-
new DirectoryEntry() { InodeName = GetDirectoryEntryName(temp), InodeType = temp.InodeType } :
65-
default(DirectoryEntry);
61+
// We avoid a native string copy into InternalDirectoryEntry.
62+
// - If the platform suppors reading into a buffer, the data is read directly into the buffer. The
63+
// data can be read as long as the buffer is valid.
64+
// - If the platform does not support reading into a buffer, the information returned in
65+
// InternalDirectoryEntry points to native memory owned by the SafeDirectoryHandle. The data is only
66+
// valid until the next call to CloseDir/ReadDir. We extend the reference until we have copied all data
67+
// to ensure it does not become invalid by a CloseDir; and we copy the data so our caller does not
68+
// use the native memory held by the SafeDirectoryHandle.
69+
dir.DangerousAddRef(ref addedRef);
6670

67-
return ret;
71+
unsafe
72+
{
73+
// s_readBufferSize is zero when the native implementation does not support reading into a buffer.
74+
byte* buffer = stackalloc byte[s_readBufferSize];
75+
InternalDirectoryEntry temp;
76+
int ret = ReadDirR(dir.DangerousGetHandle(), buffer, s_readBufferSize, out temp);
77+
// We copy data into DirectoryEntry to ensure there are no dangling references.
78+
outputEntry = ret == 0 ?
79+
new DirectoryEntry() { InodeName = GetDirectoryEntryName(temp), InodeType = temp.InodeType } :
80+
default(DirectoryEntry);
81+
82+
return ret;
83+
}
84+
}
85+
finally
86+
{
87+
if (addedRef)
88+
{
89+
dir.DangerousRelease();
90+
}
6891
}
6992
}
7093

src/Native/Unix/System.Native/pal_io.c

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -324,41 +324,38 @@ static void ConvertDirent(const struct dirent* entry, struct DirectoryEntry* out
324324
#endif
325325
}
326326

327-
int32_t SystemNative_GetDirentSize(void)
327+
int32_t SystemNative_GetReadDirRBufferSize(void)
328328
{
329+
#if HAVE_READDIR_R
329330
// dirent should be under 2k in size
330331
assert(sizeof(struct dirent) < 2048);
331332
return sizeof(struct dirent);
333+
#else
334+
return 0;
335+
#endif
332336
}
333337

334-
// To reduce the number of string copies, this function calling pattern works as follows:
335-
// 1) The managed code calls GetDirentSize() to get the platform-specific
336-
// size of the dirent struct.
337-
// 2) The managed code creates a byte[] buffer of the size of the native dirent
338-
// and passes a pointer to this buffer to this function.
339-
// 3) This function passes input byte[] buffer to the OS to fill with dirent
340-
// data which makes the 1st strcpy.
341-
// 4) The ConvertDirent function will fill DirectoryEntry outputEntry with
342-
// pointers from byte[] buffer.
343-
// 5) The managed code uses DirectoryEntry outputEntry to find start of d_name
344-
// and the value of d_namelen, if avalable, to copy the name from
345-
// byte[] buffer into a managed string that the caller can use; this makes
346-
// the 2nd and final strcpy.
338+
// To reduce the number of string copies, the caller of this function is responsible to ensure the memory
339+
// referenced by outputEntry remains valid until it is read.
340+
// If the platform supports readdir_r, the caller provides a buffer into which the data is read.
341+
// If the platform uses readdir, the caller must ensure no calls are made to readdir/closedir since those will invalidate
342+
// the current dirent. We assume the platform supports concurrent readdir calls to different DIRs.
347343
int32_t SystemNative_ReadDirR(DIR* dir, void* buffer, int32_t bufferSize, struct DirectoryEntry* outputEntry)
348344
{
349-
assert(buffer != NULL);
350345
assert(dir != NULL);
351346
assert(outputEntry != NULL);
352347

348+
#if HAVE_READDIR_R
349+
assert(buffer != NULL);
350+
353351
if (bufferSize < (int32_t)sizeof(struct dirent))
354352
{
355-
assert(false && "Buffer size too small; use GetDirentSize to get required buffer size");
353+
assert(false && "Buffer size too small; use GetReadDirRBufferSize to get required buffer size");
356354
return ERANGE;
357355
}
358356

359357
struct dirent* result = NULL;
360358
struct dirent* entry = (struct dirent*)buffer;
361-
#if HAVE_READDIR_R
362359
int error = readdir_r(dir, entry, &result);
363360

364361
// positive error number returned -> failure
@@ -379,11 +376,13 @@ int32_t SystemNative_ReadDirR(DIR* dir, void* buffer, int32_t bufferSize, struct
379376
// 0 returned with non-null result (guaranteed to be set to entry arg) -> success
380377
assert(result == entry);
381378
#else
379+
(void)buffer; // unused
380+
(void)bufferSize; // unused
382381
errno = 0;
383-
result = readdir(dir);
382+
struct dirent* entry = readdir(dir);
384383

385384
// 0 returned with null result -> end-of-stream
386-
if (result == NULL)
385+
if (entry == NULL)
387386
{
388387
memset(outputEntry, 0, sizeof(*outputEntry)); // managed out param must be initialized
389388

@@ -395,9 +394,6 @@ int32_t SystemNative_ReadDirR(DIR* dir, void* buffer, int32_t bufferSize, struct
395394
}
396395
return -1;
397396
}
398-
399-
assert(result->d_reclen <= bufferSize);
400-
memcpy_s(entry, sizeof(struct dirent), result, (size_t)result->d_reclen);
401397
#endif
402398
ConvertDirent(entry, outputEntry);
403399
return 0;

src/Native/Unix/System.Native/pal_io.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ int32_t SystemNative_ShmUnlink(const char* name);
356356
/**
357357
* Returns the size of the dirent struct on the current architecture
358358
*/
359-
int32_t SystemNative_GetDirentSize(void);
359+
int32_t SystemNative_GetReadDirRBufferSize(void);
360360

361361
/**
362362
* Re-entrant readdir that will retrieve the next dirent from the directory stream pointed to by dir.

0 commit comments

Comments
 (0)