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; }