The lackey catastrophe


We encountered a real problem with global object destruction in Explorer. The object in question was an RAII container for a graphics object, so its destructor destroyed the graphics object. But the call to destroy the object was crashing.

GDI32!IS_INDEX_IN_USER_SHARED_ARRAY+0x17
GDI32!HANDLE_TO_INDEX+0x1a
GDI32!DeleteObject+0x2a
explorer!CBitmap::~CBitmap+0x20
msvcrt!doexit+0xb6
msvcrt!_cexit+0xb
msvcrt!__CRTDLL_INIT+0x9f
ntdll!LdrxCallInitRoutine+0x16
ntdll!LdrpCallInitRoutine+0x43
ntdll!LdrShutdownProcess+0x1c1
ntdll!RtlExitUserProcess+0x96
kernel32!ExitProcess+0x32
explorer!wWinMain+0x4ef
explorer!WinMainCRTStartup+0x151
KERNEL32!BaseThreadInitThunk+0x24
ntdll!__RtlUserThreadStart+0x2b
ntdll!_RtlUserThreadStart+0x1b

What's going on?

The call to Delete­Object was occurring after GDI32 had already run its DLL_PROCESS_ATTACH DLL_PROCESS_DETACH. As a result, it was calling into a DLL that had already uninitialized, so bad things happen.

But wait, how can you call into a DLL that has already uninitialized? The EXE links to the DLL via a load-time dependency, so the EXE should uninitialize first. But it's not. Why not?

Recall how global objects are constructed and destructed. If the global object had been in a DLL, then indeed the loader dependency analysis would have seen that the global object's DLL depends upon GDI32.

As we saw in the earlier discussion, executables do not have DLL_PROCESS_DETACH. You can look at the situation in two ways: One interpretation is that the executable has already stopped running at the point it calls Exit­Process. All that's left is to shut down the DLLs. Another interpretation is that the executable is always running (seeing as DLLs run in the context of a process), so there's no point trying to wait until the executable has "finished" because if you did that, you'd be waiting forever.

Anyway, regardless of how you choose to look at the situation, the problem is the same: The lackey we hired to run down our global objects is running them down too late.

This is a case where the C runtime is doing the best it possibly can, but it's still not good enough.

Next time, we'll look at one possible extrication from this quandary.

Comments (15)
  1. skSdnW says:

    This is probably a generic problem so GDI32 could be helpful and deal with it for everyone: When it gets a DLL_PROCESS_DETACH that is for the processes it can neuter DeleteObject and let the kernel deal with the final cleanup.

    Typo: “had already run its DLL_PROCESS_ATTACH” you probably mean DLL_PROCESS_DETACH.

    1. Joshua says:

      That doesn’t actually work (GDI.DLL might be unloaded with FreeLibrary); however it’s not all that bad of an idea. If DeleteObject was changed to recover from handle out of range it would fix everybody. DLL_PROCESS_DETACH set the number of handles to zero.

      1. skSdnW says:

        You can tell if DLL_PROCESS_DETACH is because of FreeLibrary by looking at the third DllMain parameter…

    2. Antonio Rodríguez says:

      GDI32.DLL has been unloaded. An unloaded library should not be called. So GDI32.DLL can not expect to receive calls at that point. Solving the problem at that level is wrong.

      1. skSdnW says:

        Yes, letting GDI32 solve it would be at the wrong level but it would not be the first time that the ExitProcess case changes APIs: https://blogs.msdn.microsoft.com/oldnewthing/20070503-00/?p=27003/

        Another way to solve it in Explorer.exe would be:

        CBitmap *g_mybitmap;
        int WinMain(…)
        {
        CBitmap mybitmap;
        g_mybitmap = &mybitmap;

        return 0;
        }

        but that is rather ugly and only works if you return, not if you manually call ExitProcess.

  2. Adrian says:

    The semi-deterministic ordering of construction and destruction of globals (in C++) is just another in a long list of reasons why we shouldn’t use globals. Singletons are marginally better in this regard since we can explicitly control when they get created and destroyed.

    My psychic powers predict that the possible solution we’ll explore tomorrow is to skip the clean-up. After all, the process is on the way out, so the OS is going to reclaim nearly all the resources anyone could care about. As a bonus, you may get faster shutdown.

    1. not important says:

      Andrei – Just like you I think that the problem is “The semi-deterministic ordering of construction and destruction of globals (in C++) “. Your solution is to not use globals. Another solution is to not do “interesting” work in constructor / destructor and do this work in methods that need to be called explicitly (Initialize / Finalize) by programmers.

      1. I’d like to see an RAII container try that…

  3. Michael says:

    I think there is a mistake: “The call to Delete­Object was occurring after GDI32 had already run its DLL_PROCESS_ATTACH.”, it should say DLL_PROCESS_DETACH, or am I wrong?

    1. Brian_EE says:

      Technically, Raymond wasn’t wrong either. The call to GDI32 certainly was after _ATTACH had run, because _DETACH runs after _ATTACH, and the call was after _DETACH.

      DLL_PROCESS_ATTACH->(bunch of stuff)->DLL_PROCESS_DETACH->(the breaking call)

      1. Not wrong, sure, but not terribly interesting either.

  4. Maybe WinMainCRTStartup should call the global destructors then call ExitProcess.

    1. Darran Rowe says:

      If you look at the call stack, it is wWinMain that is calling ExitProcess, it isn’t returning to CRT/VCRuntime code.
      I have a good idea why this is happening, but I don’t want to spoil it.

  5. ZLB says:

    One way to fix this would be to use #pragma init_seg(user) to force the object to be constucted last and destructed first.

    Not a nice solution though…

  6. Yuhong Bao says:

    This reminds me of UnregisterClass in DllMain. I disassembled it and in addition to NtUserUnregisterClass it deals with activation contexts (which is what user32!ClassNameToVersion does) and sometimes free memory, all via NTDLL calls.

Comments are closed.

Skip to main content