Por que FindNextUrlCacheEntry() do WinInet está retornando ERROR_FILE_NOT_FOUND=2 em vez de ERROR_NO_MORE_ITEMS=295

Por que FindNextUrlCacheEntry() do WinInet está retornando ERROR_FILE_NOT_FOUND=2 em vez de ERROR_NO_MORE_ITEMS=295

Descrição

De acordo com o título, o WinInet FindNextUrlCacheEntry()resulta em um código de erro não documentado de 2 (ERROR_FILE_NOT_FOUND) em vez de ERROR_NO_MORE_ITEMS=295. Alguém pode explicar por que ou sugerir outras maneiras de investigar?

Ambiente

  • Janelas 10
  • Ou seja, 11
  • VisualStudio 2006 (mas pode ser reconstruído no VS2010)

Sinopse

Detalhes mais abaixo, mas o cerne da questão é que dentro de um loop, este fragmento de código:

/ Get the next entry
if (!FindNextUrlCacheEntry(hCache, *plpCacheEntry, &bufSize)) 
{
    DWORD d = GetLastError();
    ATLTRACE(_T("getNextEntry - FindNExtUrlCacheEntry returned %d: %s ==> %s"), d, (*plpCacheEntry)->lpszSourceUrlName, (*plpCacheEntry)->lpszLocalFileName);
    // Are we done?
            // <.... more error handling logic ....>

resulta nesta saída de rastreamento:

>>[12536] getNextEntry - FindNExtUrlCacheEntry returned 2: (null) ==> (null)

Se hackearmos nosso código para tratar essa condição de forma semelhante a ERROR_NO_MORE_ITEMS, as coisas funcionarão conforme o esperado. Se ignorarmos isso e continuarmos tentando obter mais entradas, obteremos um loop infinito.

Visão geral

Observação: para esta postagem, estou tratando o código de erro 2 como sinônimo de ERROR_FILE_NOT_FOUND, mas como é um código não documentado para a chamada de API em questão, o 2 pode na verdade ser de outra coisa.

Temos um controle C++ ActiveX do VisualStudio 2006 que roda no IE. Seu objetivo é fazer uma limpeza parcial do cache do IE para arquivos que correspondam ao nome do host do nosso servidor de aplicativos. (Esta ação é controlada comparando os números de versão entre um valor de cookie de solicitação do servidor com o último armazenado no cliente. A exclusão ocorre apenas quando uma incompatibilidade de versão é detectada.)

O controle ActiveX tem funcionado bem por muitos anos. Mas recentemente comecei a falhar conforme mencionado acima. Não sabemos se é devido a uma atualização do Windows KB ou a iniciativas de segurança recentes ou a qualquer outra coisa. Nosso código lida adequadamente com o caso ERROR_NO_MORE_ITEMS e encerra seu trabalho. Mas trata o 2 como um erro e o aplicativo não continua.

A documentação para FindNextUrlCacheEntry() está aqui: https://docs.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-findnexturlcacheentrya A chamada retorna verdadeiro/falso e define o código de erro como ERROR_NO_MORE_ITEMS ou ERROR_INSUFFICIENT_BUFFER.

Nossa lógica:

Posso fornecer detalhes, mas hesito em postar código proprietário. Resumidamente, a lógica segue os passos recomendados para isso. Um exemplo está aquihttps://support.microsoft.com/en-us/help/815718/how-to-clear-the-cache-when-your-application-hosts-a-webbrowser-controexceto que não lidamos com "grupos de cache". Você pode rolar o código que começa após o comentário

// Start to delete URLs that do not belong to any group.

De qualquer forma, nossa lógica é a seguinte:

  1. Obtenha um identificador para o cache por meio de FindFirstUrlCacheEntry(), realocando a memória conforme necessário para o buffer de entrada e verificando GetNextError()o sucesso
  2. Faça um loop sobre esta e as entradas subsequentes via FindNextUrlCacheEntry(), realocando a memória conforme necessário para o buffer de entrada, verificando o valor de retorno verdadeiro/falso
  3. Se verdadeiro: chame DeleteUrlCacheEntry()se os nomes de host corresponderem e a entrada não for do tipo: COOKIE_CACHE_ENTRYou HISTORY_CACHE_ENTRY. Caso contrário, pule a entrada
  4. Se for falso: verificamos o valor de GetNextError()e fazemos o seguinte:
  5. ERROR_NO_MORE_ITEMS(=259): encerre o loop e saia do ponto de entrada ActiveX com sucesso
  6. Defina o código de retorno do ponto de entrada do controle ActiveX para o código de erro (2 no nosso caso). O chamador JavaScript testa 0 e interrompe o usuário do erro em alert() com instruções para ligar para o suporte

Gambiarra:

A única solução alternativa para permitir que o aplicativo funcione é fazer com que o usuário exclua todo o cache do IE. Neste ponto, FindFirstUrlCacheEntry() não encontra nada e o loop de exclusão nunca é inserido. (Na verdade, isso é especulação, já que não podemos testar este caso explicitamente. Veja as notas de depuração abaixo.) Em qualquer caso, o erro não ocorre

Esforços de depuração:

Podemos forçar a incompatibilidade de versão do cookie definindo um ponto de interrupção no código JavaScript e modificando manualmente a string da versão antes que a comparação seja feita. Isso "engana" o código, fazendo-o chamar o ponto de entrada de exclusão do controle ActiveX.

Somos extremamente limitados aqui. Temos apenas acesso ocasional a um único PC onde o problema se reproduz. Os esforços para anexar remotamente um depurador VisualStudio via msvsmon.exe nos permitem ver a saída do ATLTRACE (também conhecido como printf), mas não podemos fazer com que os pontos de interrupção sejam acionados. Em vez disso, estamos reduzidos a ATLTRACE() e DebugView da Sysinternals.

O que vemos é que ERROR_FILE_NOT_FOUND age como ERROR_NO_MORE_FILES. Ou seja, se hackearmos o código C++ para encerrar o loop find-next/delete em qualquer uma das condições, ele sempre excluirá alguns arquivos esperados e será concluído sem erros. No entanto, é difícil saber se realmente não há mais arquivos ou não: interrogar manualmente o cache do IE é, na melhor das hipóteses, complicado, e as medidas de segurança em nossos PCs tornam isso ainda mais complicado. Por exemplo, subpastas de C:\Users\<usr>\AppData\Local\Microsoft\Windows\INetCache\Low\IE\...não aparecem em um diretório cmd ou power-shell ou comando ls. Ainda assim, parece que quanto mais páginas do nosso aplicativo acessamos no cliente, mais arquivos são limpos pelo controle ActiveX antes de terminar. Um caso funcional disso se parece com este no DebugView:

[12536] DeleteCacheEntries(10.3.31.28:9080) BEGIN: hr=0 pErrCode=0
[12536] deleteCacheEntriesFor(10.3.31.28:9080) BEGIN
[12536] deleteCacheEntriesFor(10.3.31.28:9080) - getFirstEntry returned 0
[12536] deleteCacheEntryIf - (lpdErrCd now=0) deleting: http://10.3.31.28:9080/images/our.jpg ==> C:\Users\<user>\AppData\Local\Microsoft\Windows\INetCache\Low\IE\Q6HU0E35\our[1].jpg
....<lots more "deleting" ...>
[12536] getNextEntry - FindNExtUrlCacheEntry returned 2: (null) ==> (null)
[12536] getNextEntry - ERROR=2: (null) ==> (null)
[12536] deleteCacheEntriesFor(10.3.31.28:9080) - getNextEntry returned 2. Breaking out of loop!
[12536] deleteCacheEntriesFor(10.3.31.28:9080) - Complete. Closing URL cache handle current. errCd=0
[12536] deleteCacheEntriesFor(10.3.31.28:9080) - Done. Returning 0

Também tentamos um hack que assume que ERROR_FILE_NOT_FOUND significa simplesmente que há uma entrada de cache lá, mas de alguma forma estamos impedidos de vê-la. Ou seja, em vez de sair do loop, continuamos na esperança de encontrar outras entradas "localizáveis" para possivelmente excluir. Mas neste caso, o loop nunca termina. Aqui “nunca” significa vários minutos e mais de 2 milhões de casos de:

[5712] getNextEntry - FindNExtUrlCacheEntry returned 2: (null) ==> (null)
[5712] deleteCacheEntriesFor(10.3.31.28:9080) - getNextEntry returned 2. CONTINUING!
...<forever>...

Especulação:

Aqui estão algumas especulações reconhecidamente fracas sobre o que pode estar acontecendo

  • Algum software de segurança ou patch da KB do Windows está intervindo na execução do C++ e fazendo com que SetLastError(2) seja chamado, destruindo os valores documentados que estão sendo definidos por FindNextUrlCacheEntry()
  • Nosso código está vinculado a versões mais antigas do SDK do VisualStudio e estamos enfrentando algumas distorções de código
  • Pensamos em corrupção de pilha ou registro, mas o comportamento parece reproduzível demais para isso. Ainda...?
  • Realmente existe algo para FindNextUrlCacheEntry(), mas a segurança está impedindo que ele fique visível de alguma forma e seu "cursor" no cache nunca avança

Responder1

Acontece que isso se deve a um defeito na API Microsoft WinINet. Detalhes completos podem ser encontrados no tópico no fórum devcommunity/visualstudioaqui. (A resposta está em minhas postagens finais na parte inferior.)

informação relacionada