diff --git a/changelogs/fragments/win_become_async_older_hosts.yml b/changelogs/fragments/win_become_async_older_hosts.yml new file mode 100644 index 0000000000..e475e1e9fd --- /dev/null +++ b/changelogs/fragments/win_become_async_older_hosts.yml @@ -0,0 +1,3 @@ +bugfixes: +- winrm - running async with become on a Server 2008 or 2008 R2 host will now work +- become runas - changed runas process so it does not create a temporary file on the disk during execution diff --git a/docs/docsite/rst/user_guide/become.rst b/docs/docsite/rst/user_guide/become.rst index ae6d54a937..a7b0a36fd6 100644 --- a/docs/docsite/rst/user_guide/become.rst +++ b/docs/docsite/rst/user_guide/become.rst @@ -563,7 +563,7 @@ Limitations Be aware of the following limitations with ``become`` on Windows: * Running a task with ``async`` and ``become`` on Windows Server 2008, 2008 R2 - and Windows 7 does not work. + and Windows 7 only works when using Ansible 2.7 or newer. * By default, the become user logs on with an interactive session, so it must have the right to do so on the Windows host. If it does not inherit the diff --git a/lib/ansible/plugins/shell/powershell.py b/lib/ansible/plugins/shell/powershell.py index af07ae1c59..77b588d8eb 100644 --- a/lib/ansible/plugins/shell/powershell.py +++ b/lib/ansible/plugins/shell/powershell.py @@ -279,30 +279,6 @@ namespace Ansible public SID_AND_ATTRIBUTES User; } - [StructLayout(LayoutKind.Sequential)] - public struct JOBOBJECT_BASIC_LIMIT_INFORMATION - { - public UInt64 PerProcessUserTimeLimit; - public UInt64 PerJobUserTimeLimit; - public LimitFlags LimitFlags; - public UIntPtr MinimumWorkingSetSize; - public UIntPtr MaximumWorkingSetSize; - public UInt32 ActiveProcessLimit; - public UIntPtr Affinity; - public UInt32 PriorityClass; - public UInt32 SchedulingClass; - } - - [StructLayout(LayoutKind.Sequential)] - public class JOBOBJECT_EXTENDED_LIMIT_INFORMATION - { - public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION(); - [MarshalAs(UnmanagedType.ByValArray, SizeConst=48)] - public byte[] IO_COUNTERS_BLOB; - [MarshalAs(UnmanagedType.ByValArray, SizeConst=4)] - public UIntPtr[] LIMIT_BLOB; - } - [Flags] public enum StartupInfoFlags : uint { @@ -382,24 +358,6 @@ namespace Ansible TokenImpersonation } - enum JobObjectInfoType - { - ExtendedLimitInformation = 9, - } - - [Flags] - enum ThreadAccessRights : uint - { - SUSPEND_RESUME = 0x0002 - } - - [Flags] - public enum LimitFlags : uint - { - JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800, - JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000 - } - class NativeWaitHandle : WaitHandle { public NativeWaitHandle(IntPtr handle) @@ -427,63 +385,6 @@ namespace Ansible public uint ExitCode { get; internal set; } } - public class Job : IDisposable - { - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern IntPtr CreateJobObject( - IntPtr lpJobAttributes, - string lpName); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool SetInformationJobObject( - IntPtr hJob, - JobObjectInfoType JobObjectInfoClass, - JOBOBJECT_EXTENDED_LIMIT_INFORMATION lpJobObjectInfo, - int cbJobObjectInfoLength); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool AssignProcessToJobObject( - IntPtr hJob, - IntPtr hProcess); - - [DllImport("kernel32.dll")] - private static extern bool CloseHandle( - IntPtr hObject); - - private IntPtr handle; - - public Job() - { - handle = CreateJobObject(IntPtr.Zero, null); - if (handle == IntPtr.Zero) - throw new Win32Exception("CreateJobObject() failed"); - - JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedJobInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION(); - // on OSs that support nested jobs, one of the jobs must allow breakaway for async to work properly under WinRM - extendedJobInfo.BasicLimitInformation.LimitFlags = LimitFlags.JOB_OBJECT_LIMIT_BREAKAWAY_OK | LimitFlags.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; - - if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedJobInfo, Marshal.SizeOf(extendedJobInfo))) - throw new Win32Exception("SetInformationJobObject() failed"); - } - - public void AssignProcess(IntPtr processHandle) - { - if (!AssignProcessToJobObject(handle, processHandle)) - throw new Win32Exception("AssignProcessToJobObject() failed"); - } - - public void Dispose() - { - if (handle != IntPtr.Zero) - { - CloseHandle(handle); - handle = IntPtr.Zero; - } - - GC.SuppressFinalize(this); - } - } - public class BecomeUtil { [DllImport("advapi32.dll", SetLastError = true)] @@ -591,16 +492,6 @@ namespace Ansible [DllImport("advapi32.dll", SetLastError = true)] private static extern bool RevertToSelf(); - [DllImport("kernel32.dll", SetLastError = true)] - private static extern SafeFileHandle OpenThread( - ThreadAccessRights dwDesiredAccess, - bool bInheritHandle, - int dwThreadId); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern int ResumeThread( - SafeHandle hThread); - public static CommandResult RunAsUser(string username, string password, string lpCommandLine, string lpCurrentDirectory, string stdinInput, LogonFlags logonFlags, LogonType logonType) { @@ -645,8 +536,7 @@ namespace Ansible // Create the environment block if set IntPtr lpEnvironment = IntPtr.Zero; - // To support async + become, we have to do some job magic later, which requires both breakaway and starting suspended - CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT | CreationFlags.CREATE_BREAKAWAY_FROM_JOB | CreationFlags.CREATE_SUSPENDED; + CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT; PROCESS_INFORMATION pi = new PROCESS_INFORMATION(); @@ -675,44 +565,26 @@ namespace Ansible if (!launch_success) throw new Win32Exception("Failed to start become process"); - // If 2012/8+ OS, create new job with JOB_OBJECT_LIMIT_BREAKAWAY_OK - // so that async can work - Job job = null; - if (Environment.OSVersion.Version >= new Version("6.2")) - { - job = new Job(); - job.AssignProcess(pi.hProcess); - } - ResumeProcessById(pi.dwProcessId); - CommandResult result = new CommandResult(); - try - { - // Setup the output buffers and get stdout/stderr - FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096); - StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096); - stdout_write.Close(); + // Setup the output buffers and get stdout/stderr + FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096); + StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096); + stdout_write.Close(); - FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096); - StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096); - stderr_write.Close(); + FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096); + StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096); + stderr_write.Close(); - stdin.WriteLine(stdinInput); - stdin.Close(); + stdin.WriteLine(stdinInput); + stdin.Close(); - string stdout_str, stderr_str = null; - GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str); - UInt32 rc = GetProcessExitCode(pi.hProcess); + string stdout_str, stderr_str = null; + GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str); + UInt32 rc = GetProcessExitCode(pi.hProcess); - result.StandardOut = stdout_str; - result.StandardError = stderr_str; - result.ExitCode = rc; - } - finally - { - if (job != null) - job.Dispose(); - } + result.StandardOut = stdout_str; + result.StandardError = stderr_str; + result.ExitCode = rc; return result; } @@ -1013,44 +885,6 @@ namespace Ansible security.Persist(safeHandle, AccessControlSections.Access); } - private static void ResumeThreadById(int threadId) - { - var threadHandle = OpenThread(ThreadAccessRights.SUSPEND_RESUME, false, threadId); - if (threadHandle.IsInvalid) - throw new Win32Exception(String.Format("Thread ID {0} is invalid", threadId)); - - try - { - if (ResumeThread(threadHandle) == -1) - throw new Win32Exception(String.Format("Thread ID {0} cannot be resumed", threadId)); - } - finally - { - threadHandle.Dispose(); - } - } - - private static void ResumeProcessById(int pid) - { - var proc = Process.GetProcessById(pid); - - // wait for at least one suspended thread in the process (this handles possible slow startup race where - // primary thread of created-suspended process has not yet become runnable) - var retryCount = 0; - while (!proc.Threads.OfType().Any(t => t.ThreadState == System.Diagnostics.ThreadState.Wait && - t.WaitReason == ThreadWaitReason.Suspended)) - { - proc.Refresh(); - Thread.Sleep(50); - if (retryCount > 100) - throw new InvalidOperationException(String.Format("No threads were suspended in target PID {0} after 5s", pid)); - } - - foreach (var thread in proc.Threads.OfType().Where(t => t.ThreadState == System.Diagnostics.ThreadState.Wait && - t.WaitReason == ThreadWaitReason.Suspended)) - ResumeThreadById(thread.Id); - } - private class GenericSecurity : NativeObjectSecurity { public GenericSecurity(bool isContainer, ResourceType resType, SafeHandle objectHandle, AccessControlSections sectionsRequested) @@ -1085,6 +919,17 @@ namespace Ansible } "@ +# due to the command line size limitations of CreateProcessWithTokenW, we +# execute a simple PS script that executes our full exec_wrapper so no files +# touch the disk +$become_exec_wrapper = { + chcp.com 65001 > $null + $ProgressPreference = "SilentlyContinue" + $exec_wrapper_str = [System.Console]::In.ReadToEnd() + $exec_wrapper = [ScriptBlock]::Create($exec_wrapper_str) + &$exec_wrapper +} + $exec_wrapper = { Set-StrictMode -Version 2 $DebugPreference = "Continue" @@ -1102,61 +947,10 @@ $exec_wrapper = { return $output } - Function Invoke-Win32Api { - # Inspired by - Call a Win32 API in PowerShell without compiling C# code on - # the disk - # http://www.leeholmes.com/blog/2007/10/02/managing-ini-files-with-powershell/ - # https://blogs.technet.microsoft.com/heyscriptingguy/2013/06/27/use-powershell-to-interact-with-the-windows-api-part-3/ - [CmdletBinding()] - param( - [Parameter(Mandatory=$true)] [String]$DllName, - [Parameter(Mandatory=$true)] [String]$MethodName, - [Parameter(Mandatory=$true)] [Type]$ReturnType, - [Parameter()] [Type[]]$ParameterTypes = [Type[]]@(), - [Parameter()] [Object[]]$Parameters = [Object[]]@() - ) - - $assembly = New-Object -TypeName System.Reflection.AssemblyName -ArgumentList "Win32ApiAssembly" - $dynamic_assembly = [AppDomain]::CurrentDomain.DefineDynamicAssembly($assembly, [Reflection.Emit.AssemblyBuilderAccess]::Run) - $dynamic_module = $dynamic_assembly.DefineDynamicModule("Win32Module", $false) - $dynamic_type = $dynamic_module.DefineType("Win32Type", "Public, Class") - - $dynamic_method = $dynamic_type.DefineMethod( - $MethodName, - [Reflection.MethodAttributes]"Public, Static", - $ReturnType, - $ParameterTypes - ) - - $constructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor([String]) - $custom_attributes = New-Object -TypeName Reflection.Emit.CustomAttributeBuilder -ArgumentList @( - $constructor, - $DllName - ) - - $dynamic_method.SetCustomAttribute($custom_attributes) - $win32_type = $dynamic_type.CreateType() - $win32_type::$MethodName.Invoke($Parameters) - } - - # become process is run under a different console to the WinRM one so we - # need to set the UTF-8 codepage again, this also needs to be set before - # reading the stdin pipe that contains the module args specifying the - # remote_tmp to use. Instead this will use reflection when calling the Win32 - # API no tmp files touch the disk - $invoke_args = @{ - DllName = "kernel32.dll" - ReturnType = [bool] - ParameterTypes = @([UInt32]) - Parameters = @(65001) - } - Invoke-Win32Api -MethodName SetConsoleCP @invoke_args > $null - Invoke-Win32Api -MethodName SetConsoleOutputCP @invoke_args > $null - # stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives - # exec runspace, capture output, cleanup, return module output - - $json_raw = [System.Console]::In.ReadToEnd() + # exec runspace, capture output, cleanup, return module output. Do not change this as it is set become before being passed to the + # become process. + $json_raw = "" If (-not $json_raw) { Write-Error "no input given" -Category InvalidArgument @@ -1295,35 +1089,19 @@ Function Run($payload) { return $null } - # NB: CreateProcessWithTokenW commandline maxes out at 1024 chars, must bootstrap via filesystem - $temp = [System.IO.Path]::Combine($remote_tmp, [System.IO.Path]::GetRandomFileName() + ".ps1") - $exec_wrapper.ToString() | Set-Content -Path $temp + # NB: CreateProcessWithTokenW commandline maxes out at 1024 chars, must bootstrap via small + # wrapper which calls our read wrapper passed through stdin. Cannot use 'powershell -' as + # the $ErrorActionPreference is always set to Stop and cannot be changed + $payload_string = $payload | ConvertTo-Json -Depth 99 -Compress + $exec_wrapper = $exec_wrapper.ToString().Replace('$json_raw = ""', "`$json_raw = '$payload_string'") $rc = 0 + $exec_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($become_exec_wrapper.ToString())) + $lp_command_line = New-Object System.Text.StringBuilder @("powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $exec_command") + $lp_current_directory = "$env:SystemRoot" + Try { - # do not modify the ACL if the logon_type is LOGON32_LOGON_NEW_CREDENTIALS - # as this results in the local execution running under the same user's token, - # otherwise we need to allow (potentially unprivileges) the become user access - # to the tempfile (NB: this likely won't work if traverse checking is enaabled). - if ($logon_type -ne [Ansible.LogonType]::LOGON32_LOGON_NEW_CREDENTIALS) { - $acl = Get-Acl -Path $temp - - Try { - $acl.AddAccessRule($(New-Object System.Security.AccessControl.FileSystemAccessRule($username, "FullControl", "Allow"))) - } Catch [System.Security.Principal.IdentityNotMappedException] { - throw "become_user '$username' is not recognized on this host" - } Catch { - throw "failed to set ACL on temp become execution script: $($_.Exception.Message)" - } - Set-Acl -Path $temp -AclObject $acl | Out-Null - } - - $payload_string = $payload | ConvertTo-Json -Depth 99 -Compress - - $lp_command_line = New-Object System.Text.StringBuilder @("powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -File $temp") - $lp_current_directory = "$env:SystemRoot" - - $result = [Ansible.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, $lp_current_directory, $payload_string, $logon_flags, $logon_type) + $result = [Ansible.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, $lp_current_directory, $exec_wrapper, $logon_flags, $logon_type) $stdout = $result.StandardOut $stdout = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($stdout.Trim())) $stderr = $result.StandardError @@ -1334,8 +1112,6 @@ Function Run($payload) { } Catch { $excep = $_ Dump-Error -excep $excep -msg "Failed to become user $username" - } Finally { - Remove-Item $temp -ErrorAction SilentlyContinue } $host.SetShouldExit($rc) } @@ -1351,275 +1127,81 @@ $ErrorActionPreference = "Stop" # return asyncresult to controller $exec_wrapper = { -$DebugPreference = "Continue" -$ErrorActionPreference = "Stop" -Set-StrictMode -Version 2 + &chcp.com 65001 > $null + $DebugPreference = "Continue" + $ErrorActionPreference = "Stop" + Set-StrictMode -Version 2 -function ConvertTo-HashtableFromPsCustomObject ($myPsObject){ - $output = @{}; - $myPsObject | Get-Member -MemberType *Property | % { - $val = $myPsObject.($_.name); - If ($val -is [psobject]) { - $val = ConvertTo-HashtableFromPsCustomObject $val + function ConvertTo-HashtableFromPsCustomObject ($myPsObject){ + $output = @{}; + $myPsObject | Get-Member -MemberType *Property | % { + $val = $myPsObject.($_.name); + If ($val -is [psobject]) { + $val = ConvertTo-HashtableFromPsCustomObject $val + } + $output.($_.name) = $val } - $output.($_.name) = $val + return $output; } - return $output; -} -# stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives -# exec runspace, capture output, cleanup, return module output -$json_raw = [System.Console]::In.ReadToEnd() + # store the pipe name and no. of bytes to read, these are populated by the + # Run function before being run - do not remove or change + $pipe_name = "" + $bytes_length = 0 -If (-not $json_raw) { - Write-Error "no input given" -Category InvalidArgument -} + # stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives + # exec runspace, capture output, cleanup, return module output + $input_bytes = New-Object -TypeName byte[] -ArgumentList $bytes_length + $pipe = New-Object -TypeName System.IO.Pipes.NamedPipeClientStream -ArgumentList @( + ".", # localhost + $pipe_name, + [System.IO.Pipes.PipeDirection]::In, + [System.IO.Pipes.PipeOptions]::None, + [System.Security.Principal.TokenImpersonationLevel]::Anonymous + ) + try { + $pipe.Connect() + $pipe.Read($input_bytes, 0, $bytes_length) > $null + } finally { + $pipe.Close() + } + $json_raw = [System.Text.Encoding]::UTF8.GetString($input_bytes) -$payload = ConvertTo-HashtableFromPsCustomObject (ConvertFrom-Json $json_raw) + If (-not $json_raw) { + Write-Error "no input given" -Category InvalidArgument + } -# TODO: handle binary modules -# TODO: handle persistence + $payload = ConvertTo-HashtableFromPsCustomObject (ConvertFrom-Json $json_raw) -$actions = $payload.actions + # TODO: handle binary modules + # TODO: handle persistence -# pop 0th action as entrypoint -$entrypoint = $payload.($actions[0]) -$payload.actions = $payload.actions[1..99] + $actions = $payload.actions -$entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint)) + # pop 0th action as entrypoint + $entrypoint = $payload.($actions[0]) + $payload.actions = $payload.actions[1..99] -# load the current action entrypoint as a module custom object with a Run method -$entrypoint = New-Module -ScriptBlock ([scriptblock]::Create($entrypoint)) -AsCustomObject + $entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint)) -Set-Variable -Scope global -Name complex_args -Value $payload["module_args"] | Out-Null + # load the current action entrypoint as a module custom object with a Run method + $entrypoint = New-Module -ScriptBlock ([scriptblock]::Create($entrypoint)) -AsCustomObject -# dynamically create/load modules -ForEach ($mod in $payload.powershell_modules.GetEnumerator()) { - $decoded_module = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($mod.Value)) - New-Module -ScriptBlock ([scriptblock]::Create($decoded_module)) -Name $mod.Key | Import-Module -WarningAction SilentlyContinue | Out-Null -} + Set-Variable -Scope global -Name complex_args -Value $payload["module_args"] | Out-Null -$output = $entrypoint.Run($payload) + # dynamically create/load modules + ForEach ($mod in $payload.powershell_modules.GetEnumerator()) { + $decoded_module = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($mod.Value)) + New-Module -ScriptBlock ([scriptblock]::Create($decoded_module)) -Name $mod.Key | Import-Module -WarningAction SilentlyContinue | Out-Null + } -Write-Output $output + $output = $entrypoint.Run($payload) + Write-Output $output } # end exec_wrapper Function Run($payload) { -# BEGIN Ansible.Async native type definition - $native_process_util = @" - using Microsoft.Win32.SafeHandles; - using System; - using System.ComponentModel; - using System.Diagnostics; - using System.IO; - using System.Linq; - using System.Runtime.InteropServices; - using System.Text; - using System.Threading; - - namespace Ansible.Async { - - public static class NativeProcessUtil - { - [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode, BestFitMapping=false)] - public static extern bool CreateProcess( - [MarshalAs(UnmanagedType.LPTStr)] - string lpApplicationName, - StringBuilder lpCommandLine, - IntPtr lpProcessAttributes, - IntPtr lpThreadAttributes, - bool bInheritHandles, - uint dwCreationFlags, - IntPtr lpEnvironment, - [MarshalAs(UnmanagedType.LPTStr)] - string lpCurrentDirectory, - STARTUPINFOEX lpStartupInfo, - out PROCESS_INFORMATION lpProcessInformation); - - [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] - public static extern uint SearchPath ( - string lpPath, - string lpFileName, - string lpExtension, - int nBufferLength, - [MarshalAs (UnmanagedType.LPTStr)] - StringBuilder lpBuffer, - out IntPtr lpFilePart); - - [DllImport("kernel32.dll")] - public static extern bool CreatePipe(out IntPtr hReadPipe, out IntPtr hWritePipe, SECURITY_ATTRIBUTES lpPipeAttributes, uint nSize); - - [DllImport("kernel32.dll", SetLastError=true)] - public static extern IntPtr GetStdHandle(StandardHandleValues nStdHandle); - - [DllImport("kernel32.dll", SetLastError=true)] - public static extern bool SetHandleInformation(IntPtr hObject, HandleFlags dwMask, int dwFlags); - - [DllImport("kernel32.dll", SetLastError=true)] - public static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref int lpSize); - - [DllImport("kernel32.dll", SetLastError=true)] - public static extern bool UpdateProcThreadAttribute( - IntPtr lpAttributeList, - uint dwFlags, - IntPtr Attribute, - IntPtr lpValue, - IntPtr cbSize, - IntPtr lpPreviousValue, - IntPtr lpReturnSize); - - public static string SearchPath(string findThis) - { - StringBuilder sbOut = new StringBuilder(1024); - IntPtr filePartOut; - - if(SearchPath(null, findThis, null, sbOut.Capacity, sbOut, out filePartOut) == 0) - throw new FileNotFoundException("Couldn't locate " + findThis + " on path"); - - return sbOut.ToString(); - } - - [DllImport("kernel32.dll", SetLastError=true)] - static extern SafeFileHandle OpenThread( - ThreadAccessRights dwDesiredAccess, - bool bInheritHandle, - int dwThreadId); - - [DllImport("kernel32.dll", SetLastError=true)] - static extern int ResumeThread(SafeHandle hThread); - - public static void ResumeThreadById(int threadId) - { - var threadHandle = OpenThread(ThreadAccessRights.SUSPEND_RESUME, false, threadId); - if(threadHandle.IsInvalid) - throw new Exception(String.Format("Thread ID {0} is invalid ({1})", threadId, - new Win32Exception(Marshal.GetLastWin32Error()).Message)); - - try - { - if(ResumeThread(threadHandle) == -1) - throw new Exception(String.Format("Thread ID {0} cannot be resumed ({1})", threadId, - new Win32Exception(Marshal.GetLastWin32Error()).Message)); - } - finally - { - threadHandle.Dispose(); - } - } - - public static void ResumeProcessById(int pid) - { - var proc = Process.GetProcessById(pid); - - // wait for at least one suspended thread in the process (this handles possible slow startup race where - // primary thread of created-suspended process has not yet become runnable) - var retryCount = 0; - while(!proc.Threads.OfType().Any(t=>t.ThreadState == System.Diagnostics.ThreadState.Wait && - t.WaitReason == ThreadWaitReason.Suspended)) - { - proc.Refresh(); - Thread.Sleep(50); - if (retryCount > 100) - throw new InvalidOperationException(String.Format("No threads were suspended in target PID {0} after 5s", pid)); - } - - foreach(var thread in proc.Threads.OfType().Where(t => t.ThreadState == System.Diagnostics.ThreadState.Wait && - t.WaitReason == ThreadWaitReason.Suspended)) - ResumeThreadById(thread.Id); - } - } - - [StructLayout(LayoutKind.Sequential)] - public class SECURITY_ATTRIBUTES - { - public int nLength; - public IntPtr lpSecurityDescriptor; - public bool bInheritHandle = false; - - public SECURITY_ATTRIBUTES() { - nLength = Marshal.SizeOf(this); - } - } - - [StructLayout(LayoutKind.Sequential)] - public class STARTUPINFO - { - public Int32 cb; - public IntPtr lpReserved; - public IntPtr lpDesktop; - public IntPtr lpTitle; - public Int32 dwX; - public Int32 dwY; - public Int32 dwXSize; - public Int32 dwYSize; - public Int32 dwXCountChars; - public Int32 dwYCountChars; - public Int32 dwFillAttribute; - public Int32 dwFlags; - public Int16 wShowWindow; - public Int16 cbReserved2; - public IntPtr lpReserved2; - public IntPtr hStdInput; - public IntPtr hStdOutput; - public IntPtr hStdError; - - public STARTUPINFO() { - cb = Marshal.SizeOf(this); - } - } - - [StructLayout(LayoutKind.Sequential)] - public class STARTUPINFOEX { - public STARTUPINFO startupInfo; - public IntPtr lpAttributeList; - - public STARTUPINFOEX() { - startupInfo = new STARTUPINFO(); - startupInfo.cb = Marshal.SizeOf(this); - } - } - - [StructLayout(LayoutKind.Sequential)] - public struct PROCESS_INFORMATION - { - public IntPtr hProcess; - public IntPtr hThread; - public int dwProcessId; - public int dwThreadId; - } - - [Flags] - enum ThreadAccessRights : uint - { - SUSPEND_RESUME = 0x0002 - } - - [Flags] - public enum StartupInfoFlags : uint - { - USESTDHANDLES = 0x00000100 - } - - public enum StandardHandleValues : int - { - STD_INPUT_HANDLE = -10, - STD_OUTPUT_HANDLE = -11, - STD_ERROR_HANDLE = -12 - } - - [Flags] - public enum HandleFlags : uint - { - None = 0, - INHERIT = 1 - } - } -"@ # END Ansible.Async native type definition - - $original_tmp = $env:TMP - $original_temp = $env:TEMP $remote_tmp = $payload["module_args"]["_ansible_remote_tmp"] $remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp) if ($null -eq $remote_tmp) { @@ -1636,104 +1218,73 @@ Function Run($payload) { [System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($results_path)) | Out-Null - $env:TMP = $remote_tmp - $env:TEMP = $remote_tmp - Add-Type -TypeDefinition $native_process_util -Debug:$false - $env:TMP = $original_tmp - $env:TEMP = $original_temp - - # FUTURE: create under new job to ensure all children die on exit? - - # FUTURE: move these flags into C# enum? - # start process suspended + breakaway so we can record the watchdog pid without worrying about a completion race - Set-Variable CREATE_BREAKAWAY_FROM_JOB -Value ([uint32]0x01000000) -Option Constant - Set-Variable CREATE_SUSPENDED -Value ([uint32]0x00000004) -Option Constant - Set-Variable CREATE_UNICODE_ENVIRONMENT -Value ([uint32]0x000000400) -Option Constant - Set-Variable CREATE_NEW_CONSOLE -Value ([uint32]0x00000010) -Option Constant - Set-Variable EXTENDED_STARTUPINFO_PRESENT -Value ([uint32]0x00080000) -Option Constant - - $pstartup_flags = $CREATE_BREAKAWAY_FROM_JOB -bor $CREATE_UNICODE_ENVIRONMENT -bor $CREATE_NEW_CONSOLE ` - -bor $CREATE_SUSPENDED -bor $EXTENDED_STARTUPINFO_PRESENT - - # execute the dynamic watchdog as a breakway process to free us from the WinRM job, which will in turn exec the module - $si = New-Object Ansible.Async.STARTUPINFOEX - - # setup stdin redirection, we'll leave stdout/stderr as normal - $si.startupInfo.dwFlags = [Ansible.Async.StartupInfoFlags]::USESTDHANDLES - $si.startupInfo.hStdOutput = [Ansible.Async.NativeProcessUtil]::GetStdHandle([Ansible.Async.StandardHandleValues]::STD_OUTPUT_HANDLE) - $si.startupInfo.hStdError = [Ansible.Async.NativeProcessUtil]::GetStdHandle([Ansible.Async.StandardHandleValues]::STD_ERROR_HANDLE) - - $stdin_read = $stdin_write = 0 - - $pipesec = New-Object Ansible.Async.SECURITY_ATTRIBUTES - $pipesec.bInheritHandle = $true - - If(-not [Ansible.Async.NativeProcessUtil]::CreatePipe([ref]$stdin_read, [ref]$stdin_write, $pipesec, 0)) { - throw "Stdin pipe setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())" - } - If(-not [Ansible.Async.NativeProcessUtil]::SetHandleInformation($stdin_write, [Ansible.Async.HandleFlags]::INHERIT, 0)) { - throw "Stdin handle setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())" - } - $si.startupInfo.hStdInput = $stdin_read - - # create an attribute list with our explicit handle inheritance list to pass to CreateProcess - [int]$buf_sz = 0 - - # determine the buffer size necessary for our attribute list - If(-not [Ansible.Async.NativeProcessUtil]::InitializeProcThreadAttributeList([IntPtr]::Zero, 1, 0, [ref]$buf_sz)) { - $last_err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - If($last_err -ne 122) { # ERROR_INSUFFICIENT_BUFFER - throw "Attribute list size query failed, Win32Error: $last_err" - } - } - - $si.lpAttributeList = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($buf_sz) - - # initialize the attribute list - If(-not [Ansible.Async.NativeProcessUtil]::InitializeProcThreadAttributeList($si.lpAttributeList, 1, 0, [ref]$buf_sz)) { - throw "Attribute list init failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())" - } - - $handles_to_inherit = [IntPtr[]]@($stdin_read) - $pinned_handles = [System.Runtime.InteropServices.GCHandle]::Alloc($handles_to_inherit, [System.Runtime.InteropServices.GCHandleType]::Pinned) - - # update the attribute list with the handles we want to inherit - If(-not [Ansible.Async.NativeProcessUtil]::UpdateProcThreadAttribute($si.lpAttributeList, 0, 0x20002 <# PROC_THREAD_ATTRIBUTE_HANDLE_LIST #>, ` - $pinned_handles.AddrOfPinnedObject(), [System.Runtime.InteropServices.Marshal]::SizeOf([type][IntPtr]) * $handles_to_inherit.Length, ` - [System.IntPtr]::Zero, [System.IntPtr]::Zero)) { - throw "Attribute list update failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())" - } - - # need to use a preamble-free version of UTF8Encoding - $utf8_encoding = New-Object System.Text.UTF8Encoding @($false) - $stdin_fs = New-Object System.IO.FileStream @($stdin_write, [System.IO.FileAccess]::Write, $true, 32768) - $stdin = New-Object System.IO.StreamWriter @($stdin_fs, $utf8_encoding, 32768) - - $pi = New-Object Ansible.Async.PROCESS_INFORMATION - - $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($exec_wrapper.ToString())) - - # FUTURE: direct cmdline CreateProcess path lookup fails- this works but is sub-optimal - $exec_cmd = [Ansible.Async.NativeProcessUtil]::SearchPath("powershell.exe") - $exec_args = New-Object System.Text.StringBuilder @("`"$exec_cmd`" -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command") - - # TODO: use proper Win32Exception + error - If(-not [Ansible.Async.NativeProcessUtil]::CreateProcess($exec_cmd, $exec_args, - [IntPtr]::Zero, [IntPtr]::Zero, $true, $pstartup_flags, [IntPtr]::Zero, $env:windir, $si, [ref]$pi)) { - #throw New-Object System.ComponentModel.Win32Exception - throw "Worker creation failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())" - } - - # FUTURE: watch process for quick exit, capture stdout/stderr and return failure - - $watchdog_pid = $pi.dwProcessId - - [Ansible.Async.NativeProcessUtil]::ResumeProcessById($watchdog_pid) - - # once process is resumed, we can send payload over stdin + # can't use anonymous pipes as the spawned process will not be a child due to + # the way WMI works, use a named pipe with a random name instead and set to + # only allow current user to read from the pipe + $pipe_name = "ansible-async-$jid-$([guid]::NewGuid())" + $current_user = ([Security.Principal.WindowsIdentity]::GetCurrent()).User $payload_string = $payload | ConvertTo-Json -Depth 99 -Compress - $stdin.WriteLine($payload_string) - $stdin.Close() + $payload_bytes = [System.Text.Encoding]::UTF8.GetBytes($payload_string) + + $pipe_sec = New-Object -TypeName System.IO.Pipes.PipeSecurity + $pipe_ar = New-Object -TypeName System.IO.Pipes.PipeAccessRule -ArgumentList @( + $current_user, + [System.IO.Pipes.PipeAccessRights]::Read, + [System.Security.AccessControl.AccessControlType]::Allow + ) + $pipe_sec.AddAccessRule($pipe_ar) + $pipe = New-Object -TypeName System.IO.Pipes.NamedPipeServerStream -ArgumentList @( + $pipe_name, + [System.IO.Pipes.PipeDirection]::Out, + 1, + [System.IO.Pipes.PipeTransmissionMode]::Byte, + [System.IO.Pipes.PipeOptions]::Asynchronous, + 0, + 0, + $pipe_sec + ) + + try { + $exec_wrapper_str = $exec_wrapper.ToString() + $exec_wrapper_str = $exec_wrapper_str.Replace('$pipe_name = ""', "`$pipe_name = `"$pipe_name`"") + $exec_wrapper_str = $exec_wrapper_str.Replace('$bytes_length = 0', "`$bytes_length = $($payload_bytes.Count)") + + $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($exec_wrapper_str)) + $exec_args = "powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command" + + # not all connection plugins support breakaway from job that is required + # for async, Win32_Process.Create() is still able to escape so we use + # that here + $process = Invoke-CimMethod -ClassName Win32_Process -Name Create -Arguments @{CommandLine=$exec_args} + $rc = $process.ReturnValue + if ($rc -ne 0) { + $error_msg = switch($rc) { + 2 { "Access denied" } + 3 { "Insufficient privilege" } + 8 { "Unknown failure" } + 9 { "Path not found" } + 21 { "Invalid parameter" } + default { "Other" } + } + throw "Failed to start async process: $rc ($error_msg)" + } + $watchdog_pid = $process.ProcessId + + # wait until the client connects, throw an error if the timeout is reached + $wait_async = $pipe.BeginWaitForConnection($null, $null) + $wait_async.AsyncWaitHandle.WaitOne(5000) > $null + if (-not $wait_async.IsCompleted) { + throw "timeout while waiting for child process to connect to named pipe" + } + $pipe.EndWaitForConnection($wait_async) + + # write the exec manifest to the child process + $pipe.Write($payload_bytes, 0, $payload_bytes.Count) + $pipe.Flush() + $pipe.WaitForPipeDrain() + } finally { + $pipe.Close() + } # populate initial results before we resume the process to avoid result race $result = @{ diff --git a/test/integration/targets/win_async_wrapper/tasks/main.yml b/test/integration/targets/win_async_wrapper/tasks/main.yml index e437e9cb43..92d367a2fe 100644 --- a/test/integration/targets/win_async_wrapper/tasks/main.yml +++ b/test/integration/targets/win_async_wrapper/tasks/main.yml @@ -22,7 +22,7 @@ - name: async poll immediate success async_test: sleep_delay_sec: 0 - async: 10 + async: 20 poll: 1 register: asyncresult @@ -59,7 +59,7 @@ - name: async poll retry async_test: sleep_delay_sec: 5 - async: 10 + async: 20 poll: 1 register: asyncresult @@ -94,8 +94,8 @@ - name: async poll timeout async_test: - sleep_delay_sec: 5 - async: 3 + sleep_delay_sec: 25 + async: 20 poll: 1 register: asyncresult ignore_errors: true @@ -146,7 +146,7 @@ - name: echo some non ascii characters win_command: cmd.exe /c echo über den Fußgängerübergang gehen - async: 10 + async: 20 poll: 1 register: nonascii_output diff --git a/test/integration/targets/win_become/tasks/main.yml b/test/integration/targets/win_become/tasks/main.yml index e860ff78e4..1f2f241884 100644 --- a/test/integration/targets/win_become/tasks/main.yml +++ b/test/integration/targets/win_become/tasks/main.yml @@ -189,37 +189,16 @@ - whoami_out.label.sid == 'S-1-16-16384' - whoami_out.logon_type == 'Service' - # Test out Async on Windows Server 2012+ - - name: get OS version - win_shell: | - $version = [System.Environment]::OSVersion.Version - if ($version -ge [Version]"6.2") { - "async" - } elseif ($version -lt [Version]"6.1") { - "old-gramps" - } else { - "" - } - register: os_version - - - name: test become + async on older hosts + - name: test become + async vars: *become_vars win_command: whoami async: 10 register: whoami_out - ignore_errors: yes - - name: verify older hosts failed with become + async - assert: - that: - - whoami_out is failed - when: os_version.stdout_lines[0] != "async" - - - name: verify newer hosts worked with become + async + - name: verify become + async worked assert: that: - whoami_out is successful - when: os_version.stdout_lines[0] == "async" - name: test failure with string become invalid key vars: *become_vars @@ -244,12 +223,24 @@ failed_when: "failed_flags_invalid_flag.msg != \"Failed to parse become_flags 'logon_flags=with_profile,invalid': become_flags logon_flags value 'invalid' is not valid, valid values are: with_profile, netcredentials_only\"" # Server 2008 doesn't work with network and network_cleartext, there isn't really a reason why you would want this anyway + - name: check if we are running on a dinosaur, neanderthal or an OS of the modern age + win_shell: | + $version = [System.Environment]::OSVersion.Version + if ($version -lt [Version]"6.1") { + "dinosaur" + } elseif ($version -lt [Version]"6.2") { + "neanderthal" + } else { + "False" + } + register: os_version + - name: become different types vars: *become_vars win_whoami: become_flags: logon_type={{item.type}} register: become_logon_type - when: not ((item.type == 'network' or item.type == 'network_cleartext') and os_version.stdout_lines[0] == "old-gramps") + when: not ((item.type == 'network' or item.type == 'network_cleartext') and os_version.stdout_lines[0] == "dinosaur") failed_when: become_logon_type.logon_type != item.actual and become_logon_type.sid != user_limited_result.sid with_items: - type: interactive @@ -298,6 +289,7 @@ become_flags: logon_flags={{item.flags}} register: become_logon_flags failed_when: become_logon_flags.stdout_lines[0]|bool != item.actual + when: os_version.stdout_lines[0] not in ["dinosaur", "neanderthal"] # usual suspect 2008 doesn't support the no profile flags with_items: - flags: actual: False