'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.



Solution 1:[1]

Just for the record, fxstatat() and statx() structs do not give me the correct file permission value on MAC.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 TTTT