Skip to content

Commit

Permalink
Implement ProcessStartInfo.InheritHandles
Browse files Browse the repository at this point in the history
  • Loading branch information
AustinWise committed Sep 16, 2024
1 parent 924fc2a commit dde9ffa
Show file tree
Hide file tree
Showing 14 changed files with 286 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal static unsafe partial bool CreateProcessWithLogonW(
int creationFlags,
IntPtr environmentBlock,
string? lpCurrentDirectory,
ref Interop.Kernel32.STARTUPINFO lpStartupInfo,
ref Interop.Kernel32.STARTUPINFOEX lpStartupInfo,
ref Interop.Kernel32.PROCESS_INFORMATION lpProcessInformation);

[Flags]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ internal static partial class StartupInfoOptions
internal const int STARTF_USESHOWWINDOW = 0x00000001;
internal const int STARTF_USESTDHANDLES = 0x00000100;
internal const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
internal const int EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
internal const int CREATE_NO_WINDOW = 0x08000000;
}

internal static partial class ProcThreadAttribute
{
internal const int HANDLE_LIST = 131074;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,21 @@ internal static unsafe partial bool CreateProcess(
int dwCreationFlags,
IntPtr lpEnvironment,
string? lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo,
ref STARTUPINFOEX lpStartupInfo,
ref PROCESS_INFORMATION lpProcessInformation
);

[LibraryImport(Libraries.Kernel32, EntryPoint = "InitializeProcThreadAttributeList", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static unsafe partial bool InitializeProcThreadAttributeList(byte* lpAttributeList, uint dwAttributeCount, uint dwFlags, nuint* lpSize);

[LibraryImport(Libraries.Kernel32, EntryPoint = "UpdateProcThreadAttribute", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static unsafe partial bool UpdateProcThreadAttribute(byte* lpAttributeList, uint dwFlags, nuint Attribute, Span<nint> lpValue, nuint cbSize, void* lpPreviousValue, nuint* lpReturnSize);

[LibraryImport(Libraries.Kernel32, EntryPoint = "DeleteProcThreadAttributeList", SetLastError = true)]
internal static unsafe partial void DeleteProcThreadAttributeList(byte* lpAttributeList);

[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
Expand Down Expand Up @@ -56,5 +67,12 @@ internal struct STARTUPINFO
internal IntPtr hStdOutput;
internal IntPtr hStdError;
}

[StructLayout(LayoutKind.Sequential)]
internal unsafe struct STARTUPINFOEX
{
internal STARTUPINFO StartupInfo;
internal byte* AttributeList;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable<
[System.ComponentModel.EditorAttribute("System.Diagnostics.Design.StartFileNameEditor, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", "System.Drawing.Design.UITypeEditor, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
public string FileName { get { throw null; } set { } }
public bool InheritHandles { get { throw null; } [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")] set { } }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public bool LoadUserProfile { get { throw null; } set { } }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@
<data name="CantUseEnvVars" xml:space="preserve">
<value>The Process object must have the UseShellExecute property set to false in order to use environment variables.</value>
</data>
<data name="CantDisableHandleInheritanceAndUseShellExecute" xml:space="preserve">
<value>The Process object must have the UseShellExecute property set to false in order to disable handle inheritance.</value>
</data>
<data name="CantDisableHandleInheritanceAndUseUserName" xml:space="preserve">
<value>The Process object must have the InheritHandles property set to true in order to start a process as a user.</value>
</data>
<data name="UseShellExecuteNotSupported" xml:space="preserve">
<value>UseShellExecute is not supported on this platform.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ private unsafe bool StartWithShellExecuteEx(ProcessStartInfo startInfo)
if (startInfo._environmentVariables != null)
throw new InvalidOperationException(SR.CantUseEnvVars);

if (!startInfo.InheritHandles)
throw new InvalidOperationException(SR.CantDisableHandleInheritanceAndUseShellExecute);

string arguments = startInfo.BuildArguments();

fixed (char* fileName = startInfo.FileName.Length > 0 ? startInfo.FileName : null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,10 +436,11 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo)
var commandLine = new ValueStringBuilder(stackalloc char[256]);
BuildCommandLine(startInfo, ref commandLine);

Interop.Kernel32.STARTUPINFO startupInfo = default;
Interop.Kernel32.STARTUPINFOEX startupInfo = default;
Interop.Kernel32.PROCESS_INFORMATION processInfo = default;
Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default;
SafeProcessHandle procSH = new SafeProcessHandle();
byte* lpAttributeList = null;

// handles used in parent process
SafeFileHandle? parentInputPipeHandle = null;
Expand All @@ -457,14 +458,18 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo)
{
try
{
startupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFO);
startupInfo.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFOEX);

int numberOfHandles = 0;
Span<nint> handlesToInherit = stackalloc nint[3];

// set up the streams
if (startInfo.RedirectStandardInput || startInfo.RedirectStandardOutput || startInfo.RedirectStandardError)
{
if (startInfo.RedirectStandardInput)
{
CreatePipe(out parentInputPipeHandle, out childInputPipeHandle, true);
handlesToInherit[numberOfHandles++] = childInputPipeHandle.DangerousGetHandle();
}
else
{
Expand All @@ -474,6 +479,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo)
if (startInfo.RedirectStandardOutput)
{
CreatePipe(out parentOutputPipeHandle, out childOutputPipeHandle, false);
handlesToInherit[numberOfHandles++] = childOutputPipeHandle.DangerousGetHandle();
}
else
{
Expand All @@ -483,23 +489,24 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo)
if (startInfo.RedirectStandardError)
{
CreatePipe(out parentErrorPipeHandle, out childErrorPipeHandle, false);
handlesToInherit[numberOfHandles++] = childErrorPipeHandle.DangerousGetHandle();
}
else
{
childErrorPipeHandle = new SafeFileHandle(Interop.Kernel32.GetStdHandle(Interop.Kernel32.HandleTypes.STD_ERROR_HANDLE), false);
}

startupInfo.hStdInput = childInputPipeHandle.DangerousGetHandle();
startupInfo.hStdOutput = childOutputPipeHandle.DangerousGetHandle();
startupInfo.hStdError = childErrorPipeHandle.DangerousGetHandle();
startupInfo.StartupInfo.hStdInput = childInputPipeHandle.DangerousGetHandle();
startupInfo.StartupInfo.hStdOutput = childOutputPipeHandle.DangerousGetHandle();
startupInfo.StartupInfo.hStdError = childErrorPipeHandle.DangerousGetHandle();

startupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES;
startupInfo.StartupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES;
}

if (startInfo.WindowStyle != ProcessWindowStyle.Normal)
{
startupInfo.wShowWindow = (short)GetShowWindowFromWindowStyle(startInfo.WindowStyle);
startupInfo.dwFlags |= Interop.Advapi32.StartupInfoOptions.STARTF_USESHOWWINDOW;
startupInfo.StartupInfo.wShowWindow = (short)GetShowWindowFromWindowStyle(startInfo.WindowStyle);
startupInfo.StartupInfo.dwFlags |= Interop.Advapi32.StartupInfoOptions.STARTF_USESHOWWINDOW;
}

// set up the creation flags parameter
Expand Down Expand Up @@ -529,6 +536,10 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo)
{
throw new ArgumentException(SR.CantSetDuplicatePassword);
}
if (!startInfo.InheritHandles)
{
throw new InvalidOperationException(SR.CantDisableHandleInheritanceAndUseUserName);
}

Interop.Advapi32.LogonFlags logonFlags = (Interop.Advapi32.LogonFlags)0;
if (startInfo.LoadUserProfile && startInfo.UseCredentialsForNetworkingOnly)
Expand Down Expand Up @@ -578,6 +589,37 @@ ref processInfo // pointer to PROCESS_INFORMATION
}
else
{
if (!startInfo.InheritHandles && numberOfHandles != 0)
{
nuint attributeListSize = 0;
bool shouldBeFalse = Interop.Kernel32.InitializeProcThreadAttributeList(null, 1, 0, &attributeListSize);
Debug.Assert(!shouldBeFalse);
Debug.Assert(attributeListSize > 0);
Debug.Assert(attributeListSize < 1024);

byte* newList = stackalloc byte[(int)attributeListSize];
if (!Interop.Kernel32.InitializeProcThreadAttributeList(newList, 1, 0, &attributeListSize))
{
throw new Win32Exception();
}
lpAttributeList = newList;

if (!Interop.Kernel32.UpdateProcThreadAttribute(
lpAttributeList,
0,
Interop.Advapi32.ProcThreadAttribute.HANDLE_LIST,
handlesToInherit,
(nuint)MemoryMarshal.AsBytes(handlesToInherit.Slice(0, numberOfHandles)).Length,
null,
null))
{
throw new Win32Exception();
}

startupInfo.AttributeList = lpAttributeList;
creationFlags |= Interop.Advapi32.StartupInfoOptions.EXTENDED_STARTUPINFO_PRESENT;
}

fixed (char* environmentBlockPtr = environmentBlock)
fixed (char* commandLinePtr = &commandLine.GetPinnableReference(terminate: true))
{
Expand All @@ -586,7 +628,7 @@ ref processInfo // pointer to PROCESS_INFORMATION
commandLinePtr, // pointer to the command line string
ref unused_SecAttrs, // address to process security attributes, we don't need to inherit the handle
ref unused_SecAttrs, // address to thread security attributes.
true, // handle inheritance flag
startInfo.InheritHandles || numberOfHandles != 0, // handle inheritance flag
creationFlags, // creation flags
(IntPtr)environmentBlockPtr, // pointer to new environment block
workingDirectory, // pointer to current directory name
Expand Down Expand Up @@ -622,6 +664,10 @@ ref processInfo // pointer to PROCESS_INFORMATION
}
finally
{
if (lpAttributeList != null)
{
Interop.Kernel32.DeleteProcThreadAttributeList(lpAttributeList);
}
childInputPipeHandle?.Dispose();
childOutputPipeHandle?.Dispose();
childErrorPipeHandle?.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,16 @@ public SecureString? Password
get { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(Password))); }
set { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(Password))); }
}

/// <summary>
/// If true, the child process inherits handles from this process that were opened as
/// inheritable.
/// </summary>
public bool InheritHandles
{
get { return true; }
[SupportedOSPlatform("windows")]
set { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(InheritHandles))); }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,16 @@ public string Domain
[CLSCompliant(false)]
[SupportedOSPlatform("windows")]
public SecureString? Password { get; set; }

/// <summary>
/// If true, the child process inherits handles from this process that were opened as
/// inheritable.
/// </summary>
public bool InheritHandles
{
get;
[SupportedOSPlatform("windows")]
set;
} = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,32 @@ public void Password_GetSetUnix_ThrowsPlatformNotSupportedException()
Assert.Throws<PlatformNotSupportedException>(() => info.Password = new SecureString());
}

[Fact]
public void InheritHandles_GetDefaultValue_ReturnsExpected()
{
var info = new ProcessStartInfo();
Assert.True(info.InheritHandles);
}

[Fact]
[PlatformSpecific(TestPlatforms.Windows)]
public void InheritHandles_Set_GetReturnsExpected()
{
var info = new ProcessStartInfo();
Assert.True(info.InheritHandles);
info.InheritHandles = false;
Assert.False(info.InheritHandles);
}

[Fact]
[PlatformSpecific(TestPlatforms.AnyUnix)]
public void InheritHandles_SetUnix_Throws()
{
var info = new ProcessStartInfo();
Assert.Throws<PlatformNotSupportedException>(() => info.InheritHandles = true);
Assert.Throws<PlatformNotSupportedException>(() => info.InheritHandles = false);
}

[Theory]
[InlineData(null)]
[InlineData("")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@ namespace System.Diagnostics.Tests
{
public class ProcessStreamReadTests : ProcessTestBase
{
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public void TestSyncErrorStream()
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[InlineData(true)]
[InlineData(false)]
public void TestSyncErrorStream(bool inheritHandles)
{
Process p = CreateProcessPortable(RemotelyInvokable.ErrorProcessBody);
p.StartInfo.RedirectStandardError = true;
if (OperatingSystem.IsWindows())
{
// Ensure redirecting works regardless of the InheritHandles setting.
p.StartInfo.InheritHandles = inheritHandles;
}
p.Start();
string expected = RemotelyInvokable.TestConsoleApp + " started error stream" + Environment.NewLine +
RemotelyInvokable.TestConsoleApp + " closed error stream" + Environment.NewLine;
Expand Down Expand Up @@ -86,11 +93,18 @@ public void TestAsyncErrorStream_SynchronizingObject(bool invokeRequired)
Assert.Equal(invokeRequired ? 3 : 0, invokeCalled);
}

[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public void TestSyncOutputStream()
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[InlineData(true)]
[InlineData(false)]
public void TestSyncOutputStream(bool inheritHandles)
{
Process p = CreateProcessPortable(RemotelyInvokable.StreamBody);
p.StartInfo.RedirectStandardOutput = true;
if (OperatingSystem.IsWindows())
{
// Ensure redirecting works regardless of the InheritHandles setting.
p.StartInfo.InheritHandles = inheritHandles;
}
p.Start();
string s = p.StandardOutput.ReadToEnd();
Assert.True(p.WaitForExit(WaitInMS));
Expand Down Expand Up @@ -380,13 +394,20 @@ public async Task ReadAsync_OutputStreams_Cancel_RespondsQuickly()
}
}

[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public void TestSyncStreams()
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[InlineData(true)]
[InlineData(false)]
public void TestSyncStreams(bool inheritHandles)
{
const string expected = "This string should come as output";
Process p = CreateProcessPortable(RemotelyInvokable.ReadLine);
p.StartInfo.RedirectStandardInput = true;
p.StartInfo.RedirectStandardOutput = true;
if (OperatingSystem.IsWindows())
{
// Ensure redirecting works regardless of the InheritHandles setting.
p.StartInfo.InheritHandles = inheritHandles;
}
p.OutputDataReceived += (s, e) => { Assert.Equal(expected, e.Data); };
p.Start();
using (StreamWriter writer = p.StandardInput)
Expand Down
Loading

0 comments on commit dde9ffa

Please sign in to comment.