1120 Alameda Orquidea, Atibaia, SP,  BRA

fernando@driverentry.com.br

CLEANUP e CLOSE

Já escrevi um post que descreve os passos de como criar um driver bem simples. Este driver simplesmente mantém uma lista de strings que recebe via IRP’s de escrita e as devolve nas IRP’s de leitura. Muito bem, se você ainda não sabe o que é uma IRP, um outro post tenta explicar o que são as IRP’s e até descreve os passos necessários para utilizar o IRP Tracker para observar as IRP’s indo e vindo. Se fizermos um pequeno teste utilizando o IRP Tracker sobre o driver de exemplo que acabei de comentar, teremos uma saída semelhante a figura abaixo.


Podemos ver as todas as IRP’s que nosso device recebeu desde a IRP_MJ_CREATE até a IRP_MJ_CLOSE. Entre estas IRP’s notamos que algumas delas não foram completadas com sucesso. Como nenhuma rotina foi designada para tratar IRP_MJ_CLEANUP, estas IRP’s são terminam com STATUS_INVALID_DEVICE_REQUEST. Neste post vou comentar um pouco sobre a esta IRP e como seu device interage com os processos que obtiveram um handle para ele.

Para ver detalhes da IRP, dê um duplo clique sobre a linha da IRP no IRP Tracker para que a janela abaixo seja exibida. Nessa janela é possível ver, entre outros detalhes, o FileObject utilizado na IRP. Isso nos será útil neste post durante os testes.


Quando o IRP_MJ_CLEANUP ocorre?

O Object Manager envia uma IRP de CLEANUP a fim de notificar o driver de que o último handle para determinado FileObject foi fechado. Como alguns de vocês devem saber, quando uma aplicação usa o CreateFile para obter um handle para nosso device, isso resulta na criação de um FileObject. As operações subseqüentes utilizando este handle estarão vinculadas a este FileObject. Mais detalhes sobre isso neste post.

Um FileObject não tem uma relação direta com um handle. Um handle pode ser duplicado ou mesmo herdado do processo pai na criação de um novo processo. O resultado destas ações é ter vários handles que serão traduzidos para o mesmo FileObject. Desta forma, nem sempre quando uma aplicação chama a função CloseHandle, uma IRP de CLEANUP ou CLOSE é enviada ao driver.

Para acompanhar os passos abaixo, o código fonte do programa de exemplo está disponível para download ao final deste post. Este programa utiliza o driver de exemplo construído em outro post, o código fonte do driver pode ser baixado daqui. A partir dos fontes você pode compilar e instalar o driver de teste. Se você não sabe como fazer isso, este post pode te ajudar. Depois de instalar o driver de teste, seria interessante poder executar as chamadas abaixo com a ajuda de um depurador, e assim, poder observar os resultados no IRP Tracker a cada linha executada.

É importante ler os comentários do fonte abaixo. Fiquei com preguiça de duplicar essas informações aqui no post.

/****
***     main
**
**      Espero que todos saibam que este é o ponto
**      de entrada de uma aplicação. Caso contrário,
**      você pode estar se preciptando em ler um blog
**      de driver para Windows.
*/
int main(int argc, char* argv[])
{
    HANDLE  h1, h3, h3;
    CHAR    szBuffer[100];
    DWORD   dwBytes;
 
    //-f--> Aqui estamos abrindo o primeiro handle
    //      para o nosso device. Este é o passo 1
    //      no IRP Tracker. Verifique o valor do
    //      FileObject para poder comparar em operações
    //      futuras.
    h1 = CreateFile("\\\\.\\EchoDevice",
                    GENERIC_ALL,
                    0,
                    NULL,
                    OPEN_EXISTING,
                    0,
                    NULL);
 
    //-f--> Aqui estamos abrindo o segundo handle
    //      para o nosso device. Este é o passo 2
    //      no IRP Tracker. Verifique o valor do
    //      FileObject para poder comparar em operações
    //      futuras.
    h3 = CreateFile("\\\\.\\EchoDevice",
                    GENERIC_ALL,
                    0,
                    NULL,
                    OPEN_EXISTING,
                    0,
                    NULL);
 
    //-f--> Aqui lançaremos uma IRP_MJ_READ para o
    //      primeiro FileObject obtido. Passo 3 no
    //      IRP Tracker. Note que o primeiro FileObject
    //      é utilizado nesta IRP.
    ReadFile(h1,
             szBuffer,
             sizeof(szBuffer),
             &dwBytes,
             NULL);
 
    //-f--> Aqui lançaremos uma IRP_MJ_READ para o
    //      segundo FileObject obtido. Passo 4 no
    //      IRP Tracker. Note que o segundo FileObject
    //      é utilizado nesta IRP.
    ReadFile(h3,
             szBuffer,
             sizeof(szBuffer),
             &dwBytes,
             NULL);
 
    //-f--> A duplicação de handle não é notificada
    //      ao driver. Apenas o Object Manager sabe
    //      disso. Não existe nenhum passo correspondente
    //      no IRP Tracker.
    DuplicateHandle(GetCurrentProcess(),
                    h1,
                    GetCurrentProcess(),
                    &h3,
                    0,
                    FALSE,
                    DUPLICATE_SAME_ACCESS);
 
    //-f--> Como o terceiro handle foi obtido atravéz de
    //      uma duplicação do primeiro, o FileObject
    //      correspondente é o mesmo do primeiro handle.
    //      Passo 5 no IRP Tracker. Note que o primeiro
    //      FileObject é utilizado nesta IRP.
    ReadFile(h3,
             szBuffer,
             sizeof(szBuffer),
             &dwBytes,
             NULL);
 
    //-f--> Como temos dois handles para o primeiro FileObject,
    //      fechar um deles não implicará em nenhuma notificação
    //      ao nosso device. Não existe passo correspondente no
    //      IRP Tracker.
    CloseHandle(h1);
 
    //-f--> Este handle não foi duplicado, assim, quando o fecharmos,
    //      uma IRP_MJ_CLEANUP seguido de uma IRP_MJ_CLOSE serão
    //      enviadas ao driver. Passo 6 no IRP Tracker.
    CloseHandle(h3);
 
    //-f--> Fechando este handle, teremos o mesmo comportamento
    //      observado no fechamento do handle h3. Do ponto de
    //      vista do driver, o primeiro FileObject será destruído
    //      agora. Passo 7 no IRP Tracker.
    CloseHandle(h3);
 
    //-f--> E todos viveram felizes para sempre.
    return 0;
}

Obter um handle para um objeto nos assegura que este objeto será válido enquanto não fecharmos o handle. Um objeto só pode ser destruído pelo sistema depois que todos os handles para ele forem fechados. Para isso, o Object Manager mantém dois contadores, o ProcessHandleCount e o SystemHandleCount. O primeiro deles mantém a quantidade de handles abertos para um objeto em um determinado processo. O outro mantém a soma de todos os ProcessHandleCount para o objeto no sistema. Estes contadores são decrementados à medida que os handles são fechados. Quando chegam a zero, uma IRP_MJ_CLEANUP é gerada.

Normalmente a IRP_MJ_CLEANUP nos serve como evento para cancelar qualquer operação assincrona relativa ao FileObject que está sendo finalizado. IRP’s são ligadas às threads que às lançaram e qualquer IRP assincrona deveria ser devidamente cancelada neste momento.

Então para quê serve o IRP_MJ_CLOSE?

Além dos contadores de referência acima citados, existe também o ObjectReferenceCount, que além de ser incrementado quando um novo handle é obtido, também é incrementado quando uma referência de kernel é feita utilizando funções do tipo ObReferenceObject por exemplo. Estas chamadas incrementam o ObjectReferenceCount sem que um novo handle seja gerado. Para quem conhece COM, esta chamada tem o comportamento semelhante ao AddRef. Isso permite que o objeto se mantenha válido para o kernel, mesmo depois que todos os handles para ele foram destruídos. Enfim, quando este contador chegar a zero, então a IRP_MJ_CLOSE é enviada.

Um driver pode associar estruturas de dados a um determinado FileObject utilizando os pointeiros FsContext e FsContext2 conforme eu já comentei neste outro post. Estas estruturas só poderão ser desalocadas quando recebermos o IRP_MJ_CLOSE.

Operações após o IRP_MJ_CLEANUP

Quando recebemos uma IRP_MJ_CLEANUP, não significa que o fim está próximo. Outros componentes do kernel podem lançar novas IRP’s de leitura ou escrita mesmo depois deste evento. Sem falar das IRP_MJ_INTERNAL_DEVICE_CONTROL que podem ser trocadas entre drivers.

Um cenário muito comum no desenvolvimento de File Systems é justamente quando uma refêrencia de um FileObject que é mantida pelo Cache Manager. Mesmo quando todos os handles forem fechados pelas aplicações, o sistema ainda retém esta refêrencia antecipando que alguma aplicação pode querer abrir o mesmo arquivo novamente. O Cache Manager possui system threads que realizam a então chamada “Escrita retardada”. Este recurso concentra várias escritas a um arquivo em uma única operação posterior a fim de diminuir o número de acessos a disco, assim ganhando performance. É muito comum tais escritas serem realizadas depois que o arquivo teve todos os seus handles fechados por aplicações, e assim temos escritas depois do IRP_MJ_CLEANUP.

No final de tudo isso, espero mais uma vez ter mais ajudado que atrapalhado. Assuntos referentes a Cache Manager, Memory Manager e File Systems não são muito triviais, mas são interessantes o suficiente para ler a respeito e entender o que o sistema faz antes de falar que tudo é uma porcaria.

Até a próxima!

TestCleanup.zip

6 Responses

  1. Fernando, legal o post. O que acontece as vezes e’ que o FileObject se mantem vivo mesmo com os RefCounts em 0. Pelo que andei lendo na OSR e na documentacao da Microsoft, existe um PointerRefCount que ainda pode ser usado por algum outro processamento dentro de algum driver. O que eu percebi e’ que mesmo os reference counters em zero (object/handle) alguem ainda joga as IRP diretamente, como MJ_SET_SECURITY, sem passar pelo MJ_CREATE.

    Eu vou investigar um pouco mais, mas de qquer forma obrigado pelo post!

  2. Alias, parabens, pq nao e’ todo mundo que entende de Cache Manager :). Abracos, e qdo vieres pra Porto Alegre fala com o Strauss pra gente tomar um chopp!

    1. Olá Gustavo,

      Por favor, me mantenha informado desde caso em específico. São esses problemas cabeludos que nos ajudam a entender o que se passa na verdade (conosco ou com o sistema). Vamos aprender juntos com essa oportunidade.

      Forte abraço,
      Fernando.

      1. Gustavo,

        Por falar em FileObjects sem seus respectivos creates. Fiquei aqui me lembrando de quando eu enfrentava esse tipo de problema com um filtro de criptografia em tempo real. Existem APIs que geram FileObjects a drivers de terceiros, tais como IoCreateStreamFileObject e compania, e que os filtros não ficam sabendo desta aquisição. Resultado, operações com FileObjects que não vieram de IRP_MJ_CREATE.

        Para saber de qual arquivo estamos falando, podemos criar uma lista ligada abastecida de maneira não trivial, mas que é descrito por este artigo da OSR, ou ainda podemos simplesmente perguntar ao sistema (se estivermos em um nível menor que DISPATCH_LEVEL) utilizando a ObQueryNameString.

        Vale lembrar que o ObQueryNameString passa pela stack de devices até chegar ao driver de file system para obter esta informação. Por isso se você estiver em uma chamada de file system, isso irá gerar um deadlock. Para fugir disso você deverá lancar uma IRP_MJ_QUERY_INFORMATION para o device no qual você está atachado.

        Se precisar de um exemplo, isso é feito por uma lib que vem junto com o exemplo SFilter do IFS Kit. Procure o módulo NameLookup.c.

        Have fun,
        Fernando.

  3. Fernando,

    Sim. Ja passei muito por isso. Gracas a Deus que a Microsoft fez o MiniFilter que ajuda a nao ter mais que fazer essas coisas. Agora vc pode dar um FltGetFileNameInformation e ele faz o resto. O interessante e’ que essa funcao ate testa se existe um TopLevelIrp (IoGetTopLevelIrp), pra evitar as deadlocks. O interessante e’ que ele mantem esse cache enquanto a estrutura for valida (ele usa o MJ_CREATE/CLEAN_UP pra manter o nome no cache). E quer mais uma noticia? Qdo chega o MJ_SET_SECURITY, ele da um CACHE_MISSED neste lookup. E ate onde eu percebi, sim, parece ser uma chamada do CacheManager, pois existe uma TopLevelIrp pendurada. Como eu sei que essa IRP nao e’ minha, eu uso a FltGetNameInformaionUnsafe. E’ unsafe pq nao testa o IrpTopLevel, ele simplesmente monta e joga pra frente. Inclusive eu andei testando limpando a toplevel, chamando a “safe” function, depois colocando a irp de volta. Ate funcionou, mas eu prefiro continuar usando a Unsafe ate identificar direitinho o que se passa com o set security.

Leave a Reply

Your email address will not be published.