How do I know that Resource Monitor isnt just retaining a handle to the terminated process?


Not too long ago, I investigated whether the Resource Monitor retains an open handle to the process as its way to continue showing statistics after the process exited. I used Task Manager to determine whether there was still an open handle, on the theory that Task Manager shows all the valid process IDs, even for processes that have terminated.

A reader who needs to choose a better screen name pointed out that it's possible that Task Manager intentionally removes terminated processes from display.

This was a fair criticism, so let's test this theory.

#include <windows.h>
#include <stdio.h> // horrors! Mixing stdio and C++!

int __cdecl main()
{
 PROCESS_INFORMATION pi;
 STARTUPINFO si = { sizeof(si) };
 TCHAR command[] = TEXT("notepad.exe");
 CreateProcess(nullptr, command,
  nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi);
 WaitForSingleObject(pi.hProcess, INFINITE);
 printf("Process has exited. "
        "Keeping handle open for a little while longer.\n");
 Sleep(10 * 1000);
 CloseHandle(pi.hProcess);
 CloseHandle(pi.hThread);
 return 0;
}

This program launches Notepad, waits for it to exit, then waits an additional ten seconds before terminating. During those ten seconds, the process handle is still open, and hopefully we'll see the process still listed in Task Manager.

Run this test program, close the Notepad window the opens, and then during the ten second delay, go to the Details page of Task Manager to see if Notepad is present.

Nope, it's not there. The commenter was correct. Task Manager does remove terminated processes from display, even though the process object has not been destroyed.

(This changed at some point, because the old Task Manager did keep zombie processes around. That was one way to find out that you had a process handle leak: The zombie processes started piling up in Task Manager. My guess is that the change occurred when Task Manager was rewritten for Windows 8.)

Okay, so our test was flawed. We'll have to try a different test. This time, let's write a program that launches Notepad, waits for it to exit, and then tries to reopen it by its process ID. This will succeed if there is still an outstanding handle to the process, and will fail if there are no outstanding handles.

#include <windows.h>
#include <stdio.h> // horrors! Mixing stdio and C++!

int __cdecl main()
{
 PROCESS_INFORMATION pi;
 STARTUPINFO si = { sizeof(si) };
 TCHAR command[] = TEXT("notepad.exe");
 CreateProcess(nullptr, command,
  nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi);
 WaitForSingleObject(pi.hProcess, INFINITE);
 CloseHandle(pi.hProcess);
 CloseHandle(pi.hThread);
 printf("Process has exited. Try to open the process by ID.\n");
 Sleep(1000);
 auto h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
   FALSE, pi.dwProcessId);
 printf("Resulting handle: %p\n", h);
 if (h) CloseHandle(h);
 return 0;
}

This second test program waits for Notepad to exit, and then closes all the handles and then tries to reopen it.

Note that there is a brief Sleep after the process exits. This gives a little time for anybody who was doing the "Close the handle as soon as the process terminate" thing to close their handle. Without it, you will find that even without Resource Manager running, you'll see a handle retained somewhere. My guess it's anti-malware software.

Okay, run the test program with no Resource Monitor running. Close Notepad, wait one second, and observe that it reports that the resulting handle is 00000000, which means that the Open­Process failed and there is no process with that ID, meaning that the process object has been destroyed, which means that there are no open handles to it.

Now run our experiment with Resource Monitor: Run the test program, let Resource Monitor observe the new Notepad process. Now connect a debugger to Resource Monitor and freeze it, so that if it had an open handle to Notepad, it won't be able to close it. Close the Notepad window, wait one second, and observe that the test program reports that the resulting handle is 00000000. This demonstrates that Resource Monitor doesn't retain a handle to the Notepad process.

Comments (17)
  1. ranta says:

    When I read the “This gives a little time” part, I immediately thought oplocks would be the correct way to solve this, now with process handles rather than file handles. But I have difficulty imagining what the semantics should be.

  2. Austin Donnelly (MSFT) says:

    Although in general, be careful when getting a handle to a process based on a process ID. Those process IDs can (and do) get reused over time, so you might not be opening a handle to the process you thought you wanted. If you really must open a process by PID, ensure you also know it’s parent’s PID. Then when you open the handle, you can validate that the process’s parent is really the one you expected.

    1. skSdnW says:

      No, the parent could also in theory be reused (unless you are the parent of course). The correct tuple is id + start-time, see RM_UNIQUE_PROCESS: “Uniquely identifies a process by its PID and the time the process began”

      1. Pietro Gagliardi (andlabs) says:

        Yeah, but GetWindowThreadProcessId(), EnumProcesses(), and OpenProcess() don’t let you specify those unique pairs, and there are no -Ex() APIs that change this. Are those unique pairs specific to Restart Manager in this case?

        1. Joshua says:

          Open-query-close. Such is life.

        2. skSdnW says:

          That struct was invented by the RM team but the uniqueness of such a tuple is valid everywhere.

          You don’t need a new API, 99% of the time OpenProcess on the PID gives you the correct process and the overhead is simply the extra query for the start time to verify the PID. If the open fails or if the PID was reused you have to abandon your operation anyway.

      2. dmex says:

        @skSdnW

        “The correct tuple is id + start-time”

        This is one of those implementation details that should not be relied upon…

        Previous versions of Windows and the Restart Manager API (unfortunatly) still use the PID and ‘start-time’. However, the process ‘start-time’ does not include timezone information and when the timezone changes (e.g. Daylight Saving Time) some processes will re-use the pid and the ‘start-time’ because the system clock changed…

        Process Explorer also uses the “id + start-time” to show the child/parent process tree by validating the ‘ParentPID+ start-time’ is less than the ‘ChildPID+start-time’ but when the timezone changes (e.g. Daylight Saving Time) the children can have earlier start-times than their parents and Process Explorer ends up showing an incorrect child/parent process tree.

        This also gets more ticky on Windows 10 since some system processes such as the “Secure system” and “Registry” processes have creation times earlier than their parent ‘System’ process and this is why Windows 10 now uses whats called a “ProcessSequenceNumber” for internal bookkeeping which is “guaranteed monotonic unlike time” and negates the need to compare “id + start-time” and solves the issues with timezone changes but Process Explorer and the Restart Manager API (unfortunatly) don’t use it yet.

  3. Kyle S. says:

    This is a fantastic lesson. skSdnW noticed a logical gap in a model that otherwise fit the data extremely well. They were rewarded for their thoroughness with accusations of paranoia.

    Models are great because they don’t need to be completely accurate to be useful. But skSdnW has reminded us to be conscious of the limitations of modeling, and the other commenters have reminded us of how quickly programmers will convert short-term success into arrogance.

    1. Rick C says:

      I’m curious what paranoia you thought you saw, given that Raymond said “This was a fair criticism”.

      If you’re referring to the rude name comment, well, read it backwards, and bear in mind this change was made after Raymond complained about the commenter’s original name, which was ruder.

      1. Kyle S. says:

        Rick, I’m not referring to Raymond. I’m referring to people who replied to skSdnW’s comment on the original article.

  4. Joshua says:

    The man with the social skills of a thermonuclear device is remarkably tolerant.

  5. alexx says:

    One way to find out if you have zombie processes by using Process Explorer is to search for handles named “Non-existent Process”. If match, procexp will show handles with name “Non-existent Process (xxxx)”, where xxxx is the PID of the zombie process.

  6. Someone says:

    “meaning that the process object has been destroyed, which means that there are no open handles to it.”

    My understanding is a bit different.

    The process object can live for itself, without any open handle. Example: Your program starts Notepad.exe, and then close the thread and the process handle you got from CreateProcess. The process and therefore it’s process object will be alive as long as the user does not close it. Note that this is different from other kernel objects; processes are not need to be “owned” by something outside the kernel.

    This situation changes when the process terminates: In the Terminated state, the process object is no longer needed by the kernel itself, so it will only be kept around until all referencing handles are closed. If the process terminates with no referencing handles at this point in time, then the process object is removed as soon as the kernel has finished the cleanup.

    1. Someone says:

      If one is using the SysInternals tool “handle” (https://docs.microsoft.com/en-us/sysinternals/downloads/handle), it is surprising how many references to a process are held by kernel parts like “System”, “csrss.exe” or “lsass.exe” or even “svchost.exe”. So, the handle count as such is not enough for the discussion. The view needs to be splitted into “handle held by kernel component” and “handle held by regular process”.

      1. alegr1 says:

        The handle tool can only show who owns handles to an object. It cannot show who owns references, because only kernel can add references to an object, and they don’t have an owner.

    2. My statement was “destroyed implies no handles.” The converse is not true, which is the case you’re talking about.

    3. ranta says:

      Re “this is different from other kernel objects”: I think the main difference here is that processes and threads are opened by ID, rather than by name. If these objects don’t have names in the first place, it does not matter whether the object manager deletes the name when the last handle is closed. (Drivers that create named objects can suppress this automatic deletion by setting the OBJ_PERMANENT flag in OBJECT_ATTRIBUTES.)

Comments are closed.

Skip to main content