Как безопасно предотвратить вызов функции статуса в Wininet? - PullRequest
1 голос
/ 26 апреля 2019

Мы используем WinInet внутри DLL для выполнения асинхронных сетевых вызовов.

Когда приложение закрывается, мы удаляем зарегистрированные функции обратного вызова для ожидающих запросов, используя InetSetStatusCallback(connect_handle, NULL);.Однако иногда функция обратного вызова по-прежнему вызывается после выгрузки DLL, что приводит к сбоям приложения.

Симптомы в точности аналогичны последним часто задаваемым вопросам этого блога: Вопросы WinHTTP: о обратных вызовах

Я пытаюсь найти способ безопасного удаления функций обратного вызова длявсе ожидающие запросы, чтобы они не вызывались WinInet после выгрузки DLL.

Ответы [ 2 ]

1 голос
/ 28 апреля 2019

после того, как мы передадим указатель на функцию обратного вызова - модуль, который, конечно же, содержит эту функцию обратного вызова, не должен выгружаться, пока не будет вызван обратный вызов. это, конечно, не проблема, если функция обратного вызова была в модуле EXE , который никогда не будет выгружен. но в случае DLL нам нужно добавить ссылку на DLL перед установкой обратного вызова для предотвращения выгрузки DLL . это легко сделать с помощью функции GetModuleHandleExW

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

хорошо, но как не позволить DLL быть выгруженным? нам нужно разыменовать его (звоните FreeLibrary), когда наш обратный вызов больше не будет вызываться. когда это будет?

InternetSetStatusCallback устанавливает функцию обратного вызова для некоторого дескриптора. эта ручка, конечно, должна закрываться через InternetCloseHandle и:

Можно безопасно InternetCloseHandle в обратном вызове для ручки быть закрытым. Если для дескриптора зарегистрирован статусный обратный вызов закрывается, и дескриптор был создан с ненулевым контекстом значение, будет выполнен INTERNET_STATUS_HANDLE_CLOSING обратный вызов. это индикацией будет последний обратный вызов , сделанный из дескриптора и указывающий что ручка разрушается.

Если асинхронные запросы ожидают обработки для дескриптора или любого из его дочерние ручки, ручка не может быть немедленно закрыта, но будет аннулированной. Любые новые запросы, выполненные с использованием дескриптора, вернутся с ERROR_INVALID_HANDLE уведомлением. Асинхронные запросы завершится с INTERNET_STATUS_REQUEST_COMPLETE . Заявки должны будьте готовы получить любую INTERNET_STATUS_REQUEST_COMPLETE показания на ручке перед финалом 1046 * INTERNET_STATUS_HANDLE_CLOSING указывается, что указывает что ручка полностью закрыта.

so INTERNET_STATUS_HANDLE_CLOSING - это будет последний обратный вызов, сделанный из дескриптора. финал звонок. именно в это время нам нужен модуль пересылки DLL - обратный вызов больше не будет вызываться. ссылка не нужна больше.

ки. когда ясно. но как ? мы не можем напрямую вызывать FreeLibrary с этой точки, потому что если DLL будет жить по этой последней ссылке - она ​​будет выгружена при вызове FreeLibrary, а когда мы вернемся - мы вернемся в пространство emply и произойдем сбой.

мы тоже не можем вызвать FreeLibraryAndExitThread - потому что мы в произвольном потоке. однако существует не правильно , но работает над практическим решением:

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

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

мы можем создать новый поток самостоятельно, и из этого потока вызовем FreeLibraryAndExitThread (здесь этот вызов правильный). но как я говорю - это теоретически не правильное решение. здесь существуют гонка - вызов FreeLibraryAndExitThread может выполняться и выгружать DLL до того, как мы вернемся из CreateThread вызова. в результате мы терпим крах сразу после CreateThread (мы пытаемся выполнить уже выгруженный код). конечно это вряд ли, но ..

мы можем вставить Sleep (некоторая задержка) перед вызовом FreeLibraryAndExitThread, но в любом случае - теоретически может быть гонка для любой задержки. Опять какое конкретное значение для задержки выбрать ?! и DLL будет отложено на выгрузку на это время. не правильно и не приятно. несмотря на то, что это относительно просто «решение», я не буду его использовать.

будет правильным решением - позвонить (точнее, jump ) на FreeLibrary без возврата на место, откуда будет этот прыжок. но нужно вернуться на место, откуда был вызван обратный вызов. это невозможно в c / c ++ , но возможно в asm коде. конечно, для этого требуется отдельный файл asm для каждой целевой платформы ( x86, x64 как минимум)

так что правильное и элегантное решение следующее: функция обратного вызова INTERNET_STATUS_CALLBACK в любом случае должна быть связана с некоторым классом, где мы храним контекст дескриптора. это должна быть статическая функция в классе и DWORD_PTR dwContext - значение контекста, определяемое приложением, связанное с hInternet , должно указывать на экземпляр класса (на самом деле этот указатель ). обычно это делается следующим образом:

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
        );
};

здесь мы должны внедрить _StatusCallback в asm код для вызова FreeLibrary здесь. и chage возвращает значение StatusCallback от void до BOOL - TRUE мы возвращаемся, если это последний вызов (dwInternetStatus == INTERNET_STATUS_HANDLE_CLOSING) и FALSE иначе.

так что начинайте решение

// 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

скелет базового класса:

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 реализация:

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;
}

как мы регистрируем обратный вызов (в моем коде внутри SendRequest для дескриптора, возвращенного с 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();
}

поэтому здесь мы вызываем GetModuleHandleExW для добавления ссылки на DLL перед установкой обратного вызова. если установлен обратный вызов - мы вызываем FreeLibrary для почтения DLL . обратите внимание, что здесь вызов FreeLibrary безопасен и корректен, поскольку тот, кто вызвал SendRequest, должен иметь ссылку на DLL - DLL , конечно, не должен выгружаться во время этого вызова. поэтому, когда мы вызываем FreeLibrary из этой функции - мы гарантируем , чтобы освободить не последнюю ссылку на DLL (мы освобождаем ссылку от вызова GetModuleHandleExW, но существуют дополнительные ссылки на DLL во время выполнения этой функции. Вызывающий (прямой или косвенный) вызывающий этой функции заботится об этом). на месте dwContext мы передаем этот указатель.

итак, теперь последняя часть - _StatusCallback реализация.

для 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

для x86 ( мл / 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
0 голосов
/ 26 апреля 2019

Скорее всего, вам следует подождать / отменить все ожидающие асинхронные вызовы.Оставлять их живыми и выгружать код совсем нехорошо (правильно).

...