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

  1. Inspect e.code: 6 = stale/invalid handle (a handle-lifecycle bug in the caller), 5 = insufficient access (reopen with query rights).
  2. Call wait_process exactly once with the handle obtained from Process.create / execute, and let the owning scope close it.
  3. Never cache process handles across waits or close them from your own code.
  4. 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

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


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/3f17ed4e6afb018d. Report an issue: GitHub.