Skip to content

Allow MemoryMappedFile to work on character device #2183

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 8, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@
Link="Common\Interop\Unix\Interop.OpenFlags.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Permissions.cs"
Link="Common\Interop\Unix\Interop.Permissions.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Stat.cs"
Link="Common\Interop\Unix\Interop.Stat.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.SysConf.cs"
Link="Common\Interop\Unix\Interop.SysConf.cs" />
<Compile Include="Microsoft\Win32\SafeMemoryMappedFileHandle.Unix.cs" />
Expand All @@ -119,4 +121,4 @@
<Reference Include="System.Threading" />
<Reference Include="System.Threading.Thread" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}
}

/// <summary>
/// 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
Expand All @@ -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
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

/// <summary>
/// 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).
/// </summary>

private static SafeMemoryMappedFileHandle CreateCore(
FileStream? fileStream, string? mapName, HandleInheritability inheritability,
MemoryMappedFileAccess access, MemoryMappedFileOptions options, long capacity)
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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));
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
}

/// <summary>
/// Test that we can map special character devices on Unix using FileStream.
/// </summary>
[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) { };
}

/// <summary>
/// Test that we can map special character devices on Unix using file name.
/// </summary>
[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) { };
}
}
}