// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.IO; using System.IO.Strategies; using System.Threading; namespace Microsoft.Win32.SafeHandles { public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid { private const UnixFileMode PermissionMask = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; // If the file gets created a new, we'll select the permissions for it. Most Unix utilities by default use 666 (read and // write for all), so we do the same (even though this doesn't match Windows, where by default it's possible to write out // a file and then execute it). No matter what we choose, it'll be subject to the umask applied by the system, such that the // actual permissions will typically be less than what we select here. internal const UnixFileMode DefaultCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.OtherRead | UnixFileMode.OtherWrite; internal static bool DisableFileLocking { get; } = OperatingSystem.IsBrowser() // #40065: Emscripten does not support file locking || AppContextConfigHelper.GetBooleanConfig("System.IO.DisableFileLocking", "DOTNET_SYSTEM_IO_DISABLEFILELOCKING", defaultValue: false); // not using bool? as it's not thread safe private volatile NullableBool _canSeek = NullableBool.Undefined; private volatile NullableBool _supportsRandomAccess = NullableBool.Undefined; private bool _deleteOnClose; private bool _isLocked; public SafeFileHandle() : this(ownsHandle: true) { } private SafeFileHandle(bool ownsHandle) : base(ownsHandle) { SetHandle(new IntPtr(-1)); } public bool IsAsync { get; private set; } internal bool CanSeek => !IsClosed && GetCanSeek(); internal bool SupportsRandomAccess { get { NullableBool supportsRandomAccess = _supportsRandomAccess; if (supportsRandomAccess == NullableBool.Undefined) { _supportsRandomAccess = supportsRandomAccess = GetCanSeek() ? NullableBool.True : NullableBool.False; } return supportsRandomAccess == NullableBool.True; } set { Debug.Assert(value == false); // We should only use the setter to disable random access. _supportsRandomAccess = value ? NullableBool.True : NullableBool.False; } } #pragma warning disable CA1822 internal ThreadPoolBoundHandle? ThreadPoolBinding => null; internal void EnsureThreadPoolBindingInitialized() { /* nop */ } internal bool TryGetCachedLength(out long cachedLength) { cachedLength = -1; return false; } #pragma warning restore CA1822 private static SafeFileHandle Open(string path, Interop.Sys.OpenFlags flags, int mode, bool failForSymlink, out bool wasSymlink, Func<Interop.ErrorInfo, Interop.Sys.OpenFlags, string, Exception?>? createOpenException) { wasSymlink = false; Debug.Assert(path != null); SafeFileHandle handle = Interop.Sys.Open(path, flags, mode); handle._path = path; if (handle.IsInvalid) { Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); handle.Dispose(); if (failForSymlink && error.Error == Interop.Error.ELOOP) { wasSymlink = true; return handle; } if (createOpenException?.Invoke(error, flags, path) is Exception ex) { throw ex; } if (error.Error == Interop.Error.EISDIR) { error = Interop.Error.EACCES.Info(); } Interop.CheckIo(error.Error, path); } return handle; } // Each thread will have its own copy. This prevents race conditions if the handle had the last error. [ThreadStatic] internal static Interop.ErrorInfo? t_lastCloseErrorInfo; protected override bool ReleaseHandle() { // If DeleteOnClose was requested when constructed, delete the file now. // (Unix doesn't directly support DeleteOnClose, so we mimic it here.) // We delete the file before releasing the lock to detect the removal in Init. if (_deleteOnClose) { // Since we still have the file open, this will end up deleting // it (assuming we're the only link to it) once it's closed, but the // name will be removed immediately. Debug.Assert(_path is not null); Interop.Sys.Unlink(_path); // ignore errors; it's valid that the path may no longer exist } // When the SafeFileHandle was opened, we likely issued an flock on the created descriptor in order to add // an advisory lock. This lock should be removed via closing the file descriptor, but close can be // interrupted, and we don't retry closes. As such, we could end up leaving the file locked, // which could prevent subsequent usage of the file until this process dies. To avoid that, we proactively // try to release the lock before we close the handle. if (_isLocked) { Interop.Sys.FLock(handle, Interop.Sys.LockOperations.LOCK_UN); // ignore any errors _isLocked = false; } // Close the descriptor. Although close is documented to potentially fail with EINTR, we never want // to retry, as the descriptor could actually have been closed, been subsequently reassigned, and // be in use elsewhere in the process. Instead, we simply check whether the call was successful. int result = Interop.Sys.Close(handle); if (result != 0) { t_lastCloseErrorInfo = Interop.Sys.GetLastErrorInfo(); } return result == 0; } public override bool IsInvalid { get { long h = (long)handle; return h < 0 || h > int.MaxValue; } } // Specialized Open that returns the file length and permissions of the opened file. // This information is retrieved from the 'stat' syscall that must be performed to ensure the path is not a directory. internal static SafeFileHandle OpenReadOnly(string fullPath, FileOptions options, out long fileLength, out UnixFileMode filePermissions) { SafeFileHandle handle = Open(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read, options, preallocationSize: 0, DefaultCreateMode, out fileLength, out filePermissions, false, out _, null); Debug.Assert(fileLength >= 0); return handle; } internal static SafeFileHandle Open(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, UnixFileMode? unixCreateMode = null, Func<Interop.ErrorInfo, Interop.Sys.OpenFlags, string, Exception?>? createOpenException = null) { return Open(fullPath, mode, access, share, options, preallocationSize, unixCreateMode ?? DefaultCreateMode, out _, out _, false, out _, createOpenException); } internal static SafeFileHandle? OpenNoFollowSymlink(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, out bool wasSymlink, UnixFileMode? unixCreateMode = null, Func<Interop.ErrorInfo, Interop.Sys.OpenFlags, string, Exception?>? createOpenException = null) { return Open(fullPath, mode, access, share, options, preallocationSize, unixCreateMode ?? DefaultCreateMode, out _, out _, true, out wasSymlink, createOpenException); } private static SafeFileHandle Open(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, UnixFileMode openPermissions, out long fileLength, out UnixFileMode filePermissions, bool failForSymlink, out bool wasSymlink, Func<Interop.ErrorInfo, Interop.Sys.OpenFlags, string, Exception?>? createOpenException = null) { // Translate the arguments into arguments for an open call. Interop.Sys.OpenFlags openFlags = PreOpenConfigurationFromOptions(mode, access, share, options, failForSymlink); SafeFileHandle? safeFileHandle = null; try { while (true) { safeFileHandle = Open(fullPath, openFlags, (int)openPermissions, failForSymlink, out wasSymlink, createOpenException); if (failForSymlink && wasSymlink) { fileLength = default; filePermissions = default; return safeFileHandle; } // When Init return false, the path has changed to another file entry, and // we need to re-open the path to reflect that. if (safeFileHandle.Init(fullPath, mode, access, share, options, preallocationSize, out fileLength, out filePermissions)) { return safeFileHandle; } else { safeFileHandle.Dispose(); } } } catch (Exception) { safeFileHandle?.Dispose(); throw; } } /// <summary>Translates the FileMode, FileAccess, and FileOptions values into flags to be passed when opening the file.</summary> /// <param name="mode">The FileMode provided to the stream's constructor.</param> /// <param name="access">The FileAccess provided to the stream's constructor</param> /// <param name="share">The FileShare provided to the stream's constructor</param> /// <param name="options">The FileOptions provided to the stream's constructor</param> /// <param name="failForSymlink">Whether to cause ELOOP error when opening a symlink</param> /// <returns>The flags value to be passed to the open system call.</returns> private static Interop.Sys.OpenFlags PreOpenConfigurationFromOptions(FileMode mode, FileAccess access, FileShare share, FileOptions options, bool failForSymlink) { // Translate FileMode. Most of the values map cleanly to one or more options for open. Interop.Sys.OpenFlags flags = default; if (failForSymlink) { flags |= Interop.Sys.OpenFlags.O_NOFOLLOW; } switch (mode) { default: case FileMode.Open: // Open maps to the default behavior for open(...). No flags needed. break; case FileMode.Truncate: if (DisableFileLocking) { // if we don't lock the file, we can truncate it when opening // otherwise we truncate the file after getting the lock flags |= Interop.Sys.OpenFlags.O_TRUNC; } break; case FileMode.Append: // Append is the same as OpenOrCreate, except that we'll also separately jump to the end later case FileMode.OpenOrCreate: flags |= Interop.Sys.OpenFlags.O_CREAT; break; case FileMode.Create: flags |= Interop.Sys.OpenFlags.O_CREAT; if (DisableFileLocking) { flags |= Interop.Sys.OpenFlags.O_TRUNC; } break; case FileMode.CreateNew: flags |= (Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL); break; } // Translate FileAccess. All possible values map cleanly to corresponding values for open. switch (access) { case FileAccess.Read: flags |= Interop.Sys.OpenFlags.O_RDONLY; break; case FileAccess.ReadWrite: flags |= Interop.Sys.OpenFlags.O_RDWR; break; case FileAccess.Write: flags |= Interop.Sys.OpenFlags.O_WRONLY; break; } // Handle Inheritable, other FileShare flags are handled by Init if ((share & FileShare.Inheritable) == 0) { flags |= Interop.Sys.OpenFlags.O_CLOEXEC; } // Translate some FileOptions; some just aren't supported, and others will be handled after calling open. // - Asynchronous: Handled in ctor, setting _useAsync and SafeFileHandle.IsAsync to true // - DeleteOnClose: Doesn't have a Unix equivalent, but we approximate it in Dispose // - Encrypted: No equivalent on Unix and is ignored // - RandomAccess: Implemented after open if posix_fadvise is available // - SequentialScan: Implemented after open if posix_fadvise is available // - WriteThrough: Handled here if ((options & FileOptions.WriteThrough) != 0) { flags |= Interop.Sys.OpenFlags.O_SYNC; } return flags; } private bool Init(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, out long fileLength, out UnixFileMode filePermissions) { Interop.Sys.FileStatus status = default; bool statusHasValue = false; fileLength = -1; filePermissions = 0; // Make sure our handle is not a directory. // We can omit the check when write access is requested. open will have failed with EISDIR. if ((access & FileAccess.Write) == 0) { // Stat the file descriptor to avoid race conditions. FStatCheckIO(path, ref status, ref statusHasValue); if ((status.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR) { throw Interop.GetExceptionForIoErrno(Interop.Error.EACCES.Info(), path); } if ((status.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFREG) { // we take advantage of the information provided by the fstat syscall // and for regular files (most common case) // avoid one extra sys call for determining whether file can be seeked _canSeek = NullableBool.True; Debug.Assert(Interop.Sys.LSeek(this, 0, Interop.Sys.SeekWhence.SEEK_CUR) >= 0); } fileLength = status.Size; filePermissions = ((UnixFileMode)status.Mode) & PermissionMask; } IsAsync = (options & FileOptions.Asynchronous) != 0; // Lock the file if requested via FileShare. This is only advisory locking. FileShare.None implies an exclusive // lock on the file and all other modes use a shared lock. While this is not as granular as Windows, not mandatory, // and not atomic with file opening, it's better than nothing. Interop.Sys.LockOperations lockOperation = (share == FileShare.None) ? Interop.Sys.LockOperations.LOCK_EX : Interop.Sys.LockOperations.LOCK_SH; if (CanLockTheFile(lockOperation, access) && !(_isLocked = Interop.Sys.FLock(this, lockOperation | Interop.Sys.LockOperations.LOCK_NB) >= 0)) { // The only error we care about is EWOULDBLOCK, which indicates that the file is currently locked by someone // else and we would block trying to access it. Other errors, such as ENOTSUP (locking isn't supported) or // EACCES (the file system doesn't allow us to lock), will only hamper FileStream's usage without providing value, // given again that this is only advisory / best-effort. Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); if (errorInfo.Error == Interop.Error.EWOULDBLOCK) { throw Interop.GetExceptionForIoErrno(errorInfo, path); } } // On Windows, DeleteOnClose happens when all kernel handles to the file are closed. // Unix kernels don't have this feature, and .NET deletes the file when the Handle gets disposed. // When the file is opened with an exclusive lock, we can use it to check the file at the path // still matches the file we've opened. // When the delete is performed by another .NET Handle, it holds the lock during the delete. // Since we've just obtained the lock, the file will already be removed/replaced. // We limit performing this check to cases where our file was opened with DeleteOnClose with // a mode of OpenOrCreate. if (_isLocked && ((options & FileOptions.DeleteOnClose) != 0) && share == FileShare.None && mode == FileMode.OpenOrCreate) { FStatCheckIO(path, ref status, ref statusHasValue); Interop.Sys.FileStatus pathStatus; if (Interop.Sys.Stat(path, out pathStatus) < 0) { // If the file was removed, re-open. // Otherwise throw the error 'stat' gave us (assuming this is the // error 'open' will give us if we'd call it now). Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); if (error.Error == Interop.Error.ENOENT) { return false; } throw Interop.GetExceptionForIoErrno(error, path); } if (pathStatus.Ino != status.Ino || pathStatus.Dev != status.Dev) { // The file was replaced, re-open return false; } } // Enable DeleteOnClose when we've successfully locked the file. // On Windows, the locking happens atomically as part of opening the file. _deleteOnClose = (options & FileOptions.DeleteOnClose) != 0; // These provide hints around how the file will be accessed. Specifying both RandomAccess // and Sequential together doesn't make sense as they are two competing options on the same spectrum, // so if both are specified, we prefer RandomAccess (behavior on Windows is unspecified if both are provided). Interop.Sys.FileAdvice fadv = (options & FileOptions.RandomAccess) != 0 ? Interop.Sys.FileAdvice.POSIX_FADV_RANDOM : (options & FileOptions.SequentialScan) != 0 ? Interop.Sys.FileAdvice.POSIX_FADV_SEQUENTIAL : 0; if (fadv != 0) { FileStreamHelpers.CheckFileCall(Interop.Sys.PosixFAdvise(this, 0, 0, fadv), path, ignoreNotSupported: true); // just a hint. } if ((mode == FileMode.Create || mode == FileMode.Truncate) && !DisableFileLocking) { // Truncate the file now if the file mode requires it. This ensures that the file only will be truncated // if opened successfully. if (Interop.Sys.FTruncate(this, 0) < 0) { Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); if (errorInfo.Error != Interop.Error.EBADF && errorInfo.Error != Interop.Error.EINVAL) { // We know the file descriptor is valid and we know the size argument to FTruncate is correct, // so if EBADF or EINVAL is returned, it means we're dealing with a special file that can't be // truncated. Ignore the error in such cases; in all others, throw. throw Interop.GetExceptionForIoErrno(errorInfo, path); } } } if (preallocationSize > 0 && Interop.Sys.FAllocate(this, 0, preallocationSize) < 0) { Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); // Only throw for errors that indicate there is not enough space. if (errorInfo.Error == Interop.Error.EFBIG || errorInfo.Error == Interop.Error.ENOSPC) { Dispose(); // Delete the file we've created. Debug.Assert(mode == FileMode.Create || mode == FileMode.CreateNew); Interop.Sys.Unlink(path!); throw new IOException(SR.Format(errorInfo.Error == Interop.Error.EFBIG ? SR.IO_FileTooLarge_Path_AllocationSize : SR.IO_DiskFull_Path_AllocationSize, path, preallocationSize)); } } return true; } private bool CanLockTheFile(Interop.Sys.LockOperations lockOperation, FileAccess access) { Debug.Assert(lockOperation == Interop.Sys.LockOperations.LOCK_EX || lockOperation == Interop.Sys.LockOperations.LOCK_SH); if (DisableFileLocking) { return false; } else if (lockOperation == Interop.Sys.LockOperations.LOCK_EX) { return true; // LOCK_EX is always OK } else if ((access & FileAccess.Write) == 0) { return true; // LOCK_SH is always OK when reading } if (!Interop.Sys.TryGetFileSystemType(this, out Interop.Sys.UnixFileSystemTypes unixFileSystemType)) { return false; // assume we should not acquire the lock if we don't know the File System } switch (unixFileSystemType) { case Interop.Sys.UnixFileSystemTypes.nfs: // #44546 case Interop.Sys.UnixFileSystemTypes.smb: case Interop.Sys.UnixFileSystemTypes.smb2: // #53182 case Interop.Sys.UnixFileSystemTypes.cifs: return false; // LOCK_SH is not OK when writing to NFS, CIFS or SMB default: return true; // in all other situations it should be OK } } private void FStatCheckIO(string path, ref Interop.Sys.FileStatus status, ref bool statusHasValue) { if (!statusHasValue) { if (Interop.Sys.FStat(this, out status) != 0) { Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); throw Interop.GetExceptionForIoErrno(error, path); } statusHasValue = true; } } private bool GetCanSeek() { Debug.Assert(!IsClosed); Debug.Assert(!IsInvalid); NullableBool canSeek = _canSeek; if (canSeek == NullableBool.Undefined) { _canSeek = canSeek = Interop.Sys.LSeek(this, 0, Interop.Sys.SeekWhence.SEEK_CUR) >= 0 ? NullableBool.True : NullableBool.False; } return canSeek == NullableBool.True; } internal long GetFileLength() { int result = Interop.Sys.FStat(this, out Interop.Sys.FileStatus status); FileStreamHelpers.CheckFileCall(result, Path); return status.Size; } private enum NullableBool { Undefined = 0, False = -1, True = 1 } } } |