File: System\IO\MemoryMappedFiles\MemoryMappedFile.Unix.cs
Web Access
Project: src\src\libraries\System.IO.MemoryMappedFiles\src\System.IO.MemoryMappedFiles.csproj (System.IO.MemoryMappedFiles)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Text;
using Microsoft.Win32.SafeHandles;
 
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, SafeFileHandle? fileHandle, long fileSize, out bool isRegularFile)
        {
            // if the length has already been fetched and it's more than 0 it's a regular file and there is no need for the FStat sys-call
            isRegularFile = fileHandle is not null && (fileSize > 0 || IsRegularFile(fileHandle));
 
            if (isRegularFile)
            {
                if (access == MemoryMappedFileAccess.Read && capacity > fileSize)
                {
                    throw new ArgumentException(SR.Argument_ReadAccessWithLargeCapacity);
                }
 
                // one can always create a small view if they do not want to map an entire file
                if (fileSize > capacity)
                {
                    throw new ArgumentOutOfRangeException(nameof(capacity), SR.ArgumentOutOfRange_CapacityGEFileSizeRequired);
                }
 
                if (access == MemoryMappedFileAccess.Write)
                {
                    throw new ArgumentException(SR.Argument_NewMMFWriteAccessNotAllowed, nameof(access));
                }
            }
 
            static bool IsRegularFile(SafeFileHandle fileHandle)
            {
                Interop.CheckIo(Interop.Sys.FStat(fileHandle, out Interop.Sys.FileStatus status));
                return (status.Mode & Interop.Sys.FileTypes.S_IFREG) != 0;
            }
        }
 
        /// <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(
            SafeFileHandle? fileHandle, string? mapName,
            HandleInheritability inheritability, MemoryMappedFileAccess access,
            MemoryMappedFileOptions options, long capacity, long fileSize)
        {
            VerifyMemoryMappedFileAccess(access, capacity, fileHandle, fileSize, out bool isRegularFile);
 
            if (mapName != null)
            {
                // Named maps are not supported in our Unix implementation.  We could support named maps on Linux using
                // shared memory segments (shmget/shmat/shmdt/shmctl/etc.), but that doesn't work on OSX by default due
                // to very low default limits on OSX for the size of such objects; it also doesn't support behaviors
                // like copy-on-write or the ability to control handle inheritability, and reliably cleaning them up
                // relies on some non-conforming behaviors around shared memory IDs remaining valid even after they've
                // been marked for deletion (IPC_RMID).  We could also support named maps using the current implementation
                // by not unlinking after creating the backing store, but then the backing stores would remain around
                // and accessible even after process exit, with no good way to appropriately clean them up.
                // (File-backed maps may still be used for cross-process communication.)
                throw CreateNamedMapsNotSupportedException();
            }
 
            bool ownsFileStream = false;
            if (fileHandle != null)
            {
                if (isRegularFile && 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
                    {
                        Interop.CheckIo(Interop.Sys.FTruncate(fileHandle, capacity));
                    }
                    catch (ArgumentException exc)
                    {
                        // If the capacity is too large, we'll get an ArgumentException from SetLength,
                        // but on Windows this same condition is represented by an IOException.
                        throw new IOException(exc.Message, exc);
                    }
                }
            }
            else
            {
                // This map is backed by memory-only.  With files, multiple views over the same map
                // will end up being able to share data through the same file-based backing store;
                // for anonymous maps, we need a similar backing store, or else multiple views would logically
                // each be their own map and wouldn't share any data.  To achieve this, we create a backing object
                // (either memory or on disk, depending on the system) and use its file descriptor as the file handle.
                // However, we only do this when the permission is more than read-only.  We can't change the size
                // of an object that has read-only permissions, but we also don't need to worry about sharing
                // views over a read-only, anonymous, memory-backed map, because the data will never change, so all views
                // will always see zero and can't change that.  In that case, we just use the built-in anonymous support of
                // the map by leaving fileStream as null.
                Interop.Sys.MemoryMappedProtections protections = MemoryMappedView.GetProtections(access, forVerification: false);
                if ((protections & Interop.Sys.MemoryMappedProtections.PROT_WRITE) != 0 && capacity > 0)
                {
                    ownsFileStream = true;
                    fileHandle = CreateSharedBackingObject(protections, capacity, inheritability);
                }
            }
 
            return new SafeMemoryMappedFileHandle(fileHandle, ownsFileStream, inheritability, access, options, capacity);
        }
 
        /// <summary>
        /// Used by the CreateOrOpen factory method groups.
        /// </summary>
        private static SafeMemoryMappedFileHandle CreateOrOpenCore(
            string mapName,
            HandleInheritability inheritability, MemoryMappedFileAccess access,
            MemoryMappedFileOptions options, long capacity)
        {
            // Since we don't support mapName != null, CreateOrOpenCore can't
            // be used to Open an existing map, and thus is identical to CreateCore.
            return CreateCore(null, mapName, inheritability, access, options, capacity, -1);
        }
 
#pragma warning disable IDE0060
        /// <summary>
        /// Used by the OpenExisting factory method group and by CreateOrOpen if access is write.
        /// We'll throw an ArgumentException if the file mapping object didn't exist and the
        /// caller used CreateOrOpen since Create isn't valid with Write access
        /// </summary>
        private static SafeMemoryMappedFileHandle OpenCore(
            string mapName, HandleInheritability inheritability, MemoryMappedFileAccess access, bool createOrOpen)
        {
            throw CreateNamedMapsNotSupportedException();
        }
 
        /// <summary>
        /// Used by the OpenExisting factory method group and by CreateOrOpen if access is write.
        /// We'll throw an ArgumentException if the file mapping object didn't exist and the
        /// caller used CreateOrOpen since Create isn't valid with Write access
        /// </summary>
        private static SafeMemoryMappedFileHandle OpenCore(
            string mapName, HandleInheritability inheritability, MemoryMappedFileRights rights, bool createOrOpen)
        {
            throw CreateNamedMapsNotSupportedException();
        }
#pragma warning restore IDE0060
 
        /// <summary>Gets an exception indicating that named maps are not supported on this platform.</summary>
        private static PlatformNotSupportedException CreateNamedMapsNotSupportedException()
        {
            return new PlatformNotSupportedException(SR.PlatformNotSupported_NamedMaps);
        }
 
        private static FileAccess TranslateProtectionsToFileAccess(Interop.Sys.MemoryMappedProtections protections)
        {
            return
                (protections & (Interop.Sys.MemoryMappedProtections.PROT_READ | Interop.Sys.MemoryMappedProtections.PROT_WRITE)) != 0 ? FileAccess.ReadWrite :
                (protections & (Interop.Sys.MemoryMappedProtections.PROT_WRITE)) != 0 ? FileAccess.Write :
                FileAccess.Read;
        }
 
        private static SafeFileHandle CreateSharedBackingObject(Interop.Sys.MemoryMappedProtections protections, long capacity, HandleInheritability inheritability)
        {
            return Interop.Sys.IsMemfdSupported ?
                CreateSharedBackingObjectUsingMemoryMemfdCreate(protections, capacity, inheritability) :
                CreateSharedBackingObjectUsingMemoryShmOpen(protections, capacity, inheritability)
                    ?? CreateSharedBackingObjectUsingFile(protections, capacity, inheritability);
        }
 
        private static SafeFileHandle? CreateSharedBackingObjectUsingMemoryShmOpen(
           Interop.Sys.MemoryMappedProtections protections, long capacity, HandleInheritability inheritability)
        {
            // Determine the flags to use when creating the shared memory object
            Interop.Sys.OpenFlags flags = (protections & Interop.Sys.MemoryMappedProtections.PROT_WRITE) != 0 ?
                Interop.Sys.OpenFlags.O_RDWR :
                Interop.Sys.OpenFlags.O_RDONLY;
            flags |= Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL; // CreateNew
 
            // Determine the permissions with which to create the file
            var perms = UnixFileMode.None;
            if ((protections & Interop.Sys.MemoryMappedProtections.PROT_READ) != 0)
                perms |= UnixFileMode.UserRead;
            if ((protections & Interop.Sys.MemoryMappedProtections.PROT_WRITE) != 0)
                perms |= UnixFileMode.UserWrite;
            if ((protections & Interop.Sys.MemoryMappedProtections.PROT_EXEC) != 0)
                perms |= UnixFileMode.UserExecute;
 
            string mapName;
            SafeFileHandle fd;
 
            do
            {
                mapName = GenerateMapName();
                fd = Interop.Sys.ShmOpen(mapName, flags, (int)perms); // Create the shared memory object.
 
                if (fd.IsInvalid)
                {
                    Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
                    fd.Dispose();
 
                    if (errorInfo.Error == Interop.Error.ENOTSUP)
                    {
                        // If ShmOpen is not supported, fall back to file backing object.
                        // Note that the System.Native shim will force this failure on platforms where
                        // the result of native shm_open does not work well with our subsequent call to mmap.
                        return null;
                    }
                    else if (errorInfo.Error == Interop.Error.ENAMETOOLONG)
                    {
                        Debug.Fail($"shm_open failed with ENAMETOOLONG for {Encoding.UTF8.GetByteCount(mapName)} byte long name.");
                        // in theory it should not happen anymore, but just to be extra safe we use the fallback
                        return null;
                    }
                    else if (errorInfo.Error != Interop.Error.EEXIST) // map with same name already existed
                    {
                        throw Interop.GetExceptionForIoErrno(errorInfo);
                    }
                }
            } while (fd.IsInvalid);
 
            try
            {
                // Unlink the shared memory object immediately so that it'll go away once all handles
                // to it are closed (as with opened then unlinked files, it'll remain usable via
                // the open handles even though it's unlinked and can't be opened anew via its name).
                Interop.CheckIo(Interop.Sys.ShmUnlink(mapName));
 
                // Give it the right capacity.  We do this directly with ftruncate rather
                // than via FileStream.SetLength after the FileStream is created because, on some systems,
                // lseek fails on shared memory objects, causing the FileStream to think it's unseekable,
                // causing it to preemptively throw from SetLength.
                Interop.CheckIo(Interop.Sys.FTruncate(fd, capacity));
 
                // shm_open sets CLOEXEC implicitly.  If the inheritability requested is Inheritable, remove CLOEXEC.
                if (inheritability == HandleInheritability.Inheritable &&
                    Interop.Sys.Fcntl.SetFD(fd, 0) == -1)
                {
                    throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo());
                }
 
                return fd;
            }
            catch
            {
                fd.Dispose();
                throw;
            }
        }
 
        private static string GenerateMapName()
        {
            // macOS shm_open documentation says that the sys-call can fail with ENAMETOOLONG if the name exceeds SHM_NAME_MAX characters.
            // The problem is that SHM_NAME_MAX is not defined anywhere and is not consistent amongst macOS versions (arm64 vs x64 for example).
            // It was reported in 2008 (https://lists.apple.com/archives/xcode-users/2008/Apr/msg00523.html),
            // but considered to be by design (http://web.archive.org/web/20140109200632/http://lists.apple.com/archives/darwin-development/2003/Mar/msg00244.html).
            // According to https://github.com/qt/qtbase/blob/1ed449e168af133184633d174fd7339a13d1d595/src/corelib/kernel/qsharedmemory.cpp#L53-L56 the actual value is 30.
            // Some other OSS libs use 32 (we did as well, but it was not enough) or 31, but we prefer 30 just to be extra safe.
            const int MaxNameLength = 30;
            // The POSIX shared memory object name must begin with '/'.  After that we just want something short (30) and unique.
            const string NamePrefix = "/dotnet_";
            return string.Create(MaxNameLength, 0, (span, state) =>
            {
                Span<char> guid = stackalloc char[32];
                Guid.NewGuid().TryFormat(guid, out int charsWritten, "N");
                Debug.Assert(charsWritten == 32);
                NamePrefix.CopyTo(span);
                guid.Slice(0, MaxNameLength - NamePrefix.Length).CopyTo(span.Slice(NamePrefix.Length));
                Debug.Assert(Encoding.UTF8.GetByteCount(span) <= MaxNameLength); // the standard uses Utf8
            });
        }
 
        private static SafeFileHandle CreateSharedBackingObjectUsingMemoryMemfdCreate(
           Interop.Sys.MemoryMappedProtections protections, long capacity, HandleInheritability inheritability)
        {
            int isReadonly = ((protections & Interop.Sys.MemoryMappedProtections.PROT_READ) != 0 &&
                    (protections & Interop.Sys.MemoryMappedProtections.PROT_WRITE) == 0) ? 1 : 0;
 
            SafeFileHandle fd = Interop.Sys.MemfdCreate(GenerateMapName(), isReadonly);
            if (fd.IsInvalid)
            {
                Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
                fd.Dispose();
 
                throw Interop.GetExceptionForIoErrno(errorInfo);
            }
 
            try
            {
                // Give it the right capacity.  We do this directly with ftruncate rather
                // than via FileStream.SetLength after the FileStream is created because, on some systems,
                // lseek fails on shared memory objects, causing the FileStream to think it's unseekable,
                // causing it to preemptively throw from SetLength.
                Interop.CheckIo(Interop.Sys.FTruncate(fd, capacity));
 
                // SystemNative_MemfdCreate sets CLOEXEC implicitly.  If the inheritability requested is Inheritable, remove CLOEXEC.
                if (inheritability == HandleInheritability.Inheritable &&
                    Interop.Sys.Fcntl.SetFD(fd, 0) == -1)
                {
                    throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo());
                }
 
                return fd;
            }
            catch
            {
                fd.Dispose();
                throw;
            }
        }
 
        private static SafeFileHandle CreateSharedBackingObjectUsingFile(Interop.Sys.MemoryMappedProtections protections, long capacity, HandleInheritability inheritability)
        {
            // We create a temporary backing file in TMPDIR.  We don't bother putting it into subdirectories as the file exists
            // extremely briefly: it's opened/created and then immediately unlinked.
            string path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
 
            FileShare share = inheritability == HandleInheritability.None ?
                FileShare.ReadWrite :
                FileShare.ReadWrite | FileShare.Inheritable;
 
            // Create the backing file, then immediately unlink it so that it'll be cleaned up when no longer in use.
            // Then enlarge it to the requested capacity.
            SafeFileHandle fileHandle = File.OpenHandle(path, FileMode.CreateNew, TranslateProtectionsToFileAccess(protections), share);
            try
            {
                Interop.CheckIo(Interop.Sys.Unlink(path));
                Interop.CheckIo(Interop.Sys.FTruncate(fileHandle, capacity), path);
            }
            catch
            {
                fileHandle.Dispose();
                throw;
            }
            return fileHandle;
        }
    }
}