Skip to content
Advertisement

How to get Linux file permissions in .NET 5 / .NET 6 without Mono.Posix with p/invoke?

I recently found, that I can make Linux system calls from .NET relatively easy.

For example, to see if I need sudo I just make a signature like this:

internal class Syscall {

[DllImport("libc", SetLastError = true)]
internal static extern uint geteuid();

// ...
}
public static bool IsRoot => Syscall.geteuid() == 0;

Neat. So much easier and faster than everything else, right? This is the simplest possible system call, other use strings and structures.

After some digging in the documentation and testing for myself, I found that strings from libc can be mapped directly to string from char* by default marshaller, most of the other stuff require just using some fun with manually mapping IntPtr to structures.

So in similar way I quickly mapped chmod, chown, lchown, getgrnam, getpwnam, getuid, symlink. All tested on my Ubuntu VM, works.

I even made my own super neat Chmod implementation that works identically to shell chmod that accepts relative permissions like u+wX. And walks through the filesystem.

And that’s where I lost a night. I needed original permissions, and I read they can be obtained with stat call. What could possibly go wrong?

First I made Stat structure using Linux manual documentation: https://man7.org/linux/man-pages/man2/stat.2.html

Then I made the appropriate extern.

First surprise: the entry point not found.

I digged, and digged and digged some more. Until I just opened my libc binary and searched for something similar to stat. Bingo! I found __xstat point. That was it, I changed my signature, I read in documentation that beside specifying ver parameter (that should be set to 3) – it should work identical to stat.

It didn’t. The call passes, but it always return -1, does not return Stat structure.

Then I found some sources of the __xstat where it checks if the ver parameter matches the kernel version. WEIRD! But I tried passing 5. Because it’s the current kernel version I use. Also some other numbers like ‘3’ and ‘0’. No luck. Nothing works. I also tested __xstat64. Same result, I mean no result.

Then I found a discussion on GitHub between .NET developers, that calling stat is super tricky, because it’s different on every kernel. Wait, WHAT!?

Yes, I know it is in Mono.Posix.NETStandard 1.0.0 package, I use it and it works. (And that’s what the guys recommended.)

But since I’m just learning platform invoke “voodoo” – I just cannot leave it like that. Why everything but the stat call works without any problem, why is there the exception? It is a completely BASIC thing. Then after “why” comes “HOW?”.

They did it in Mono. I digged in Mono sources on GitHub to find, that it’s one of the few function not actually called from libc but from their own assembly in C: https://github.com/mono/mono/blob/main/support/sys-stat.c

Interesting, but I still struggle to understand how it works.

BTW, adding Mono to my project increased my compiled executable Linux x64 file from 200kb to 1200kb. To add literally 1 function of reading a single number! BTW, it has a license issue, package signature says MIT, source file linked says MPL. And my package is asking users to accept this curious license. I mean, to accept MIT, though I’m not quite sure whether it’s really MIT or MPL. My own package uses MIT.

So – what are the (other) catches and gotchas when calling libc from dotnet? Is there a simpler way to call stat()? Is there an alternative route to get the permissions from .NET? I figured out the .NET itself DOES that internally. It gets the file permissions obtainable from FileInfo. However, the attributed are “translated” to Windows structure, and most of the information is lost in translation.

My last try:

[DllImport("libc", SetLastError = true)]
internal static extern int __xstat(int ver, string path, out Stat stat);

internal struct Stat {

    public ulong st_dev;        // device
    public ulong st_ino;        // inode
    public uint st_mode;        // protection
    public ulong st_nlink;      // number of hard links
    public uint st_uid;         // user ID of owner
    public uint st_gid;         // group ID of owner
    public ulong st_rdev;       // device type (if inode device)
    public long st_size;        // total size, in bytes
    public long st_blksize;     // blocksize for filesystem I/O
    public long st_blocks;      // number of blocks allocated
    public long st_atime;       // time of last access
    public long st_mtime;       // time of last modification
    public long st_ctime;       // time of last status change
    public long st_atime_nsec;  // Timespec.tv_nsec partner to st_atime
    public long st_mtime_nsec;  // Timespec.tv_nsec partner to st_mtime
    public long st_ctime_nsec;  // Timespec.tv_nsec partner to st_ctime

}

Called like Syscall.__xstat(5, path, out Stat stat). Returns -1 for any path I tried.

Of course

public static Permissions GetPermissions(string path) {
    if (Mono.Unix.Native.Syscall.stat(path, out var stat) != 0) throw new InvalidOperationException($"Stat call failed for {path}");
    return new Permissions((uint)stat.st_mode);
}

works. It only takes 1MB more 😉 I know, it’s nothing, but I have external dependency just for 1 simple function.

From what I researched – the Stat structure differs from kernel to kernel. I suspect if I tried some other versions, one would finally work, but it doesn’t solve the problem at all, because it can stop working after an update on target machine.

My guess is when the structure is required and allowed to change in Linux, there must be a kind of common interface / compatibility mechanism allowing users to get permissions without detailed knowledge about system and library versions on a specific target machine.

I thought libc was just something like that, but it seems like either it’s not exactly it, or there is a bit higher level interface somewhere else in Linux and I don’t mean shell here 😉

I have mainly Windows background, I used Windows p/invoke a lot. Most of the code I wrote for Windows 7 still works on Windows 11. Old Win32 calls haven’t changed, except some very system UI specific ones.

Advertisement

Answer

So, I was wrong posting the last answer. I found out, the libc binary contained something like __xstat and I called it.

Wrong! As the name would suggest, it was a kind of a private function, something intended to be an implementation detail, not a part of the API.

So I found another function with a normal name: statx. It does exactly what I need, it is well(-ish) documented here:

https://man7.org/linux/man-pages/man2/statx.2.html

Here’s the structure and values: https://code.woboq.org/qt5/include/bits/statx.h.html https://code.woboq.org/userspace/glibc/io/fcntl.h.html

TL;DR – it works.

I figured out that -100 (AT_FDCWD) passed as dirfd parameter makes relative paths relative to the current working directory.

I also figured out that passing zeros as flags works (as equivalent to AT_STATX_SYNC_AS_STAT), and the function returns what it should for a regular local filesystem.

So here’s the code:

[DllImport(LIBC, SetLastError = true)]
internal static extern int statx(int dirfd, string path, int flags, uint mask, out Statx data);

/// <summary>
/// POSIX statx data structure.
/// </summary>
internal struct Statx {

    /// <summary>
    /// Mask of bits indicating filled fields.
    /// </summary>
    internal uint Mask;
    /// <summary>
    /// Block size for filesystem I/O.
    /// </summary>
    internal uint BlockSize;
    /// <summary>
    /// Extra file attribute indicators
    /// </summary>
    internal ulong Attributes;
    /// <summary>
    /// Number of hard links.
    /// </summary>
    internal uint HardLinks;
    /// <summary>
    /// User ID of owner.
    /// </summary>
    internal uint Uid;
    /// <summary>
    /// Group ID of owner.
    /// </summary>
    internal uint Gid;
    /// <summary>
    /// File type and mode.
    /// </summary>
    internal ushort Mode;
    private ushort Padding01;
    /// <summary>
    /// Inode number.
    /// </summary>
    internal ulong Inode;
    /// <summary>
    /// Total size in bytes.
    /// </summary>
    internal ulong Size;
    /// <summary>
    /// Number of 512B blocks allocated.
    /// </summary>
    internal ulong Blocks;
    /// <summary>
    /// Mask to show what's supported in <see cref="Attributes"/>.
    /// </summary>
    internal ulong AttributesMask;
    /// <summary>
    /// Last access time.
    /// </summary>
    internal StatxTimeStamp AccessTime;
    /// <summary>
    /// Creation time.
    /// </summary>
    internal StatxTimeStamp CreationTime;
    /// <summary>
    /// Last status change time.
    /// </summary>
    internal StatxTimeStamp StatusChangeTime;
    /// <summary>
    /// Last modification time.
    /// </summary>
    internal StatxTimeStamp LastModificationTime;
    internal uint RDevIdMajor;
    internal uint RDevIdMinor;
    internal uint DevIdMajor;
    internal uint DevIdMinor;
    internal ulong MountId;
    private ulong Padding02;
    private ulong Padding03;
    private ulong Padding04;
    private ulong Padding05;
    private ulong Padding06;
    private ulong Padding07;
    private ulong Padding08;
    private ulong Padding09;
    private ulong Padding10;
    private ulong Padding11;
    private ulong Padding12;
    private ulong Padding13;
    private ulong Padding14;
    private ulong Padding15;
}

/// <summary>
/// Time stamp structure used by statx.
/// </summary>
public struct StatxTimeStamp {

    /// <summary>
    /// Seconds since the Epoch (UNIX time).
    /// </summary>
    public long Seconds;

    /// <summary>
    /// Nanoseconds since <see cref="Seconds"/>.
    /// </summary>
    public uint Nanoseconds;

}
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement