win_glue DLLMain problems with WSAStartup / WSACleanup
Paul B. Hill
pbh at MIT.EDU
Fri Mar 25 11:08:30 EST 2005
Hi,
I've found some references about the problem that Jeff described (the
problem was brought to our attention by IASTATE). The final reference
appears to offer a viable solution that I believe is cleaner than Jeff's
original proposal, although still a bit of a kludge. Joe Calzaretta provided
me with a number of ideas that got me pointed in the right direction for my
search.
On Mar 25, 2005, at 05:14, Jeffrey Altman wrote:
> In the DllMain() function of our DLLs, we currently call WSAStartup at
> process attach and WSACleanup at process detach. When the Microsoft
> Internet Connection Firewall is enabled, the call to WSACleanup will
> produce a deadlock.
>
> According to Microsoft, we cannot make these calls. Instead, the
> application must initialize Winsock for us or we must initialize it
> outside of the DllMain(). This most likely means that we will have to
> check all Winsock() api calls to see whether there is a
> WSANOTINITIALISED error and if so call WSAStartup() and then retry the
> operation. By doing so, the most we will leak is one winsock
> initialization since as far as I can tell there is no method by which
> we can safely perform a WSACleanup().
From
<http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dv_vstecha
rt/html/vcconMixedDLLLoadingProblem.asp>:
DllMain Restrictions
The DllMain entry point function is intended to perform only simple
initialization and termination tasks. Doing more than simple initialization
and termination can create deadlocks and circular dependencies. This
restriction stems from the fact that the DllMain entry point function runs
while holding a locking mechanism for the OS loader. This mechanism ensures
that code inside the DLL cannot run before the DLL has been initialized.
Furthermore, the OS loader lock prevents multiple threads or processes from
attempting to load DLLs at the same time, which could corrupt global data
structures used during the loading process. In order to help customers avoid
problems with their DllMain functions, the MSDN library documents those
operations that can and cannot be performed safely inside a DLL entry point.
For more information, see DllMain.
The following operations are specifically identified as being safe to
perform inside a DllMain function:
- Initialization statics and globals.
- Calling functions in Kernel32.dll. This is always safe since Kernel32.dll
must be loaded by the time DllMain is called.
- Creating synchronization objects such as critical sections and mutexes.
For more information, see Synchronization Objects.
- Accessing Thread Local Storage (TLS).
The following operations are specifically identified as being unsafe inside
a DllMain function under most circumstances:
- Calling the LoadLibrary, LoadLibraryEx, or FreeLibrary functions directly
or indirectly.
- Calling the registry functions.
- Calling imported functions other than those located in Kernel32.dll.
- Communication with other threads or processes.
The rules for DllMain functions are not yet enforced by the system. Instead,
the OS will attempt to detect a deadlock or dependency loop and terminate
the process if the rules are broken and something goes wrong. Usually, the
OS will not detect the deadlock, and the process will hang.
[pbh - note calling WSAStatup or WSAShutdown indirectly call LoadLibrary and
FreeLibrary ]
Also from
<http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dv_vstecha
rt/html/vcconMixedDLLLoadingProblem.asp>:
Managed Code and DllMain
It is never safe to run managed code inside DllMain. This means that it is
not safe for DllMain to be implemented in MSIL, nor is it safe for DllMain
to directly or indirectly call a function that is implemented in MSIL. If
managed code is run inside DllMain, deadlock is a possibility.
There are several circumstances under which the runtime must perform
operations that are not permitted under DllMain in order to guarantee
correct semantics. The following two sections provide specific examples of
these circumstances.
Other limitations of DllMain:
<http://support.microsoft.com/kb/142243/EN-US/>:
"DllMain, however, is not re-entrant. ..."
<http://www.microsoft.com/msj/archive/S220.aspx>:
"...whenever a new thread starts, the DllMain routines of all DLLs in the
process are called. When this happens, the "reason" parameter to DllMain is
set to DLL_THREAD_ATTACH. ... A more important lesson to take away from this
is that your DllMain function should be written with care. Try to avoid
operations inside your DllMain that require using thread synchronization.
Conventional wisdom says that the DllMain routine should be kept as small as
possible, and do as little as possible. Still, in all my reading, I've never
come across these warnings in print. Those of you who programmed in Windows
3.x knew that there were restrictions on what you could do in your DLL's
LibMain and WEP routines. Alas, these rules were never formally specified.
It appears that DllMain functions (the Win32 equivalent of LibMain and WEP)
also have restrictions. Alas, there doesn't seem to be any formal
description of the do's and don'ts when writing a DllMain routine.
As a final note, if you're interested in a problem related to what I've
described here, refer to Jeffrey Richter's December 1994 MSJ Win32 Q&A
column. In that column, Jeff also describes a deadlock situation, and
mentions that Windows NT serializes calls to the DllMain routine so that
only one thread at a time is in DllMain. The process critical section that
I've described in this column is one of the means by which the operating
system enforces this serialization of calls to the DllMain routine."
<http://www.microsoft.com/msj/archive/S202B.aspx>:
"...I discussed how the system serializes all calls to DllMain functions in
a process. This means that, when your DllMain receives the
DLL_PROCESS_DETACH notification, no other threads can execute code in any
other DllMain functions, including yours. So, when you call SetEvent, the
worker threads are trying to terminate but they can't completely terminate
until every DLL's DllMain function receives a DLL_THREAD_DETACH
notification. Since the worker threads can't terminate, your call to
WaitForMultipleObjects never returns and you have deadlocked the threads.
The essence of your problem is that you need to terminate the worker threads
just before your DLL gets a DLL_PROCESS_DETACH notification. Here is what I
propose: create your DLL as usual but modify your DLL_PROCESS_ATTACH
processing so it increments the usage count of your DLL (see Figure 3). I do
this by calling the IncrementLibraryUsageCount function (implemented inside
my DllWork.c file). By incrementing the usage count of the DLL, it won't be
unloaded when IIS calls FreeLibrary. This stops the problem of your DLL code
going away while your threads keep running. (It introduces the problem that
your DLL never gets unloaded, but I'll solve that problem in a moment.)
Export an additional function, called ShutdownLibrary, from the DLL. This
function will simply call SetEvent to signal your event object to terminate
the worker threads and then return.
Just before your thread functions return, place a call to
FreeLibraryAndExitThread. This counters your call to
IncrementLibraryUsageCount. This way, as your worker threads terminate, each
one will decrement the usage count on the DLL. Eventually, one of the worker
threads will decrement the usage count to 0 and the DLL will be unloaded
from the IIS address space. Since this function also terminates the thread,
you don't have to worry about the thread continuing to run after the DLL's
code has been unloaded. Finally, remove any calls to WaitForMultipleObjects
from your DllMain's DLL_PROCESS_DETACH processing so this code executes only
when all of the worker threads have terminated and the DLL is really being
unloaded.
Now that I moved the shutdown code from the DLL_PROCESS_DETACH processing to
the ShutdownLibrary function, I'm sure you're wondering how ShutdownLibrary
will be called since IIS doesn't know anything about it. The answer lies in
another DLL; you must create a very small stub DLL like the one shown in
Figure 4. This stub DLL needs only a DllMain function that processes
DLL_PROCESS_DETACH notifications. When it receives this notification, you'll
want to call the ShutdownLibrary function contained in your main DLL. You
make this work by telling IIS that the stub DLL, not your main DLL, is your
ISAPI DLL. When IIS calls LoadLibrary to load the stub DLL, the OS loader
automatically loads your worker DLL because the stub DLL implicitly links to
it by calling ShutdownLibrary.
When IIS calls FreeLibrary, passing the handle of the stub DLL, the stub DLL
will get a DLL_PROCESS_DETACH notification and call the worker DLL's
ShutdownLibrary function to set the event. At this point, the worker threads
will begin terminating. However, they won't be able to enter any DllMain
functions until the thread in the stub's DllMain returns. This is OK. In
fact, the stub DLL will probably get unloaded from the process's address
space almost immediately, but the worker DLL will stay in memory until all
of the worker threads have terminated completely.
There is one last problem. Since IIS loads the stub DLL instead of the
worker DLL, it will try to call functions that are in the stub DLL. At first
the solution seemed obvious; put stub functions in the stub DLL and let them
call the real functions in the worker DLL. I hated this solution because it
meant more work, but then I remembered a little feature about linking:
function forwarders.
A function forwarder is an entry in a DLL's export table that redirects a
function call to another function in another DLL...."
More information about the krbdev
mailing list