From e8d4d4730f91ea24887845f9e732a6c0ed1d461b Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 7 Jul 2026 22:32:07 -0700 Subject: [PATCH] Migrate libpsl-native Phase 1 functions to managed/libc P/Invoke Replace the low-risk libpsl-native functions (those not requiring stat/passwd struct marshalling) with pure managed code and direct LibraryImport("libc") calls, following the engine/Interop/Windows convention with a parallel engine/Interop/Unix folder. Migrated: - GetErrorCategory -> managed errno->ErrorCategory table - CreateSymLink/CreateHardLink -> libc symlink/link - IsExecutable -> libc access(path, X_OK) - KillProcess -> libc kill(pid, SIGKILL) - WaitPid -> libc waitpid - GetCurrentThreadId -> libc gettid (Linux) / pthread_threadid_np (macOS) - SetDate -> libc settimeofday (via DateTimeOffset; drops UnixTm struct) - Syslog Open/SysLog/Close -> libc openlog/syslog/closelog Interop.Unix is compiled only on non-Windows (per csproj), so the consuming bodies in CorePsPlatform are guarded with #if UNIX. The remaining libpsl-native imports (stat, passwd/group, GetUserFromPid, ForkAndExecProcess) are deferred to later phases. Verified: Windows build and cross-compiled linux-x64 build both pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CoreCLR/CorePsPlatform.cs | 157 +++++++++++------- .../engine/Interop/Unix/FileSystem.cs | 36 ++++ .../engine/Interop/Unix/Process.cs | 46 +++++ .../engine/Interop/Unix/Time.cs | 34 ++++ .../utils/tracing/SysLogProvider.cs | 42 ++++- 5 files changed, 244 insertions(+), 71 deletions(-) create mode 100644 src/System.Management.Automation/engine/Interop/Unix/FileSystem.cs create mode 100644 src/System.Management.Automation/engine/Interop/Unix/Process.cs create mode 100644 src/System.Management.Automation/engine/Interop/Unix/Time.cs diff --git a/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs b/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs index 4cbb346fb14..44ccb10963b 100644 --- a/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs +++ b/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs @@ -453,10 +453,9 @@ internal static bool NonWindowsCreateHardLink(string path, string strTargetPath) return Unix.NativeMethods.CreateHardLink(path, strTargetPath) == 0; } - internal static unsafe bool NonWindowsSetDate(DateTime dateToUse) + internal static bool NonWindowsSetDate(DateTime dateToUse) { - Unix.NativeMethods.UnixTm tm = Unix.NativeMethods.DateTimeToUnixTm(dateToUse); - return Unix.NativeMethods.SetDate(&tm) == 0; + return Unix.NativeMethods.SetDate(dateToUse) == 0; } internal static bool NonWindowsIsSameFileSystemItem(string pathOne, string pathTwo) @@ -943,8 +942,34 @@ internal static partial class NativeMethods // Ansi is a misnomer, it is hardcoded to UTF-8 on Linux and macOS // C bools are 1 byte and so must be marshalled as I1 - [LibraryImport(psLib)] - internal static partial int GetErrorCategory(int errno); + // errno values below are the standard POSIX numbers, identical on Linux and macOS. + private const int EPERM = 1; + private const int ENOENT = 2; + private const int ESRCH = 3; + private const int EINTR = 4; + private const int EACCES = 13; + private const int EINVAL = 22; + + // Maps a Unix errno to the integer value of a PowerShell ErrorCategory. + // Mirrors the mapping previously provided by libpsl-native's GetErrorCategory. + internal static int GetErrorCategory(int errno) + { + switch (errno) + { + case EINVAL: + return (int)ErrorCategory.InvalidArgument; + case ENOENT: + case ESRCH: + return (int)ErrorCategory.ObjectNotFound; + case EINTR: + return (int)ErrorCategory.OperationStopped; + case EACCES: + case EPERM: + return (int)ErrorCategory.PermissionDenied; + default: + return (int)ErrorCategory.NotSpecified; + } + } [LibraryImport(psLib)] internal static partial int GetPPid(int pid); @@ -952,78 +977,84 @@ internal static partial class NativeMethods [LibraryImport(psLib, StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] internal static partial int GetLinkCount(string filePath, out int linkCount); - [LibraryImport(psLib, StringMarshalling = StringMarshalling.Utf8)] - [return: MarshalAs(UnmanagedType.I1)] - internal static partial bool IsExecutable(string filePath); - - [LibraryImport(psLib)] - internal static partial uint GetCurrentThreadId(); - - [LibraryImport(psLib)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static partial bool KillProcess(int pid); - - [LibraryImport(psLib)] - internal static partial int WaitPid(int pid, [MarshalAs(UnmanagedType.Bool)] bool nohang); - - // This is the struct `private_tm` from setdate.h in libpsl-native. - // Packing is set to 4 to match the unmanaged declaration. - // https://github.com/PowerShell/PowerShell-Native/blob/c5575ceb064e60355b9fee33eabae6c6d2708d14/src/libpsl-native/src/setdate.h#L23 - [StructLayout(LayoutKind.Sequential, Pack = 4)] - internal unsafe struct UnixTm + // The methods below replace former libpsl-native P/Invokes with direct calls to + // libc (see engine/Interop/Unix/*). The Interop.Unix declarations are compiled only + // for non-Windows builds, so the bodies are guarded with #if UNIX. These methods are + // never invoked on Windows. +#if UNIX + // access(path, X_OK) returns 0 when the file is executable, -1 otherwise. + internal static bool IsExecutable(string filePath) { - /// Seconds (0-60). - internal int tm_sec; - - /// Minutes (0-59). - internal int tm_min; - - /// Hours (0-23). - internal int tm_hour; + return Interop.Unix.Access(filePath, Interop.Unix.X_OK) != -1; + } - /// Day of the month (1-31). - internal int tm_mday; + internal static uint GetCurrentThreadId() + { + if (OperatingSystem.IsMacOS()) + { + Interop.Unix.PthreadThreadIdNp(IntPtr.Zero, out ulong tid); + return (uint)tid; + } - /// Month (0-11). - internal int tm_mon; + return (uint)Interop.Unix.GetTid(); + } - /// The year - 1900. - internal int tm_year; + internal static bool KillProcess(int pid) + { + return Interop.Unix.Kill(pid, Interop.Unix.SIGKILL) == 0; + } - /// Day of the week (0-6, Sunday = 0). - internal int tm_wday; + internal static int WaitPid(int pid, bool nohang) + { + return Interop.Unix.WaitPid(pid, IntPtr.Zero, nohang ? Interop.Unix.WNOHANG : 0); + } - /// Day in the year (0-365, 1 Jan = 0). - internal int tm_yday; + // Set the system clock. Mirrors libpsl-native's SetDate, which converted a + // broken-down local time via mktime() and called settimeofday(). DateTimeOffset + // performs the equivalent local-time-to-Unix-seconds conversion in managed code. + internal static int SetDate(DateTime date) + { + Interop.Unix.Timeval tv; + tv.Seconds = new DateTimeOffset(date).ToUnixTimeSeconds(); + tv.Microseconds = 0; + return Interop.Unix.SetTimeOfDay(ref tv, IntPtr.Zero); + } - /// Daylight saving time. - internal int tm_isdst; + internal static int CreateSymLink(string filePath, string target) + { + // libpsl-native mapped CreateSymLink(link, target) to symlink(target, link). + return Interop.Unix.Symlink(target, filePath); } - // We need a way to convert a DateTime to a unix date. - internal static UnixTm DateTimeToUnixTm(DateTime date) + internal static int CreateHardLink(string filePath, string target) { - UnixTm tm; - tm.tm_sec = date.Second; - tm.tm_min = date.Minute; - tm.tm_hour = date.Hour; - tm.tm_mday = date.Day; - tm.tm_mon = date.Month - 1; // needs to be 0 indexed - tm.tm_year = date.Year - 1900; // years since 1900 - tm.tm_wday = 0; // this is ignored by mktime - tm.tm_yday = 0; // this is also ignored - tm.tm_isdst = date.IsDaylightSavingTime() ? 1 : 0; - return tm; + // libpsl-native mapped CreateHardLink(newlink, target) to link(target, newlink). + return Interop.Unix.Link(target, filePath); } +#else + // Windows builds exclude engine/Interop/Unix. These Unix-only helpers are never + // called on Windows but must be present so the platform-neutral wrappers compile. + internal static bool IsExecutable(string filePath) + => throw new PlatformNotSupportedException(); - [LibraryImport(psLib, SetLastError = true)] - internal static unsafe partial int SetDate(UnixTm* tm); + internal static uint GetCurrentThreadId() + => throw new PlatformNotSupportedException(); - [LibraryImport(psLib, StringMarshalling = StringMarshalling.Utf8)] - internal static partial int CreateSymLink(string filePath, string target); + internal static bool KillProcess(int pid) + => throw new PlatformNotSupportedException(); - [LibraryImport(psLib, StringMarshalling = StringMarshalling.Utf8)] - internal static partial int CreateHardLink(string filePath, string target); + internal static int WaitPid(int pid, bool nohang) + => throw new PlatformNotSupportedException(); + + internal static int SetDate(DateTime date) + => throw new PlatformNotSupportedException(); + + internal static int CreateSymLink(string filePath, string target) + => throw new PlatformNotSupportedException(); + + internal static int CreateHardLink(string filePath, string target) + => throw new PlatformNotSupportedException(); +#endif [LibraryImport(psLib)] [return: MarshalAs(UnmanagedType.LPStr)] diff --git a/src/System.Management.Automation/engine/Interop/Unix/FileSystem.cs b/src/System.Management.Automation/engine/Interop/Unix/FileSystem.cs new file mode 100644 index 00000000000..5e908042541 --- /dev/null +++ b/src/System.Management.Automation/engine/Interop/Unix/FileSystem.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Unix + { + /// The mode value passed to access to test for execute permission. + internal const int X_OK = 1; + + /// Create a symbolic link named pointing to . + /// The path the symbolic link points to. + /// The path of the symbolic link to create. + /// 0 on success, -1 on failure (see errno). + [LibraryImport("libc", EntryPoint = "symlink", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] + internal static partial int Symlink(string target, string linkPath); + + /// Create a hard link named for the existing file . + /// The path of the existing file. + /// The path of the hard link to create. + /// 0 on success, -1 on failure (see errno). + [LibraryImport("libc", EntryPoint = "link", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] + internal static partial int Link(string oldPath, string newPath); + + /// Check the calling process's permissions for the file . + /// The path to test. + /// The accessibility check(s) to perform (for example ). + /// 0 if all requested access is granted, -1 otherwise (see errno). + [LibraryImport("libc", EntryPoint = "access", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] + internal static partial int Access(string pathName, int mode); + } +} diff --git a/src/System.Management.Automation/engine/Interop/Unix/Process.cs b/src/System.Management.Automation/engine/Interop/Unix/Process.cs new file mode 100644 index 00000000000..f430f658b2a --- /dev/null +++ b/src/System.Management.Automation/engine/Interop/Unix/Process.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Unix + { + /// The SIGKILL signal number (same value on Linux and macOS). + internal const int SIGKILL = 9; + + /// The WNOHANG option for waitpid (same value on Linux and macOS). + internal const int WNOHANG = 1; + + /// Send signal to process . + /// The target process id. + /// The signal to send (for example ). + /// 0 on success, -1 on failure (see errno). + [LibraryImport("libc", EntryPoint = "kill", SetLastError = true)] + internal static partial int Kill(int pid, int sig); + + /// Wait for state changes in a child process. + /// The child process id to wait for. + /// Pointer to receive status information, or to ignore it. + /// Bitwise options such as . + /// The pid of the child whose state changed, 0 for with no change, or -1 on error. + [LibraryImport("libc", EntryPoint = "waitpid", SetLastError = true)] + internal static partial int WaitPid(int pid, IntPtr status, int options); + + /// Linux: return the caller's kernel thread id via gettid(2). + /// The current thread id. + [LibraryImport("libc", EntryPoint = "gettid")] + internal static partial int GetTid(); + + /// macOS: return the caller's integral thread id via pthread_threadid_np. + /// The pthread to query, or for the current thread. + /// Receives the integral thread id. + /// 0 on success. + [LibraryImport("libc", EntryPoint = "pthread_threadid_np")] + internal static partial int PthreadThreadIdNp(IntPtr thread, out ulong threadId); + } +} diff --git a/src/System.Management.Automation/engine/Interop/Unix/Time.cs b/src/System.Management.Automation/engine/Interop/Unix/Time.cs new file mode 100644 index 00000000000..96f3a752ffe --- /dev/null +++ b/src/System.Management.Automation/engine/Interop/Unix/Time.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Unix + { + /// + /// The struct timeval layout used by settimeofday. + /// On all 64-bit Linux and macOS platforms PowerShell supports, both members are 64-bit. + /// + [StructLayout(LayoutKind.Sequential)] + internal struct Timeval + { + /// Whole seconds since the Unix epoch. + internal long Seconds; + + /// Additional microseconds. + internal long Microseconds; + } + + /// Set the system-wide clock. + /// The new time expressed as seconds/microseconds since the Unix epoch. + /// Should be ; the timezone argument is obsolete. + /// 0 on success, -1 on failure (see errno); requires appropriate privileges. + [LibraryImport("libc", EntryPoint = "settimeofday", SetLastError = true)] + internal static partial int SetTimeOfDay(ref Timeval tv, IntPtr timezone); + } +} diff --git a/src/System.Management.Automation/utils/tracing/SysLogProvider.cs b/src/System.Management.Automation/utils/tracing/SysLogProvider.cs index f3ee184485e..60be4c028fb 100755 --- a/src/System.Management.Automation/utils/tracing/SysLogProvider.cs +++ b/src/System.Management.Automation/utils/tracing/SysLogProvider.cs @@ -369,7 +369,22 @@ internal enum LogLevel : uint internal static class NativeMethods { - private const string libpslnative = "libpsl-native"; + private const string libc = "libc"; + + // openlog options: log the pid with each message and open the connection immediately. + // These values are identical on Linux and macOS. + private const int LOG_PID = 0x01; + private const int LOG_NDELAY = 0x08; + + [DllImport(libc, CharSet = CharSet.Ansi, EntryPoint = "openlog")] + private static extern void OpenLogNative(IntPtr ident, int option, int facility); + + [DllImport(libc, CharSet = CharSet.Ansi, EntryPoint = "syslog")] + private static extern void SysLogNative(int priority, string format, string message); + + [DllImport(libc, EntryPoint = "closelog")] + internal static extern void CloseLog(); + /// /// Write a message to the system logger, which in turn writes the message to the system console, log files, etc. /// See man 3 syslog for more info. @@ -378,14 +393,25 @@ internal static class NativeMethods /// The OR of a priority and facility in the SysLogPriority enum indicating the priority and facility of the log entry. /// /// The message to put in the log entry. - [DllImport(libpslnative, CharSet = CharSet.Ansi, EntryPoint = "Native_SysLog")] - internal static extern void SysLog(SysLogPriority priority, string message); - - [DllImport(libpslnative, CharSet = CharSet.Ansi, EntryPoint = "Native_OpenLog")] - internal static extern void OpenLog(IntPtr ident, SysLogPriority facility); + internal static void SysLog(SysLogPriority priority, string message) + { + // Pass the message as a "%s" argument so any '%' characters in the message + // are not interpreted as format specifiers. + SysLogNative((int)priority, "%s", message); + } - [DllImport(libpslnative, EntryPoint = "Native_CloseLog")] - internal static extern void CloseLog(); + /// + /// Opens a connection to the system logger for the calling process. + /// + /// + /// A pointer to a null-terminated string that is prepended to every message. + /// The pointer must remain valid for the lifetime of the log session; syslog does not copy the string. + /// + /// The default facility for subsequent calls to syslog. + internal static void OpenLog(IntPtr ident, SysLogPriority facility) + { + OpenLogNative(ident, LOG_NDELAY | LOG_PID, (int)facility); + } [Flags] internal enum SysLogPriority : uint