Skip to content

[API Proposal]: APIs for working with unix file mode #67837

Description

@tmds

Background and motivation

Add direct support for getting/setting Unix file permissions and special bits.

These APIs allow to restrict/extend access to files and directories from .NET.

There have been some requests for these APIs: #925, #928, #17540.

The tar implementation can make use of them (#65951) without resorting to internal APIs.

API Proposal

namespace System.IO;

[System.FlagsAttribute]
public enum UnixFileMode
{
// From https://github.com/dotnet/runtime/issues/65951.
    None = 0,
    OtherExecute = 1,
    OtherWrite = 2,
    OtherRead = 4,
    GroupExecute = 8,
    GroupWrite = 16,
    GroupRead = 32,
    UserExecute = 64,
    UserWrite = 128,
    UserRead = 256,
    StickyBit = 512,
    SetGroup = 1024, // or, more generic name: GroupSpecial
    SetUser = 2048,  // or, more generic name: UserSpecial

// Maybe:
    AccessPermissionMask = UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute,
    DefaultFileOpenPermissions = UserRead | UserWrite | GroupRead | GroupWrite | OtherRead | OtherWrite
}

static partial class Directory
{
    // Set mode when creating file, returns SafeFileHandle.
    public static SafeFileHandle OpenHandle (string path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read, FileShare share = FileShare.Read, FileOptions options = FileOptions.None, long preallocationSize = 0, UnixFileMode unixCreateMode = UnixFileMode.DefaultFileOpenPermissions);

    // Set mode when creating directory
    public static DirectoryInfo CreateDirectory(string path, UnixFileMode unixCreateMode);
}

static partial class File
{
    public static SafeFileHandle OpenHandle (string path, System.IO.FileMode mode = System.IO.FileMode.Open, System.IO.FileAccess access = System.IO.FileAccess.Read, System.IO.FileShare share = System.IO.FileShare.Read, System.IO.FileOptions options = System.IO.FileOptions.None, long preallocationSize = 0);
    [EditorBrowsable(EditorBrowsableState.Never)]
    public static SafeFileHandle OpenHandle (string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize = 0); // Existing API, marked as Never Browsable to add a new default parameter.

    // Get/Set from SafeHandle, (.NET 7) https://github.com/dotnet/runtime/issues/20234
    public static UnixFileMode GetUnixFileMode(SafeFileHandle fileHandle);
    public static void SetUnixFileMode(SafeFileHandle fileHandle, UnixFileMode mode);
}

class FileSystemInfo
{
    // Get/Set from string path
    public UnixFileMode UnixFileMode { get; set; }
}

// Set mode when creating file, returns FileStream (by `File.Open`).
class FileStreamOptions
{
    // Set mode when creating file
    public UnixFileMode UnixCreateMode { get; set; } = UnixFileMode.DefaultFileOpenPermissions;
}

// `UnixFileMode` replaces `TarFileMode` (https://github.com/dotnet/runtime/issues/65951)
namespace System.Formats.Tar
{
-    enum TarFileMode { ... }
}

Notes

On Windows/non-Unix:

  • The unixCreateMode arg on Directory.CreateDirectory/File.OpenHandle and FileStreamOptions.UnixCreateMode are ignored. These APIs can be used in in cross-platform code.

  • FileSystemInfo.UnixFileMode get returns UnixFileMode.None.

  • FileSystemInfo.UnixFileMode set throws PNSE.

APIs that create a file/directory (Directory.CreateDirectory, FileStreamOptions.UnixCreateMode, File.OpenHandle) have 'POSIX behavior':

  • If the file/directory exists, the mode argument is ignored.

  • The OS kernel filters the provided mode by the process umask. The umask protects users from unintentionally opening up permissions. A regular Linux user has a umask of 0002 which prevents it from giving write permissions to other. Use-cases that require the exact mode, must make an additional call to SetUnixFileMode. See example 3.

  • note: creating and setting permissions is atomic to ensure permissions are applied immediately

UnixFileMode can replace TarFileMode introduced in #65951.

AccessPermissionMask, DefaultFileOpenPermissions enum values.

  • These are convenient masks. Example usage of these masks:
    // Only extract USR, GRP, and OTH file permissions, and ignore
    // S_ISUID, S_ISGID, and S_ISVTX bits.
    // It is off by default because it's possible that a file in an archive could have
    // one of these bits set and, unknown to the person extracting, could allow others to
    // execute the file as the user or group.
    const int ExtractPermissionMask = 0x1FF;
    int permissions = (int)Mode & ExtractPermissionMask;
    and
    // 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.
    private const Interop.Sys.Permissions DefaultOpenPermissions =
    Interop.Sys.Permissions.S_IRUSR | Interop.Sys.Permissions.S_IWUSR |
    Interop.Sys.Permissions.S_IRGRP | Interop.Sys.Permissions.S_IWGRP |
    Interop.Sys.Permissions.S_IROTH | Interop.Sys.Permissions.S_IWOTH;
    .

API Usage

Example 1: limit a file to be read/written by the owner only:

File.SetUnixFileMode("filename", UnixFileMode.UserRead | UnixFileMode.UserWrite);

Example 2: exclude 'Other' access to entries in a directory:

var di = new DirectoryInfo("somedir");
foreach (var fi in di.GetFileSystemInfos())
{
  fi.UnixFleMode = fi.UnixFileMode & ~(UnixFileMode.OtherExecute | UnixFileMode.OtherRead | UnixFileMode.OtherWrite);
}

Example 3: create new files/directories while extracting a tar file with the exact permissions of the tar file.

if (entry.EntryType == EntryType.Directory)
{
    DirectoryInfo directoryInfo = Directory.CreateDirectory(path, entry.Mode); // filtered by umask
    di.UnixFileMode = entry.Mode;
}
else if (entry.EntryType == EntryType.RegularFile)
{
    using var file = File.OpenHandle(path, entry.Mode); // filtered by umask
    File.SetUnixFileMode(file, entry.Mode);
}

Alternative Designs

No response

Risks

No response

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.IOblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions