Skip to content

Conversation

@SteveL-MSFT
Copy link
Member

@SteveL-MSFT SteveL-MSFT commented Feb 1, 2023

PR Summary

Per the docs, job objects can only contain processes within the same session, so if a credential is used, we simply WaitOnExit() instead of trying to use a job object.

Validated manually since you have to wait with a different credential.

PR Context

Fix #17033

PR Checklist

@SteveL-MSFT
Copy link
Member Author

@PowerShell/wg-powershell-cmdlets discussed the issue and agreed that in the credential case, it's ok to not support the job object case with child processes

@iSazonov
Copy link
Collaborator

iSazonov commented Feb 2, 2023

Docs say:

All processes associated with a job must run in the same session.

So I believe it is safe to remove try-catch and immediately call process.WaitForExit() if Credential parameter present.

@SteveL-MSFT
Copy link
Member Author

@iSazonov yes, that makes sense. I'll update the code.

Copy link
Collaborator

@iSazonov iSazonov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with one minor comment.

@iSazonov iSazonov added the CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log label Feb 2, 2023
@pull-request-quantifier-deprecated

This PR has 19 quantified lines of changes. In general, a change size of upto 200 lines is ideal for the best PR experience!


Quantification details

Label      : Extra Small
Size       : +11 -8
Percentile : 7.6%

Total files changed: 1

Change summary by file extension:
.cs : +11 -8

Change counts above are quantified counts, based on the PullRequestQuantifier customizations.

Why proper sizing of changes matters

Optimal pull request sizes drive a better predictable PR flow as they strike a
balance between between PR complexity and PR review overhead. PRs within the
optimal size (typical small, or medium sized PRs) mean:

  • Fast and predictable releases to production:
    • Optimal size changes are more likely to be reviewed faster with fewer
      iterations.
    • Similarity in low PR complexity drives similar review times.
  • Review quality is likely higher as complexity is lower:
    • Bugs are more likely to be detected.
    • Code inconsistencies are more likely to be detected.
  • Knowledge sharing is improved within the participants:
    • Small portions can be assimilated better.
  • Better engineering practices are exercised:
    • Solving big problems by dividing them in well contained, smaller problems.
    • Exercising separation of concerns within the code changes.

What can I do to optimize my changes

  • Use the PullRequestQuantifier to quantify your PR accurately
    • Create a context profile for your repo using the context generator
    • Exclude files that are not necessary to be reviewed or do not increase the review complexity. Example: Autogenerated code, docs, project IDE setting files, binaries, etc. Check out the Excluded section from your prquantifier.yaml context profile.
    • Understand your typical change complexity, drive towards the desired complexity by adjusting the label mapping in your prquantifier.yaml context profile.
    • Only use the labels that matter to you, see context specification to customize your prquantifier.yaml context profile.
  • Change your engineering behaviors
    • For PRs that fall outside of the desired spectrum, review the details and check if:
      • Your PR could be split in smaller, self-contained PRs instead
      • Your PR only solves one particular issue. (For example, don't refactor and code new features in the same PR).

How to interpret the change counts in git diff output

  • One line was added: +1 -0
  • One line was deleted: +0 -1
  • One line was modified: +1 -1 (git diff doesn't know about modified, it will
    interpret that line like one addition plus one deletion)
  • Change percentiles: Change characteristics (addition, deletion, modification)
    of this PR in relation to all other PRs within the repository.


Was this comment helpful? 👍  :ok_hand:  :thumbsdown: (Email)
Customize PullRequestQuantifier for this repository.

// Wait for the job object to finish
jobObject.WaitOne(_waithandle);
// If we are running as a different user, we cannot use a job object, so just wait on the process
process.WaitForExit();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason not to always use process.WaitForExit()? Also if the user provides explicit credentials for the current user account, process.WaitForExit() will be used instead of the job object. Is this Ok?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-Wait will wait not just for the new process but any processes that the new process spawns. By waiting on the job object it will wait until all of them have finished rather than just the current one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the intent of the job object is to also wait on any child processes, but processes in for a different user can't be added to the job object so in that case we can only WaitForExit() of the initial process. WG had discussed this and was ok with this difference.

Copy link
Collaborator

@jborean93 jborean93 Feb 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but processes in for a different user can't be added to the job object

This is not true, did you read my last comment - #19082 (comment)?

@jborean93
Copy link
Collaborator

jborean93 commented Feb 2, 2023

Per the docs, job objects can only contain processes within the same session,

That may be true but specifying a credential does not result in the process being run in a new session, it's still the same one. A new session happens when you do something like log into RDP as another user, or run something from a service which is in session 0. Ultimately this isn't the problem here.

The problem that you get today with Start-Process -Credential -Wait is due to PowerShell not using the hProcess object returned by CreateProcessWithLogon on the AssignProcessToJobObject stage. What is happening is:

  • PowerShell sees a -Credential was specified
  • It calls CreateProcessWithLogonW
    • This returns a process and thread HANDLE which the caller has an ALL_ACCESS mask on
    • Even though normally it doesn't have permissions to open the process handle, this one is special but it's not being used
    • There's actually a handle leak here as the code currently doesn't even close the process and thread handle
  • It uses the process id returned from the above to call Process.GetProcessById <-- this is the problem
  • It uses that process object and it's internal Handle when calling AssignProcessToJobObject
    • Because dotnet is trying to open a new process handle rather than use the ALL_ACCESS handle returned by CreateProcessWithLogonW this will fail for users who don't have permission to open the process (non-admins)

Now this actually works today when you are running as admin, it will wait for the process, as well as any child processes the new process spawns as expected. Where this fails is if you try and run it as a non-administrator as while the user was able to spawn the object, process permissions on the new one will not allow the caller to get the Handle needed for AssignProcessToJobObject. I've mentioned this problem and possible solutions for it in #17033 (comment).

The problem with this fix is that while it might work in some scenarios it is technically a breaking change as -Wait will no longer wait for any sub processes the new one spawns.

Just to show that this works even for non-admins if done correctly you can run this code.

# Uses Ctypes - https://www.powershellgallery.com/packages/Ctypes/0.1.0

ctypes_struct STARTUPINFOW -CharSet Unicode {
    [int]$CB
    [MarshalAs('LPWStr')][string]$Reserved
    [MarshalAs('LPWStr')][string]$Desktop
    [MarshalAs('LPWStr')][string]$Title
    [int]$X
    [int]$Y
    [int]$XSize
    [int]$YSize
    [int]$XCountChars
    [int]$YCountChars
    [int]$FillAttribute
    [int]$Flags
    [short]$ShowWindow
    [short]$Reserved2
    [IntPtr]$Reserved3
    [IntPtr]$StdInput
    [IntPtr]$StdOutput
    [IntPtr]$StdError
}

ctypes_struct PROCESS_INFORMATION -LayoutKind Sequential {
    [IntPtr]$Process
    [IntPtr]$Thread
    [int]$Pid
    [int]$Tid
}

[Flags()] enum ProcessCreationFlags {
    NONE = 0x00000000
    CREATE_SUSPENDED = 0x00000004
    CREATE_NEW_CONSOLE = 0x00000010
    CREATE_UNICODE_ENVIRONMENT = 0x00000400
}

$si = [STARTUPINFOW]@{
    CB = [System.Runtime.InteropServices.Marshal]::SizeOf([Type][STARTUPINFOW])
}
$pi = [PROCESS_INFORMATION]::new()

$advapi32 = New-CtypesLib Advapi32.dll
$kernel32 = New-CtypesLib Kernel32.dll

$commandLine = [System.Text.StringBuilder]::new("C:\Windows\System32\cmd.exe")
$res = $advapi32.SetLastError().CharSet('Unicode').CreateProcessWithLogonW[bool](
    "vagrant",
    $null,
    "vagrant",
    0,
    $commandLine.ToString(),
    $commandLine,
    [int][ProcessCreationFlags]'CREATE_SUSPENDED, CREATE_NEW_CONSOLE, CREATE_UNICODE_ENVIRONMENT',
    $null,
    "C:\Windows",
    [ref]$si,
    [ref]$pi
)
if (-not $res) {
    throw [System.ComponentModel.Win32Exception]$advapi32.LastError
}

$job = $kernel32.SetLastError().CreateJobObjectW[IntPtr]($null, "MyJob")
if ($job -eq [IntPtr]::Zero) {
    throw [System.ComponentModel.Win32Exception]$kernel32.LastError
}

$res = $kernel32.SetLastError().AssignProcessToJobObject[bool]($job, $pi.Process)
if (-not $res) {
    throw [System.ComponentModel.Win32Exception]$kernel32.LastError
}

$res = $kernel32.SetLastError().ResumeThread($pi.Thread)
if ($res -eq -1) {
    throw [System.ComponentModel.Win32Exception]$kernel32.LastError
}

$kernel32.CloseHandle[void]($pi.Process)
$kernel32.CloseHandle[void]($pi.Thread)
$kernel32.CloseHandle[void]($job)

I even do this in my ProcessEx module with no problems.

@PaulHigin
Copy link
Contributor

@jborean93 Can you create a new issue about this?

@PaulHigin PaulHigin merged commit 7fe5cb3 into PowerShell:master Feb 3, 2023
@jborean93
Copy link
Collaborator

jborean93 commented Feb 3, 2023

@PaulHigin there is one already #17033. Why would we merge a change where it changes the existing behaiour? It should be fixed properly in the first place.

I'm a bit disappointed this was merged before someone actually read and addressed my last comment. Especially since PowerShell has traditionally tried to shy away from behaviour changes. It's especially concerning that a change like this was merged without:

  • Any tests to assert this actual behaviour or that it was fixed
  • Any documentation/changelog details for people to see a breaking change actually occurred

The last point also ties into issues I had with the 7.3 release where there were a few breaking/behaviour changes but no real way for people to understand what that might be until they upgraded and things failed.

@SteveL-MSFT
Copy link
Member Author

@jborean93 would you be willing to submit a PR to fix this to work with a job object?

@jborean93
Copy link
Collaborator

Opened #19096, just need to figure out a good way to test it out.

@doctordns
Copy link
Collaborator

This issue has been resolved as implemented (via #19082)

@doctordns doctordns added the Resolution-Fixed The issue is fixed. label Mar 14, 2023
@iSazonov
Copy link
Collaborator

@doctordns What do you mean?

@doctordns
Copy link
Collaborator

doctordns commented Mar 14, 2023

This issue was raised to the cmdlet working group, and we felt the issue was concluded via #19082. @SteveL-MSFT agreed, so I labeled this issue as resolved-fixed. Is there a problem?

@iSazonov
Copy link
Collaborator

You write in 19082 - it is not issue, it is PR.

@ghost
Copy link

ghost commented Mar 14, 2023

🎉v7.4.0-preview.2 has been released which incorporates this pull request.:tada:

Handy links:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log Extra Small Resolution-Fixed The issue is fixed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Start-Process -Wait doesn't work when running as a different user (-Credential)

5 participants