3

I'm using a MemoryMappedFile in .NET 5 and encounter a problem that seemed to have been solved long ago.

I use it for inter-process communication so it must be located in ring 0. For that to achieve I use the name prefix Global:

var mmf = MemoryMappedFile.CreateOrOpen(@"Global\test", 1024, MemoryMappedFileAccess.ReadWrite);

This works fine for privileged processes.

For non-privileged ones I want to use

mmf = MemoryMappedFile.OpenExisting(@"Global\test", MemoryMappedFileRights.ReadWrite);

But that fails with an "UnauthorizedAccessException".

The solution that worked for .NET Framework up to 4.8 was to explicitly set MemoryMappedFileSecurity as described e.g. here: Gaining access to a MemoryMappedFile from low-integrity process

Unfortunately the MemoryMappedFileSecurity does no longer seem to exist and MemoryMappedFile.OpenExisting (or CreateNew) do no have an overload for that or similar either.

Interim solution

.Net5 mostlikely won't get that missing feature. See https://github.com/dotnet/runtime/issues/941

As a workaround I use the P/Invoke approach. In case someone has the same problem here is a working solution using null DACL to grant all access

private void CreateFile()
{
    try
    {
        using(var secAttribs = CreateSecAttribs())
        {
            memBuffer = NativeMethods.CreateFileMapping(
                            UIntPtr.MaxValue,
                            secAttribs,
                            (uint)FileMapProtection.PAGE_READWRITE,
                            0,
                            (uint)MemOffset.TotalSize,
                            BufferName);

            if (memBuffer == IntPtr.Zero)
            {
                uint lasterror = NativeMethods.GetLastError();
                throw new Win32Exception((int)lasterror, string.Format(CultureInfo.InvariantCulture, "Error creating shared memory. Errorcode is {0}", lasterror));
            }
            
            IntPtr accessor = NativeMethods.MapViewOfFile(memBuffer, (uint)ViewAccess.FILE_MAP_ALL_ACCESS, 0, 0, (int)MemOffset.TotalSize);

            if (accessor == IntPtr.Zero)
            {
                uint lasterror = NativeMethods.GetLastError();
                throw new Win32Exception((int)lasterror, string.Format(CultureInfo.InvariantCulture, "Error creating shared memory view. Errorcode is {0}", lasterror));
            }
            
            //Do sth with accessor using System.Runtime.InteropServices.Marshal
        }
    }
    catch (Exception)
    {
        memBuffer = IntPtr.Zero;
        throw;
    }
}

private static NativeMethods.SECURITY_ATTRIBUTES CreateSecAttribs()
{
    //Create the descriptor with a null DACL --> Everything is granted.
    RawSecurityDescriptor sec = new RawSecurityDescriptor(ControlFlags.DiscretionaryAclPresent, null, null, null, null);


    return new NativeMethods.SECURITY_ATTRIBUTES(sec);
}

For reference:

internal static class NativeMethods
{
    #region Structures

    [StructLayout(LayoutKind.Sequential)]
    internal class SECURITY_ATTRIBUTES : IDisposable
    {
        public int nLength;
        public IntPtr lpSecurityDescriptor;
        public int bInheritHandle;

        public SECURITY_ATTRIBUTES()
        {
            nLength = Marshal.SizeOf(typeof(SECURITY_ATTRIBUTES));
            lpSecurityDescriptor = IntPtr.Zero;
            bInheritHandle = 1;
        }

        public SECURITY_ATTRIBUTES(RawSecurityDescriptor sec) :
            this()
        {
            byte[] binDACL = new byte[sec.BinaryLength];
            sec.GetBinaryForm(binDACL, 0);

            lpSecurityDescriptor = Marshal.AllocHGlobal(sec.BinaryLength);

            Marshal.Copy(binDACL, 0, lpSecurityDescriptor, sec.BinaryLength);
        }

        ~SECURITY_ATTRIBUTES()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {

            }

            if (lpSecurityDescriptor != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(lpSecurityDescriptor);
                lpSecurityDescriptor = IntPtr.Zero;
            }
        }
    }

    #endregion

    #region General imports

    [DllImport("kernel32", EntryPoint = "CloseHandle", SetLastError = true, CharSet = CharSet.Unicode)]
    internal static extern int CloseHandle(IntPtr hHandle);

    [DllImport("kernel32", EntryPoint = "GetLastError", SetLastError = true, CharSet = CharSet.Unicode)]
    internal static extern uint GetLastError();

    #endregion

    #region Memory Mapped Files imports

    [DllImport("kernel32.dll", EntryPoint = "CreateFileMapping", SetLastError = true, CharSet = CharSet.Unicode)]
    internal static extern IntPtr CreateFileMapping(UIntPtr hFile, SECURITY_ATTRIBUTES lpAttributes, uint flProtect, uint dwMaximumSizeHigh, uint dwMaximumSizeLow, string lpName);

    [DllImport("kernel32.dll", EntryPoint = "MapViewOfFile", SetLastError = true, CharSet = CharSet.Unicode)]
    internal static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject, uint dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, uint /* UIntPtr */ dwNumberOfBytesToMap);

    [DllImport("kernel32.dll", EntryPoint = "UnmapViewOfFile", SetLastError = true, CharSet = CharSet.Unicode)]
    [return: MarshalAs(UnmanagedType.VariantBool)]
    internal static extern bool UnmapViewOfFile(IntPtr lpBaseAddress);

    #endregion
}

Update

Still no official integration until .Net 7
Adam Sitnik was so friendly to manually recreate MemoryMappedFileSecurity and Wrappers for .Net7.
See https://gist.github.com/adamsitnik/6370b05b9e80bc14b62ac6efe5d1e2e2#file-mmfs-cs-L41-L184
You might need to add CreateOrOpen() based on .Net4 or remove the ERROR_ALREADY_EXIST error case in CreateCore()

Managed Solution

This works when first called by a windows service and then by a user program (and of course vv).

var everyoneRule = new AccessRule<MemoryMappedFileRights>(
    new SecurityIdentifier(WellKnownSidType.WorldSid, null),
    MemoryMappedFileRights.ReadWrite,
    AccessControlType.Allow);

MemoryMappedFileSecurity mmfSec = new MemoryMappedFileSecurity();
mmfSec.AddAccessRule(everyoneRule);

mmf = MemoryMappedFileFactory.Create(@"Global\Test", 1024, MemoryMappedFileAccess.ReadWrite, MemoryMappedFileOptions.None, mmfSec, HandleInheritability.None);
TomB
  • 641
  • 5
  • 17
  • .NET 4 is .NET *Core* 5. Answers that targeted .NET Old may not work in a cross-platform runtime like .NET Core - a Windows solution won't work on Linux or vice versa. You may be able to fix this by targeting a specific runtime, eg instead of targeting `net5.0` in your csproj file use `net5.0-windows` – Panagiotis Kanavos Dec 18 '20 at 10:21
  • Needs more [customer requests](https://github.com/dotnet/runtime/issues/941), be sure to speak up. – Hans Passant Dec 18 '20 at 13:15
  • @PanagiotisKanavos: Nope. App is already set to net5.0-windows – TomB Dec 18 '20 at 14:14
  • 3
    @HansPassant kind of difficult when MS have locked the thread :( – Cocowalla Apr 20 '21 at 09:00
  • hi @TomB can you write exactly how to use your interim solution? I get the same issue as yours but I don't know how to use your code in MemoryMappedFile.CreateOrOpen(..). Many thanks. – Quoc Nguyen Sep 06 '22 at 11:23
  • @QuocNguyen System.IO.MemoryMappedFiles.MemoryMappedFile does not support this. You must use the P/Invoke approach. CreateOrOpen needs to be implemented yourself. CreateFileMapping From MSDN: If the object exists before the function call, the function returns a handle to the existing object (with its current size, not the specified size), and GetLastError returns ERROR_ALREADY_EXISTS. – TomB Sep 09 '22 at 12:54

0 Answers0