Skip to content

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

Closed
@tmds

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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions