1

We are using WinInet inside a DLL to make asynchronous network calls.

When the application exits, we remove the registered callback functions for pending requests by using InetSetStatusCallback(connect_handle, NULL);. However, occasionally the callback function still gets called after the DLL has unloaded, leading to application crashes.

The symptoms are exactly similar to the last FAQ of this blog: WinHTTP Questions: About Callbacks

I am trying to figure out a way to safely remove callback functions for all pending requests so that they are not called by WinInet after the DLL has unloaded.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Add a non-DLL dummy callback. – Michael Chourdakis Apr 26 '19 at 07:17
  • Any details on how setting a non-DLL dummy callback different from setting NULL callback ? Why is this expected to work ? – Shashank Gupta Apr 26 '19 at 07:39
  • Create a function that does nothing in your code and set it as a callback. However this is only a temporary solution, you should check the code for race conditions. – Michael Chourdakis Apr 26 '19 at 08:06
  • Are you aborting the requests, and waiting for them to fully abort, before unloading the DLL? – Remy Lebeau Apr 26 '19 at 08:35
  • @remy We are calling `InternetCloseHandle` on root internet handle returned from `InternetOpen` but not on the `InternetConnect` handles derived from root handle. Will closing all open `InternetConnect` handles before closing root handle solve this ? – Shashank Gupta Apr 26 '19 at 09:26
  • @ShashankGupta of course you must close all handles that you open. The documentation even says so : "*After the calling application has finished using the HINTERNET handle returned by InternetConnect, **it must be closed using the InternetCloseHandle function**.*" – Remy Lebeau Apr 26 '19 at 15:47

2 Answers2

0

after we pass pointer to callback function - module which containing this callback function of course must not be unloaded, until callback may be called. this is of course not problem if callback function was in EXE module, which never will be unloaded. but in case DLL we need add reference to DLL before set callback, for prevent DLL unload. this is easy can be done via GetModuleHandleExW function

HMODULE hmod;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (PCWSTR)&__ImageBase, &hmod);

ok, but how not let DLL be unloaded ? we need dereference it (call FreeLibrary) when our callback will be no more called. when this will be ?

the InternetSetStatusCallback sets up a callback function for some handle. this handle of course should ever be closed via InternetCloseHandle and:

It is safe to call InternetCloseHandle in a callback for the handle being closed. If there is a status callback registered for the handle being closed, and the handle was created with a non-NULL context value, an INTERNET_STATUS_HANDLE_CLOSING callback will be made. This indication will be the last callback made from a handle and indicates that the handle is being destroyed.

If asynchronous requests are pending for the handle or any of its child handles, the handle cannot be closed immediately, but it will be invalidated. Any new requests attempted using the handle will return with an ERROR_INVALID_HANDLE notification. The asynchronous requests will complete with INTERNET_STATUS_REQUEST_COMPLETE. Applications must be prepared to receive any INTERNET_STATUS_REQUEST_COMPLETE indications on the handle before the final INTERNET_STATUS_HANDLE_CLOSING indication is made, which indicates that the handle is completely closed.

so INTERNET_STATUS_HANDLE_CLOSING - will be the last callback made from a handle. finall call. exactly at this time we need defereference DLL module - callback will be no more called. not need reference more.

ok. when is clear. but how ? we can not direct call FreeLibrary from this point, because if DLL live on this last reference - it will be unloaded inside FreeLibrary call and when we return - we return to the emply space and crash.

we can not call FreeLibraryAndExitThread too - because we on arbitrary thread. however exist not correct but worked on practic solution:

ULONG WINAPI SafeUnload(void*)
{
    //Sleep(*);
    FreeLibraryAndExitThread((HMODULE)&__ImageBase, 0);
}

if (HANDLE hThread = CreateThread(0, 0, SafeUnload, 0, 0, 0))
{
    CloseHandle(hThread);
}

we can create new thread yourself, and from this thread call FreeLibraryAndExitThread (here this call is correct). but how i say - this is theoretically not correct solution. here exist race - the FreeLibraryAndExitThread call can execute and unload DLL before we return from CreateThread call. as result we crash just after CreateThread (we try execute already unloaded code). of course this is unlikely, but..

we can insert Sleep (some delay) before call FreeLibraryAndExitThread but anyway - can be race theoretically for any delay. again what concrete value for delay select ?! and DLL will be delayed unload for this time. not correct and not nice. despite this is relative simply "solution" i be not use it.

correct solution will be - call (more exactly jump) to FreeLibrary without return to place, from where this jump will be. but hust return to place, from where callback was called. this is impossible in c/c++ but possible in asm code. of course this require separate asm file for every platform target (x86, x64 how minimum)

so correct and elegant solution is next: the INTERNET_STATUS_CALLBACK callback function anyway must be associated with some class, where we hold handle context. this must be static function in class and DWORD_PTR dwContext - application-defined context value associated with hInternet must point to the instance of class (by fact this pointer). usually this is done in next way:

class REQUEST_CONTEXT 
{
  // ...
    static void WINAPI _StatusCallback(
        __in  HINTERNET hRequest,
        __in  DWORD_PTR dwContext,
        __in  DWORD dwInternetStatus,
        __in  LPVOID lpvStatusInformation,
        __in  DWORD dwStatusInformationLength
        )
    {
        reinterpret_cast<REQUEST_CONTEXT*>(dwContext)->StatusCallback(
            hRequest, 
            dwInternetStatus, 
            lpvStatusInformation, 
            dwStatusInformationLength
            );
    }

    void StatusCallback(
        __in  HINTERNET hRequest,
        __in  DWORD dwInternetStatus,
        __in  LPVOID lpvStatusInformation,
        __in  DWORD dwStatusInformationLength
        );
};

be here we need implement _StatusCallback in asm code for call FreeLibrary here. and chage return value of StatusCallback from void to BOOL - TRUE we return if this is final call (dwInternetStatus == INTERNET_STATUS_HANDLE_CLOSING) and FALSE otherwise.

so begin solution

// helper for get complex c++ names for use in asm code
#if 0
#define ASM_FUNCTION {__pragma(message(__FUNCDNAME__" proc\r\n" __FUNCDNAME__ " endp"))}
#define CPP_FUNCTION __pragma(message("extern " __FUNCDNAME__ " : PROC ; "  __FUNCSIG__))
#else
#define ASM_FUNCTION
#define CPP_FUNCTION
#endif

base class skeleton:

class REQUEST_CONTEXT 
{
    // ...
    static void WINAPI _StatusCallback(
        __in  HINTERNET hRequest,
        __in  DWORD_PTR dwContext,
        __in  DWORD dwInternetStatus,
        __in  LPVOID lpvStatusInformation,
        __in  DWORD dwStatusInformationLength
        ) ASM_FUNCTION;

    BOOL StatusCallback(
        __in  HINTERNET hRequest,
        __in  DWORD dwInternetStatus,
        __in  LPVOID lpvStatusInformation,
        __in  DWORD dwStatusInformationLength
        );

    ULONG SendRequest(PCWSTR pwszObjectName);

    void OnRequestComplete(HINTERNET hRequest, INTERNET_ASYNC_RESULT* pres);
};

StatusCallback implementation:

BOOL REQUEST_CONTEXT::StatusCallback(
                    __in  HINTERNET hRequest,
                    __in  DWORD dwInternetStatus,
                    __in  LPVOID lpvStatusInformation,
                    __in  DWORD dwStatusInformationLength
                    )
{
    CPP_FUNCTION;

    switch (dwInternetStatus)
    {
    case INTERNET_STATUS_HANDLE_CLOSING:
        Release();
        return TRUE;
    case INTERNET_STATUS_REQUEST_COMPLETE:
        OnRequestComplete(hRequest, (INTERNET_ASYNC_RESULT*)lpvStatusInformation);
        break;
    }

    return FALSE;
}

how we register callback (in my code inside SendRequest for handle returned from HttpOpenRequestW )

ULONG SendRequest(PCWSTR pwszObjectName)
{
    // ... some code ...
    AddRef();

    HMODULE hmod;
    if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (PCWSTR)&__ImageBase, &hmod))
    {
        if (HINTERNET hRequest = HttpOpenRequestW(..., (DWORD_PTR)this))
        {
            set_handle(hRequest);

            if (INTERNET_INVALID_STATUS_CALLBACK != InternetSetStatusCallback(hRequest, _StatusCallback))
            {
                INTERNET_ASYNC_RESULT ar;
                ar.dwResult = HttpSendRequestW(hRequest, 0, 0, 0, 0);
                ar.dwError = ar.dwResult ? NOERROR : GetLastError();

                if (ar.dwError != ERROR_IO_PENDING)
                {
                    OnRequestComplete(hRequest, &ar);
                }

                return NOERROR;
            }
        }

        FreeLibrary(hmod);
    }

    Release();

    return GetLastError();
}

so here we call GetModuleHandleExW for add reference to DLL before set callback. if set callback fail - we call FreeLibrary for deference DLL. note that here call FreeLibrary safe and correct, because the one who called SendRequest must have reference for DLL - DLL of course must not be unloaded during this call. so when we call FreeLibrary from this function - we guaranteed to release not the last reference to DLL (we free reference from GetModuleHandleExW call, but exist additional reference on DLL during this function executing. the caller (direct or indirect) of this function care about this). in place dwContext we pass this pointer.

so now final part - _StatusCallback implementation.

for x64 (ml64 /c /Cp $(InputFileName) -> $(InputName).obj )

extern __imp_FreeLibrary:QWORD
extern __ImageBase:BYTE

; void REQUEST_CONTEXT::StatusCallback(void *,unsigned long,void *,unsigned long)
extern ?StatusCallback@REQUEST_CONTEXT@@QEAAHPEAXK0K@Z : PROC 

.CODE 

?_StatusCallback@REQUEST_CONTEXT@@SAXPEAX_KK0K@Z proc
    xchg rcx,rdx
    mov rax,[rsp + 28h]
    sub rsp,38h
    mov [rsp + 20h],rax
    call ?StatusCallback@REQUEST_CONTEXT@@QEAAHPEAXK0K@Z
    add rsp,38h
    test eax,eax
    jnz @@1
    ret
@@1:
    lea rcx, __ImageBase
    jmp __imp_FreeLibrary
?_StatusCallback@REQUEST_CONTEXT@@SAXPEAX_KK0K@Z endp

end

for x86 (ml /c /Cp $(InputFileName) -> $(InputName).obj )

.MODEL FLAT

extern __imp__FreeLibrary@4:DWORD
extern ___ImageBase:BYTE

; int __thiscall REQUEST_CONTEXT::StatusCallback(void *,unsigned long,void *,unsigned long)
extern ?StatusCallback@REQUEST_CONTEXT@@QAEHPAXK0K@Z : PROC 

.CODE

?_StatusCallback@REQUEST_CONTEXT@@SGXPAXKK0K@Z proc
    mov ecx,[esp + 8]
    push [esp + 20]
    push [esp + 20]
    push [esp + 20]
    push [esp + 16]
    call ?StatusCallback@REQUEST_CONTEXT@@QAEHPAXK0K@Z
    test eax,eax
    jnz @@1
    ret 4*5
@@1:
    mov eax,[esp]
    lea edx, ___ImageBase
    add esp,4*4
    mov [esp],eax
    mov [esp + 4],edx
    jmp __imp__FreeLibrary@4
?_StatusCallback@REQUEST_CONTEXT@@SGXPAXKK0K@Z endp

end
RbMm
  • 31,280
  • 3
  • 35
  • 56
-1

Most likely you should wait/cancel all pending async calls. Leaving them alive and unloading the code is not nice (correct) at all.

cprogrammer
  • 5,503
  • 3
  • 36
  • 56