diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index 70346a1d1ce..f4f66a1d1fa 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Diagnostics; using System.IO; using System.Management.Automation; using System.Reflection; @@ -81,7 +82,7 @@ public static int Main(string[] args) /// In the event of success, we use an exec() call, so this method never returns. /// /// The startup arguments to pwsh. - private static void AttemptExecPwshLogin(string[] args) + private static unsafe void AttemptExecPwshLogin(string[] args) { // If the login environment variable is set, we have already done the login logic and have been exec'd if (Environment.GetEnvironmentVariable(LOGIN_ENV_VAR_NAME) != null) @@ -90,8 +91,6 @@ private static void AttemptExecPwshLogin(string[] args) return; } - bool isLinux = Platform.IsLinux; - // The first byte (ASCII char) of the name of this process, used to detect '-' for login byte procNameFirstByte; @@ -99,7 +98,7 @@ private static void AttemptExecPwshLogin(string[] args) string? pwshPath; // On Linux, we can simply use the /proc filesystem - if (isLinux) + if (Platform.IsLinux) { // Read the process name byte using (FileStream fs = File.OpenRead("/proc/self/cmdline")) @@ -114,36 +113,25 @@ private static void AttemptExecPwshLogin(string[] args) } // Read the symlink to the startup executable - IntPtr linkPathPtr = Marshal.AllocHGlobal(LINUX_PATH_MAX); - IntPtr bufSize = ReadLink("/proc/self/exe", linkPathPtr, (UIntPtr)LINUX_PATH_MAX); - pwshPath = Marshal.PtrToStringAnsi(linkPathPtr, (int)bufSize); - Marshal.FreeHGlobal(linkPathPtr); - - ArgumentNullException.ThrowIfNull(pwshPath); + byte* linkPathPtr = (byte*)NativeMemory.Alloc(LINUX_PATH_MAX); + nint len = ReadLink("/proc/self/exe", linkPathPtr, LINUX_PATH_MAX); + pwshPath = new string((sbyte*)linkPathPtr, 0, checked((int)len)); + NativeMemory.Free(linkPathPtr); // exec pwsh ThrowOnFailure("exec", ExecPwshLogin(args, pwshPath, isMacOS: false)); return; } - // At this point, we are on macOS + Debug.Assert(Platform.IsMacOS); // Set up the mib array and the query for process maximum args size - Span mib = stackalloc int[3]; - int mibLength = 2; - mib[0] = MACOS_CTL_KERN; - mib[1] = MACOS_KERN_ARGMAX; - int size = IntPtr.Size / 2; - int argmax = 0; + Span mib = [MACOS_CTL_KERN, MACOS_KERN_ARGMAX]; + nuint argmax = 0; + nuint size = (uint)sizeof(nuint); // Get the process args size - unsafe - { - fixed (int *mibptr = mib) - { - ThrowOnFailure(nameof(argmax), SysCtl(mibptr, mibLength, &argmax, &size, IntPtr.Zero, 0)); - } - } + ThrowOnFailure(nameof(argmax), SysCtl(mib, &argmax, &size, null, 0)); // Get the PID so we can query this process' args int pid = GetPid(); @@ -151,50 +139,41 @@ private static void AttemptExecPwshLogin(string[] args) // The following logic is based on https://gist.github.com/nonowarn/770696 // Now read the process args into the allocated space - IntPtr procargs = Marshal.AllocHGlobal(argmax); - IntPtr executablePathPtr = IntPtr.Zero; + byte* procargs = (byte*)NativeMemory.Alloc(argmax); + byte* executablePathPtr = null; try { - mib[0] = MACOS_CTL_KERN; - mib[1] = MACOS_KERN_PROCARGS2; - mib[2] = pid; - mibLength = 3; - - unsafe - { - fixed (int *mibptr = mib) - { - ThrowOnFailure(nameof(procargs), SysCtl(mibptr, mibLength, procargs.ToPointer(), &argmax, IntPtr.Zero, 0)); - } - - // The memory block we're reading is a series of null-terminated strings - // that looks something like this: - // - // | argc | - // | exec_path | ... \0 - // | argv[0] | ... \0 - // | argv[1] | ... \0 - // ... - // - // We care about argv[0], since that's the name the process was started with. - // If argv[0][0] == '-', we have been invoked as login. - // Doing this, the buffer we populated also recorded `exec_path`, - // which is the path to our executable `pwsh`. - // We can reuse this value later to prevent needing to call a .NET API - // to generate our exec invocation. - - // We don't care about argc's value, since argv[0] must always exist. - // Skip over argc, but remember where exec_path is for later - executablePathPtr = IntPtr.Add(procargs, sizeof(int)); - - // Skip over exec_path - byte *argvPtr = (byte *)executablePathPtr; - while (*argvPtr != 0) { argvPtr++; } - while (*argvPtr == 0) { argvPtr++; } - - // First char in argv[0] - procNameFirstByte = *argvPtr; - } + mib = [MACOS_CTL_KERN, MACOS_KERN_PROCARGS2, pid]; + + ThrowOnFailure(nameof(procargs), SysCtl(mib, procargs, &argmax, null, 0)); + + // The memory block we're reading is a series of null-terminated strings + // that looks something like this: + // + // | argc | + // | exec_path | ... \0 + // | argv[0] | ... \0 + // | argv[1] | ... \0 + // ... + // + // We care about argv[0], since that's the name the process was started with. + // If argv[0][0] == '-', we have been invoked as login. + // Doing this, the buffer we populated also recorded `exec_path`, + // which is the path to our executable `pwsh`. + // We can reuse this value later to prevent needing to call a .NET API + // to generate our exec invocation. + + // We don't care about argc's value, since argv[0] must always exist. + // Skip over argc, but remember where exec_path is for later + executablePathPtr = procargs + sizeof(int); + + // Skip over exec_path + byte* argvPtr = executablePathPtr; + while (*argvPtr != 0) { argvPtr++; } + while (*argvPtr == 0) { argvPtr++; } + + // First char in argv[0] + procNameFirstByte = *argvPtr; if (!IsLogin(procNameFirstByte, args)) { @@ -202,16 +181,14 @@ private static void AttemptExecPwshLogin(string[] args) } // Get the pwshPath from exec_path - pwshPath = Marshal.PtrToStringAnsi(executablePathPtr); - - ArgumentNullException.ThrowIfNull(pwshPath); + pwshPath = new string((sbyte*)executablePathPtr); // exec pwsh ThrowOnFailure("exec", ExecPwshLogin(args, pwshPath, isMacOS: true)); } finally { - Marshal.FreeHGlobal(procargs); + NativeMemory.Free(procargs); } } @@ -451,14 +428,14 @@ private static void ThrowOnFailure(string call, int code) /// /// The path to the symlink to read. /// Pointer to a buffer to fill with the result. - /// The size of the buffer we have supplied. + /// The size of the buffer we have supplied. /// The number of bytes placed in the buffer. [DllImport("libc", EntryPoint = "readlink", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, SetLastError = true)] - private static extern IntPtr ReadLink(string pathname, IntPtr buf, UIntPtr size); + private static extern unsafe nint ReadLink(string pathname, byte* buf, nuint bufsiz); /// /// The `getpid` POSIX syscall we use to quickly get the current process PID on macOS. @@ -488,19 +465,27 @@ private static void ThrowOnFailure(string call, int code) /// /// The `sysctl` BSD sycall used to get system information on macOS. /// - /// The Management Information Base name, used to query information. - /// The length of the MIB name. + /// The Management Information Base name, used to query information. + /// The length of the MIB name. /// The object passed out of sysctl (may be null) /// The size of the object passed out of sysctl. /// The object passed in to sysctl. - /// The length of the object passed in to sysctl. + /// The length of the object passed in to sysctl. /// [DllImport("libc", EntryPoint = "sysctl", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, SetLastError = true)] - private static extern unsafe int SysCtl(int *mib, int mibLength, void *oldp, int *oldlenp, IntPtr newp, int newlenp); + private static extern unsafe int SysCtl(int* name, uint namelen, void* oldp, nuint* oldlenp, void* newp, nuint newlen); + + private static unsafe int SysCtl(Span mib, void* oldp, nuint* oldlenp, void* newp, nuint newlen) + { + fixed (int* name = &MemoryMarshal.GetReference(mib)) + { + return SysCtl(name, (uint)mib.Length, oldp, oldlenp, newp, newlen); + } + } #endif } }