KillOnParentExit rejects the Microsoft Store PowerShell execution alias with access denied

未关闭
#137,325 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
48/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
csharp

调研方向

Start with SafeProcessHandle.StartCore in src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs, the Windows startup path linked in the report. Reproduce the failure with the supplied program on Windows using the Store PowerShell alias, then compare it with the physical executable and the existing bounded replacement-job retry. Done when the alias starts with KillOnParentExit enabled, or the API reports and documents the unsupported case clearly.

由索引模型根据 Issue 内容生成。

描述

area-System.Diagnostics.Process untriaged

Description

ProcessStartInfo.KillOnParentExit=true causes Process.Start to reject the Microsoft Store PowerShell execution alias with native error 5 on .NET 11 GA. The same alias starts successfully when the option is false, including suspended creation. Applications that enable parent-exit cleanup therefore cannot launch this otherwise working executable.

Reproduction Steps

Install Microsoft Store PowerShell (tested package Microsoft.PowerShell 7.6.9.0 x64) with its execution alias enabled. Create a console project targeting net11.0 with implicit usings and nullable enabled, and replace Program.cs with:

using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

if (!OperatingSystem.IsWindows()) return;
Console.WriteLine($"Runtime {Environment.Version}; {RuntimeInformation.OSDescription}; {RuntimeInformation.ProcessArchitecture}");
var executable = args.Length > 0 ? Path.GetFullPath(args[0]) : Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
    "Microsoft", "WindowsApps", "Microsoft.PowerShell_8wekyb3d8bbwe", "pwsh.exe");
using var launcher = Process.GetCurrentProcess();
using var outer = JoinJob(launcher);
using var inner = JoinJob(launcher);
foreach (var suspended in new[] { false, true })
foreach (var lifetime in new[] { false, true, false })
{
    using var child = new Process { StartInfo = new ProcessStartInfo(executable)
    {
        UseShellExecute = false,
        RedirectStandardInput = true,
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        InheritedHandles = [],
        CreateNoWindow = true,
        CreateNewProcessGroup = true,
        KillOnParentExit = lifetime,
        StartSuspended = suspended,
        ArgumentList = { "--version" },
    }};
    try
    {
        child.Start();
        if (suspended) child.SafeHandle.Resume();
        child.StandardInput.Close();
        var stdout = child.StandardOutput.ReadToEndAsync();
        var stderr = child.StandardError.ReadToEndAsync();
        await Task.WhenAll(child.WaitForExitAsync(), stdout, stderr).WaitAsync(TimeSpan.FromSeconds(10));
        Console.WriteLine($"KillOnParentExit={lifetime}; StartSuspended={suspended}; Exit={child.ExitCode}; Output={(await stdout).Trim()}; Error={(await stderr).Trim()}");
    }
    catch (Win32Exception exception)
    {
        Console.WriteLine($"KillOnParentExit={lifetime}; StartSuspended={suspended}; NativeErrorCode={exception.NativeErrorCode}; {exception.Message}");
    }
    finally
    {
        try
        {
            if (!child.HasExited) child.Kill();
            await child.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(5));
        }
        catch (InvalidOperationException) { } // Start failed; there is no owned child.
    }
}

static SafeFileHandle JoinJob(Process launcher)
{
    // Plain, non-killing jobs; no resource limits, UI limits or breakaway changes.
    var job = Native.CreateJobObjectW(0, null);
    if (job.IsInvalid || !Native.AssignProcessToJobObject(job, launcher.SafeHandle))
    {
        var error = Marshal.GetLastPInvokeError();
        job.Dispose();
        throw new Win32Exception(error);
    }
    return job;
}

static class Native
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
    internal static extern SafeFileHandle CreateJobObjectW(nint attributes, string? name);
    [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool AssignProcessToJobObject(SafeFileHandle job, SafeProcessHandle process);
}

Run dotnet run with SDK 11.0.100. The default executable is the package-specific alias beneath the current user's local application data. An explicit executable path can instead be passed after --.

The program joins two ordinary, non-killing jobs before launching children. It neither changes pre-existing jobs nor requests breakaway. Every successfully created child is waited and cleaned up through its own process handle.

Expected behavior

The alias should start with parent-exit cleanup enabled, or the API should explicitly document and identify this unsupported executable type.

Actual behavior

Both StartSuspended=false and StartSuspended=true produce this false/true/false sequence:

KillOnParentExit=False: Exit=0; Output=PowerShell 7.6.9
KillOnParentExit=True: Win32Exception; NativeErrorCode=5; Access is denied.
KillOnParentExit=False: Exit=0; Output=PowerShell 7.6.9

The exception comes from SafeProcessHandle.StartCore during Process.Start. The runtime's bounded replacement-job retry does not make the alias start.

With the same redirected streams, restricted inheritance, process group, and nested jobs, the physical C:\Program Files\PowerShell\7\pwsh.exe (also PowerShell 7.6.9) succeeds for every combination. dotnet.exe --version and cmd.exe /d /c echo probe-ok also succeed with and without the option. This report claims the tested alias failure; it does not infer a general Windows job limitation.

Regression?

Not established. This is distinct from #134990: ordinary executable launches after containment changes now work in the GA runtime.

Known Workarounds

Launching the physical PowerShell executable succeeds. Disabling KillOnParentExit lets the alias start but removes the requested lifetime guarantee.

Configuration

  • SDK: 11.0.100.
  • Runtime: 11.0.0, commit 297acf930f.
  • OS: Microsoft Windows 10.0.26300, x64.
  • Microsoft Store package: Microsoft.PowerShell 7.6.9.0, x64.
  • PowerShell output: PowerShell 7.6.9.

Other information

Checked open and closed runtime issues for KillOnParentExit with alias, package, WindowsApps, and PowerShell terms. The Windows startup implementation on v11.0.0 and main at 719ca2546bd31a54244731185c55c8fc208204bf still supplies the lifetime job through the extended startup attributes. No alias-specific fix was found in main or the .NET 11 release notes.

Discovered while investigating ModularPipelines #6302. Please support this alias launch with KillOnParentExit, or document its restriction with an actionable error.

主要语言
C#
星标
18.4k
派生
5.8k
平均合并
2 天 19 小时
30 天内合并 PR
590

环境准备

在 Codespaces 中打开

在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

dotnet/runtime 的其他 Issue

查看 dotnet/runtime 的全部 Issue

相似的 Issue

更多 C# Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。