diff --git a/src/libraries/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj b/src/libraries/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj index 058554ec058b8d..e04c6f837ac55b 100644 --- a/src/libraries/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj +++ b/src/libraries/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj @@ -96,6 +96,8 @@ Link="Common\Interop\Unix\Interop.OpenFlags.cs" /> + @@ -119,4 +121,4 @@ - \ No newline at end of file + diff --git a/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Unix.cs b/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Unix.cs index da7fbb0795dd85..16bb696a017985 100644 --- a/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Unix.cs +++ b/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Unix.cs @@ -8,6 +8,46 @@ namespace System.IO.MemoryMappedFiles { public partial class MemoryMappedFile { + // This will verify file access and return file size. fileSize will return -1 for special devices. + private static void VerifyMemoryMappedFileAccess(MemoryMappedFileAccess access, long capacity, FileStream? fileStream, out long fileSize) + { + fileSize = -1; + + if (fileStream != null) + { + Interop.Sys.FileStatus status; + + int result = Interop.Sys.FStat(fileStream.SafeFileHandle, out status); + if (result != 0) + { + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + throw Interop.GetExceptionForIoErrno(errorInfo); + } + + bool isRegularFile = (status.Mode & Interop.Sys.FileTypes.S_IFCHR) == 0; + + if (isRegularFile) + { + fileSize = status.Size; + if (access == MemoryMappedFileAccess.Read && capacity > status.Size) + { + throw new ArgumentException(SR.Argument_ReadAccessWithLargeCapacity); + } + + // one can always create a small view if they do not want to map an entire file + if (fileStream.Length > capacity) + { + throw new ArgumentOutOfRangeException(nameof(capacity), SR.ArgumentOutOfRange_CapacityGEFileSizeRequired); + } + + if (access == MemoryMappedFileAccess.Write) + { + throw new ArgumentException(SR.Argument_NewMMFWriteAccessNotAllowed, nameof(access)); + } + } + } + } + /// /// Used by the 2 Create factory method groups. A null fileHandle specifies that the /// memory mapped file should not be associated with an existing file on disk (i.e. start @@ -18,6 +58,8 @@ private static unsafe SafeMemoryMappedFileHandle CreateCore( HandleInheritability inheritability, MemoryMappedFileAccess access, MemoryMappedFileOptions options, long capacity) { + VerifyMemoryMappedFileAccess(access, capacity, fileStream, out long fileSize); + if (mapName != null) { // Named maps are not supported in our Unix implementation. We could support named maps on Linux using @@ -35,10 +77,10 @@ private static unsafe SafeMemoryMappedFileHandle CreateCore( bool ownsFileStream = false; if (fileStream != null) { - // This map is backed by a file. Make sure the file's size is increased to be - // at least as big as the requested capacity of the map. - if (fileStream.Length < capacity) + if (fileSize >= 0 && capacity > fileSize) { + // This map is backed by a file. Make sure the file's size is increased to be + // at least as big as the requested capacity of the map for Write* access. try { fileStream.SetLength(capacity); diff --git a/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Windows.cs b/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Windows.cs index c2399181c5a562..11ffb4ecb75c86 100644 --- a/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Windows.cs +++ b/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Windows.cs @@ -11,12 +11,26 @@ namespace System.IO.MemoryMappedFiles { public partial class MemoryMappedFile { + // This will verify file access. + private static void VerifyMemoryMappedFileAccess(MemoryMappedFileAccess access, long capacity, FileStream fileStream) + { + if (access == MemoryMappedFileAccess.Read && capacity > fileStream.Length) + { + throw new ArgumentException(SR.Argument_ReadAccessWithLargeCapacity); + } + + // one can always create a small view if they do not want to map an entire file + if (fileStream.Length > capacity) + { + throw new ArgumentOutOfRangeException(nameof(capacity), SR.ArgumentOutOfRange_CapacityGEFileSizeRequired); + } + } + /// /// Used by the 2 Create factory method groups. A null fileHandle specifies that the /// memory mapped file should not be associated with an existing file on disk (i.e. start /// out empty). /// - private static SafeMemoryMappedFileHandle CreateCore( FileStream? fileStream, string? mapName, HandleInheritability inheritability, MemoryMappedFileAccess access, MemoryMappedFileOptions options, long capacity) @@ -25,6 +39,11 @@ private static SafeMemoryMappedFileHandle CreateCore( Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = GetSecAttrs(inheritability); + if (fileStream != null) + { + VerifyMemoryMappedFileAccess(access, capacity, fileStream); + } + SafeMemoryMappedFileHandle handle = fileHandle != null ? Interop.CreateFileMapping(fileHandle, ref secAttrs, GetPageAccess(access) | (int)options, capacity, mapName) : Interop.CreateFileMapping(new IntPtr(-1), ref secAttrs, GetPageAccess(access) | (int)options, capacity, mapName); diff --git a/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs b/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs index f743f1c49d6e0d..0f6552df57bd03 100644 --- a/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs +++ b/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs @@ -154,24 +154,11 @@ public static MemoryMappedFile CreateFromFile(string path, FileMode mode, string throw new ArgumentException(SR.Argument_EmptyFile); } - if (access == MemoryMappedFileAccess.Read && capacity > fileStream.Length) - { - CleanupFile(fileStream, existed, path); - throw new ArgumentException(SR.Argument_ReadAccessWithLargeCapacity); - } - if (capacity == DefaultSize) { capacity = fileStream.Length; } - // one can always create a small view if they do not want to map an entire file - if (fileStream.Length > capacity) - { - CleanupFile(fileStream, existed, path); - throw new ArgumentOutOfRangeException(nameof(capacity), SR.ArgumentOutOfRange_CapacityGEFileSizeRequired); - } - SafeMemoryMappedFileHandle? handle = null; try { @@ -224,11 +211,6 @@ public static MemoryMappedFile CreateFromFile(FileStream fileStream, string? map throw new ArgumentException(SR.Argument_NewMMFWriteAccessNotAllowed, nameof(access)); } - if (access == MemoryMappedFileAccess.Read && capacity > fileStream.Length) - { - throw new ArgumentException(SR.Argument_ReadAccessWithLargeCapacity); - } - if (inheritability < HandleInheritability.None || inheritability > HandleInheritability.Inheritable) { throw new ArgumentOutOfRangeException(nameof(inheritability)); @@ -242,12 +224,6 @@ public static MemoryMappedFile CreateFromFile(FileStream fileStream, string? map capacity = fileStream.Length; } - // one can always create a small view if they do not want to map an entire file - if (fileStream.Length > capacity) - { - throw new ArgumentOutOfRangeException(nameof(capacity), SR.ArgumentOutOfRange_CapacityGEFileSizeRequired); - } - SafeMemoryMappedFileHandle handle = CreateCore(fileStream, mapName, inheritability, access, MemoryMappedFileOptions.None, capacity); diff --git a/src/libraries/System.IO.MemoryMappedFiles/tests/MemoryMappedFile.CreateFromFile.Tests.cs b/src/libraries/System.IO.MemoryMappedFiles/tests/MemoryMappedFile.CreateFromFile.Tests.cs index 3f5e9645f95343..2dd5b6303bcceb 100644 --- a/src/libraries/System.IO.MemoryMappedFiles/tests/MemoryMappedFile.CreateFromFile.Tests.cs +++ b/src/libraries/System.IO.MemoryMappedFiles/tests/MemoryMappedFile.CreateFromFile.Tests.cs @@ -5,6 +5,7 @@ using Microsoft.Win32.SafeHandles; using System.Collections.Generic; using System.Runtime.InteropServices; +using Microsoft.DotNet.XUnitExtensions; using Xunit; namespace System.IO.MemoryMappedFiles.Tests @@ -868,5 +869,81 @@ public void ReusingNames_Windows(string name) } } + private void ValidateDeviceAccess(MemoryMappedFile memMap, long viewCapacity, MemoryMappedFileAccess access) + { + using (MemoryMappedViewAccessor view = memMap.CreateViewAccessor(0, viewCapacity, access)) + { + if (access != MemoryMappedFileAccess.Write) + { + byte b = view.ReadByte(0); + // /dev/zero return zeroes. + Assert.Equal(0, b); + } + + if (access != MemoryMappedFileAccess.Read) + { + view.Write(0, (byte)1); + } + } + } + + /// + /// Test that we can map special character devices on Unix using FileStream. + /// + [ConditionalTheory] + [InlineData(MemoryMappedFileAccess.Read)] + [InlineData(MemoryMappedFileAccess.ReadWrite)] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public void OpenCharacterDeviceAsStream(MemoryMappedFileAccess access) + { + const string device = "/dev/zero"; + if (!File.Exists(device)) + { + throw new SkipTestException($"'{device}' is not available."); + } + + long viewCapacity = 0xFF; + + try + { + using (FileStream fs = new FileStream(device, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)) + using (MemoryMappedFile memMap = MemoryMappedFile.CreateFromFile(fs, null, viewCapacity, access, HandleInheritability.None, false)) + { + ValidateDeviceAccess(memMap, viewCapacity, access); + } + } + catch (UnauthorizedAccessException) { } + // ENODEV Operation not supported by device. + catch (IOException ex) when (ex.HResult == 19) { }; + } + + /// + /// Test that we can map special character devices on Unix using file name. + /// + [ConditionalTheory] + [InlineData(MemoryMappedFileAccess.Read)] + [InlineData(MemoryMappedFileAccess.ReadWrite)] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public void OpenCharacterDeviceAsFile(MemoryMappedFileAccess access) + { + const string device = "/dev/zero"; + if (!File.Exists(device)) + { + throw new SkipTestException($"'{device}' is not available."); + } + + long viewCapacity = 0xFF; + + try + { + using (MemoryMappedFile memMap = MemoryMappedFile.CreateFromFile(device, FileMode.Open, null, viewCapacity, access)) + { + ValidateDeviceAccess(memMap, viewCapacity, access); + } + } + catch (UnauthorizedAccessException) { } + // ENODEV Operation not supported by device. + catch (IOException ex) when (ex.HResult == 19) { }; + } } }