puppetlabs/puppet · error · Puppet::Util::Windows::Error
Failed to get child process exit code
Error message
Failed to get child process exit code
What it means
Raised by Puppet::Util::Windows::Process.wait_process (lib/puppet/util/windows/process.rb:48) when GetExitCodeProcess returns FALSE after the wait loop. The Win32 code on Error#code is typically ERROR_INVALID_HANDLE (6) — the handle was closed or is bogus — or ERROR_ACCESS_DENIED (5) — the handle was not opened with query rights. Note the wait loop exits on anything other than WAIT_TIMEOUT, including WAIT_FAILED, so a broken wait can hand an unusable handle to GetExitCodeProcess.
Source
Thrown at lib/puppet/util/windows/process.rb:48
if arguments[:suppress_window]
create_args[:creation_flags] = CREATE_NO_WINDOW
end
if arguments[:cwd]
create_args[:cwd] = arguments[:cwd]
end
Process.create(create_args)
end
module_function :execute
def wait_process(handle)
while WaitForSingleObject(handle, WAIT_INTERVAL) == WAIT_TIMEOUT
sleep(0)
end
exit_status = -1
FFI::MemoryPointer.new(:dword, 1) do |exit_status_ptr|
if GetExitCodeProcess(handle, exit_status_ptr) == FFI::WIN32_FALSE
raise Puppet::Util::Windows::Error, _("Failed to get child process exit code")
end
exit_status = exit_status_ptr.read_dword
# $CHILD_STATUS is not set when calling win32/process Process.create
# and since it's read-only, we can't set it. But we can execute a
# a shell that simply returns the desired exit status, which has the
# desired effect.
%x(#{ENV.fetch('COMSPEC', nil)} /c exit #{exit_status})
end
exit_status
end
module_function :wait_process
def get_current_process
# this pseudo-handle does not require closing per MSDN docs
GetCurrentProcess()View on GitHub (pinned to e227c27540)
Solutions
- Inspect e.code: 6 = stale/invalid handle (a handle-lifecycle bug in the caller), 5 = insufficient access (reopen with query rights).
- Call wait_process exactly once with the handle obtained from Process.create / execute, and let the owning scope close it.
- Never cache process handles across waits or close them from your own code.
- If you control creation, keep close_handles semantics in mind so the handle is still valid at wait time.
Example fix
# before — handle reused after a prior wait already consumed it Process.wait_process(phandle) Process.wait_process(phandle) # raises: handle stale # after — one wait per handle, straight from creation info = Process.create(command_line: cmd) Process.wait_process(info.process_handle)
Defensive patterns
Strategy: try-catch
Try / catch
begin
exit_code = Process.wait_process(handle)
rescue Puppet::Util::Windows::Error => e
Puppet.err "wait_process failed (code #{e.code}): #{e.message}"
exit_code = nil # treat outcome as unknown
end Prevention
- One wait per handle; never re-wait or externally close handles
- Keep creation and waiting in the same scope so handles cannot go stale
- Log Error#code to distinguish handle bugs (6) from permissions (5)
When it happens
Trigger: Passing a process handle that was already closed (double close, external CloseHandle, GC of the win32-process object); a handle opened without PROCESS_QUERY_INFORMATION / PROCESS_QUERY_LIMITED_INFORMATION rights; WaitForSingleObject failing so the loop falls through immediately on a bad handle.
Common situations: Custom exec wrappers around Puppet::Util::Windows::Process.execute that manage handles themselves; reusing a handle after wait_process already consumed it; races where the child exits during creation.
Related errors
- OpenProcess(#{desired_access.to_s(8)}, #{inherit}, #{process
- QueryFullProcessImageNameW(phandle, #{use_win32_path_format}
- GetTokenInformation(#{token_handle}, #{token_information}, n
- ReplaceFile(#{target}, #{source})
- MoveFileEx(#{source}, #{target}, #{flags.to_s(8)})
AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21).
Data as JSON: /api/errors/3f17ed4e6afb018d.
Report an issue: GitHub.