Base de pruebas de casos anómalos en Windows con Application Verifier

· Actualizado el: · · Desarrollo en Windows, Investigación de fallos, Cámara industrial, Application Verifier, Pruebas de casos anómalos, Fuga de identificadores

Application Verifier es una herramienta muy útil cuando se quiere sacar a la luz de antemano las anomalías que ocurren en el código nativo de Windows o en el límite Win32. En particular, cuando se quieren probar anomalías de handle, corrupción de heap o failure paths en situaciones de recursos bajos, permite exponer bastante pronto problemas que las pruebas del recorrido normal por sí solas no dejan ver.

En la primera parte, Investigación de caídas en cámaras industriales por funcionamiento prolongado: edición de fuga de identificadores, repasamos un caso en el que investigamos una aplicación de control que se caía tras un funcionamiento prolongado y descubrimos que la causa era una fuga de identificadores. Pero reforzar solo el registro es apenas la mitad del camino. Lo que realmente se quiere es poder comprobar de antemano si, en caso de que en el futuro se produzca de forma imprevista una fuga de memoria, una fuga de identificadores, un fallo a mitad de camino o una liberación olvidada por un error de programación, el sistema quedará en un estado en el que se pueda saber qué ocurrió.

Para eso usamos Application Verifier. Es una herramienta que permite añadir comprobaciones y fault injection en tiempo de ejecución a los procesos que funcionan en el código nativo de Windows o en el límite Win32. Lo que resulta especialmente útil en la práctica es que permite provocar de antemano una situación parecida a la falta de memoria o de recursos, sin necesidad de agotar realmente la memoria de la máquina.

En esta segunda parte repasamos qué es Application Verifier, qué se puede hacer con él y cómo incorporarlo a una base de pruebas de casos anómalos, en el contexto de una aplicación de control de cámaras industriales.

Índice

  1. Conclusión breve (en una frase)
  2. Qué es Application Verifier
    • 2.1. En una frase, qué es
    • 2.2. En qué situaciones resulta eficaz
    • 2.3. Qué ventajas aporta
    • 2.4. Cómo conseguirlo y habilitarlo en el entorno local
  3. Qué se puede hacer con Application Verifier
    • 3.1. Basics: Handles, Heaps, Locks, Memory, TLS, etc.
    • 3.2. Low Resource Simulation: adelantar la falta de memoria o de recursos
    • 3.3. Page Heap y el depurador
    • 3.4. !avrf, !htrace y los registros
  4. Por qué se introdujo esta vez
    • 4.1. El objetivo no es solo “encontrar un bug”
    • 4.2. Provocar fenómenos parecidos a la falta de memoria
    • 4.3. Comprobar si se puede seguir el rastro cuando ocurre una anomalía de handle
  5. Cómo provocar fenómenos parecidos a la falta de memoria o de recursos
    • 5.1. La idea detrás de Low Resource Simulation
    • 5.2. Qué se puede hacer fallar
    • 5.3. Cómo aplicarlo en la práctica
  6. Cómo observar una anomalía de handle
    • 6.1. La comprobación Handles
    • 6.2. Ver la pila de open / close con !htrace
    • 6.3. Cómo combinarlo con el registro propio
  7. Cómo construir la base de pruebas de casos anómalos
    • 7.1. Centrar la unidad de ejecución en un harness
    • 7.2. Separar el menú de pruebas
    • 7.3. Qué recopilar
    • 7.4. Criterios de aprobación
    • 7.5. Puntos de atención
  8. Guía rápida de cuándo usar qué
  9. Resumen
  10. Referencias

1. Conclusión breve (en una frase)

  • Application Verifier es una herramienta que facilita detectar en tiempo de ejecución los usos indebidos que ocurren en el límite de código no administrado / nativo de Windows
  • Lo útil no es solo “encontrar bugs”, sino poder provocar de antemano casos anómalos que normalmente no aparecen
  • Con Handles se detecta el uso de invalid handle, con Heaps se hace visible la corrupción de heap, y con Low Resource Simulation se puede hacer fault injection de situaciones parecidas a la falta de memoria o de recursos
  • Delegar por completo en Application Verifier la investigación de leaks de un EXE que permanece en ejecución durante mucho tiempo es una mala idea; lo realista es combinarlo con el registro propio de Handle Count y del ciclo de vida de los recursos
  • En la base de pruebas de casos anómalos, resulta más legible separar la ejecución normal del verifier de la ejecución con fault injection
  • Incluso cuando se quiere probar un DLL, el objetivo sobre el que se habilita Application Verifier es el EXE de prueba que realmente lo ejecuta

En resumen, Application Verifier es una herramienta que saca a la superficie los “bugs desagradables” que se esconden en el entorno nativo y Win32 de Windows. En particular, en un mundo donde se mezclan de forma habitual SDK nativos, P/Invoke y API de Win32, como ocurre en las aplicaciones de control de equipos, encaja bastante bien.

2. Qué es Application Verifier

2.1. En una frase, qué es

Application Verifier es una herramienta de verificación en tiempo de ejecución para aplicaciones en modo usuario de Windows. Supervisa cómo la aplicación en ejecución utiliza las API del sistema operativo y maneja los recursos, y puede detectar usos sospechosos o inyectar fallos de forma intencionada.

A diferencia del “análisis estático” o las “pruebas unitarias”, es una herramienta que muestra cómo se rompe el código cuando realmente se ejecuta esa ruta. Por eso resulta adecuada para sacar a la luz failure paths que no se ven en las pruebas funcionales habituales.

Flujo de trabajo con Application VerifierDiagrama que muestra cómo un harness de pruebas ejecuta la aplicación de control o el wrapper del SDK, que pasa por Application Verifier hacia la API de Win32, el DLL nativo y los recursos del SO, generando verifier stop, salida del depurador y registros de AppVerifier, mientras la aplicación también escribe su propio registro estructurado.Harness de pruebasAplicación de control / wrapper del SDKApplication VerifierAPI de Win32 / DLL nativo / recursos del SOverifier stopSalida del depuradorRegistros de AppVerifierRegistro estructurado propio

2.2. En qué situaciones resulta eficaz

Es especialmente eficaz en situaciones como estas:

  • se llama a un DLL nativo o al SDK de una cámara
  • se cruza P/Invoke o COM
  • se usan mucho, directa o indirectamente, handles, heap, locks o memoria virtual
  • en el recorrido normal apenas se cae, pero la gestión del ciclo de vida parece a punto de romperse solo en casos anómalos
  • lo que aparece primero no es “se cae”, sino “de vez en cuando devuelve un fallo raro”

Por el contrario, no es una herramienta pensada para seguir el object graph de un mundo puramente administrado (managed). Por eso, incluso en una aplicación C#, resulta bastante eficaz si el SDK nativo o el límite Win32 tienen peso, pero no se trata de ver con esta herramienta, y solo con ella, todas las fugas de un heap puramente administrado.

2.3. Qué ventajas aporta

En la práctica, las ventajas se reducen básicamente a estas tres:

  1. Permite detener antes los usos indebidos en el límite nativo
    • invalid handle
    • heap corruption
    • uso indebido de locks
    • uso indebido de las API de memoria virtual, etc.
  2. Permite adelantar roturas que solo aparecen en situaciones de recursos bajos
    • el equivalente a malloc falla de vez en cuando
    • CreateEvent o CreateFile fallan de vez en cuando
    • VirtualAlloc falla
  3. Es más fácil de seguir combinado con el depurador
    • !avrf
    • !htrace
    • !heap -p -a
    • el registro de los verifier stop

En una aplicación de control de equipos, el problema es “no saber qué ocurrió en un caso anómalo”. Application Verifier ayuda bastante a reducir esa falta de conocimiento.

2.4. Cómo conseguirlo y habilitarlo en el entorno local

Antes de nada, conviene resolver la parte de reunir las herramientas. Si esto falla, todo lo que viene después queda en el terreno teórico.

Application Verifier se distribuye junto con el Windows SDK. No viene incluido en Windows por sí solo, así que hay que iniciar el instalador del SDK y, en la pantalla de selección de funciones, marcar la casilla de “Application Verifier”. El nombre del ejecutable es appverif.exe.

Hay tres requisitos previos para usarlo:

  • el usuario que lo ejecuta debe pertenecer al grupo Administrators de esa máquina
  • ARM64EC no es una plataforma compatible
  • el objetivo de verificación debe ser código no administrado (nativo)

Conviene entender así la relación entre la GUI y la línea de comandos, para no confundirse:

  Qué hace
GUI (appverif.exe) Escribe en el registro el nombre del EXE de destino y la combinación de pruebas que se habilitan
Línea de comandos (appverif -enable ...) Escribe exactamente la misma configuración de registro, pero mediante un comando
En tiempo de ejecución Cuando se inicia el EXE de destino, el sistema consulta esa configuración, carga el DLL del verifier y engancha las API de Win32

Es decir, da igual cuál de las dos se use: lo que hacen es lo mismo. En la práctica, se suele usar la GUI la primera vez que se hace a mano, y la línea de comandos en CI o en scripts.

El manejo de la GUI consiste en hacer clic derecho en el panel Applications de la izquierda, elegir “Add Application” para añadir el EXE de destino, marcar Basics u otras pruebas en el panel Tests de la derecha y pulsar “Save”. Para quitarlo, se hace clic derecho en el mismo panel Applications, se elige “Delete Application” y se pulsa “Save”.

De aquí se derivan dos restricciones importantes:

  • No se puede habilitar a posteriori sobre un proceso que ya está en ejecución. El enganche se produce cuando se carga el DLL, así que el orden es: configurar primero y arrancar después.
  • La configuración permanece hasta que se elimina explícitamente. Si se deja así pensando que “solo era para probar una vez”, ese EXE seguirá arrancando siempre bajo el verifier en esa máquina.

Por cierto, el registro de las detecciones se guarda, por defecto, en formato binario en %USERPROFILE%\AppVerifierLogs, y se puede convertir a XML con la GUI o con la línea de comandos para agregarlo.

3. Qué se puede hacer con Application Verifier

3.1. Basics: Handles, Heaps, Locks, Memory, TLS, etc.

El conjunto básico de Application Verifier es Basics. Aquí se agrupan las comprobaciones que más se usan en la práctica.

Capa Qué observa Dónde se usa en este contexto
Handles Uso de invalid handle Si se está usando un handle ya cerrado o dañado
Heaps Heap corruption Detectar corrupción de búfer o use-after-free en el límite del SDK nativo
Leak Recursos no liberados en el momento de descargar el DLL Comprobación de pruebas de harness de corta duración o casos que incluyen la descarga
Locks / SRWLock Uso indebido de locks Comprobación de conflictos entre reconnect y shutdown
Memory Uso indebido de VirtualAlloc, MapViewOfFile, etc. Comprobación de anomalías en torno a búferes grandes o memoria compartida
TLS Uso indebido de las API de Thread Local Storage Salvaguarda para código nativo con límites de hilo complejos
Threadpool Consistencia de las API de threadpool y el estado de los workers Ayuda cuando hay muchos callbacks o procesamiento asíncrono

El punto clave es “no leerlo después de que se haya caído, sino detener en el acto el uso sospechoso”. En los fallos de tipo funcionamiento prolongado, esta anticipación resulta muy útil.

3.2. Low Resource Simulation: adelantar la falta de memoria o de recursos

Aquí es donde resulta especialmente útil en la práctica. Porque permite provocar fenómenos parecidos a la falta de memoria o de recursos sin necesidad de agotar realmente la RAM.

La idea es sencilla:

  • se toma una llamada a determinada API
  • con una probabilidad determinada
  • se la hace fallar deliberadamente

Con esto se puede recorrer un error path por el que normalmente casi nunca se pasa.

En concreto, facilita provocar de forma intencionada fenómenos como estos:

  • que HeapAlloc o VirtualAlloc fallen
  • que CreateFile falle
  • que CreateEvent falle
  • que MapViewOfFile falle
  • que fallen asignaciones de tipo OLE/COM como SysAllocString

Es mucho más manejable que intentar provocar una falta de memoria real sometiendo a estrés a toda la máquina. Además, también se puede dirigir el fault injection a un DLL concreto. En una aplicación de control de equipos donde se mezclan wrappers propios y SDK de proveedores, resulta bastante práctico.

3.3. Page Heap y el depurador

Para observar corrupción de heap, la combinación de Heaps con page heap es potente. En particular, el full page heap tiene la ventaja de usar guard pages para detenerse en el instante mismo en que algo se rompe.

Sin embargo, es bastante pesado. Más que un barrido exhaustivo durante mucho tiempo, resulta más práctico limitarlo a los escenarios más cercanos a la reproducción y ejecutarlo bajo el depurador.

Así que, en la operación real, un reparto de tareas realista sería este:

  • primero cubrir un rango amplio con Basics
  • cuando el heap parezca sospechoso, usar full page heap
  • si resulta demasiado pesado, bajar a light page heap
  • las pruebas de larga duración equivalentes a producción se observan sobre todo con el registro propio

En definitiva, AppVerifier no es una varita mágica, sino una herramienta a la que se le cambia la hoja según la situación.

3.4. !avrf, !htrace y los registros

Antes, un término. El verifier stop, que ha aparecido varias veces hasta aquí, es el evento de detección que emite Application Verifier cuando concluye que “este uso es anómalo”. No es una simple línea de registro: su característica es que, si se está ejecutando bajo un depurador, se detiene en el acto. A cada stop se le asigna un número, que se muestra por ejemplo como VERIFIER STOP 00000300. Hay stops que se pueden continuar y otros que no (que no dejan más opción que terminar el proceso).

Application Verifier no se limita a emitir el stop y quedarse ahí. Cuenta con extensiones de depurador y registros que facilitan seguir qué ocurrió.

  • !avrf
    • permite ver la configuración actual del verifier y el stop que está ocurriendo en ese momento
  • !htrace
    • permite ver la pila de open / close / invalid reference de un handle
  • !heap -p -a
    • combinado con page heap, permite seguir el rastro de un bloque de heap dañado
  • el registro de AppVerifier
    • permite conservar el registro de cuándo se produjo un stop

Resulta especialmente útil que, al habilitar Handles, handle tracing se active automáticamente. Con esto, es más fácil seguir después “dónde se abrió este handle y dónde se cerró”.

4. Por qué se introdujo esta vez

4.1. El objetivo no es solo “encontrar un bug”

El objetivo de esta ocasión no era simplemente “encontrar un bug con AppVerifier”. Dicho de forma más práctica, lo que se quería confirmar era lo siguiente:

  • si en el futuro se produce una fuga de recursos por otro failure path distinto
  • si el registro conservará el contexto correctamente
  • si se podrá seguir el rastro por completo junto con la información del depurador
  • si no se acabará en un estado de “no se sabe qué ocurrió”

Es decir, se usó no solo como detector, sino como prueba de la propia base de observación.

4.2. Provocar fenómenos parecidos a la falta de memoria

Provocar una falta de memoria real en una máquina de desarrollo habitual es bastante engorroso. Y, además, si toda la máquina se vuelve inestable, la propia prueba se llena de ruido.

Por eso se optó por usar Low Resource Simulation para hacer pasar de forma deliberada por los failure paths que probablemente ocurrirían con una falta de memoria o de recursos.

Con esto resulta más fácil responder a preguntas como estas:

  • si CreateEvent falla, ¿quedan registrados cameraId y phase en el log?
  • ¿se ejecuta correctamente la limpieza tras una inicialización a medias?
  • si VirtualAlloc falla, ¿el reintento no rompe nada?
  • si CreateFile falla en la ruta de guardado, ¿se devuelve correctamente el handle?

Conviene subrayar que el objetivo no es provocar la anomalía en sí misma, sino que se pueda entender cómo se rompe el sistema cuando ocurre.

4.3. Comprobar si se puede seguir el rastro cuando ocurre una anomalía de handle

Al igual que con la fuga de identificadores que apareció en la primera parte, en todo lo relacionado con handles el lugar donde finalmente se cae y la causa real tienden a estar desalineados.

Por eso, lo que se quería confirmar era esto:

  • si al aparecer un invalid handle stop se puede seguir el open / close con !htrace
  • si eso se puede vincular con resourceId / sessionId / phase del registro propio
  • si el handle count vuelve a su valor tras un fallo
  • si, al convertir el harness en un proceso de vida corta, resulta más fácil ver la diferencia de la fuga

Cuando esto se puede ver con claridad, se pasa de un simple “apareció un bug” a poder llegar a “en qué responsabilidad se rompió la gestión del ciclo de vida”.

5. Cómo provocar fenómenos parecidos a la falta de memoria o de recursos

5.1. La idea detrás de Low Resource Simulation

Low Resource Simulation es, básicamente, fault injection. Más que reproducir fielmente un entorno de recursos bajos, la idea es mezclar de forma artificial los fallos de API representativos que ocurren en situaciones de recursos bajos.

Por eso, su utilidad está bastante clara:

  • comprobar la limpieza posterior en un failure path
  • comprobar la solidez de retry / reconnect
  • comprobar una inicialización con éxitos y fallos parciales mezclados
  • comprobar si el registro se conserva incluso en un “fallo que normalmente no ocurre”

El truco aquí es no hacer fallar todo desde el principio. Si de golpe se activa todo, el registro explota y deja de estar claro “qué se está observando”.

5.2. Qué se puede hacer fallar

Con Low Resource Simulation se pueden hacer fallar de forma probabilística, entre otros, los siguientes tipos de API:

Tipo Ejemplo Ejemplo en la aplicación de control del equipo
Heap_Alloc Reserva de heap Búfer temporal, metadatos de imagen, reservas internas del wrapper del SDK
Virtual_Alloc Reserva de memoria virtual Búfer de fotogramas grande, ring buffer
File CreateFile, etc. Apertura de la ruta de guardado o del archivo de registro
Event CreateEvent, etc. Notificación de frame ready, sincronización de stop/reconnect
MapView CreateMapView, etc. Memoria compartida o memory mapped file
Ole_Alloc SysAllocString, etc. Límite COM / OLE
Wait Familia WaitForXXX Fallos en la espera de sincronización
Registry Acceso al registro Lectura/escritura de configuración o ajustes del driver

En la práctica, lo importante no es abrirlo todo a la vez, sino habilitarlo de forma acotada, empezando por lo más cercano al failure path que se quiere ver esta vez.

5.3. Cómo aplicarlo en la práctica

Como referencia de línea de comandos, un ejemplo sería el siguiente:

appverif /verify CameraHarness.exe
appverif /verify CameraHarness.exe /faults
appverif -enable lowres -for CameraHarness.exe -with heap_alloc=20000 virtual_alloc=20000 file=20000 event=20000
appverif -query lowres -for CameraHarness.exe

Copiar y pegar sin entender la intención no sirve de mucho, así que se explica línea por línea qué hace cada una.

Comando Qué hace
appverif /verify CameraHarness.exe Habilita el conjunto de pruebas Basics para CameraHarness.exe
appverif /verify CameraHarness.exe /faults Además de lo anterior, habilita fault injection. Pero solo para OLE_ALLOC y HEAP_ALLOC
appverif -enable lowres -for CameraHarness.exe -with heap_alloc=20000 ... Habilita lowres (Low Resource Simulation) y especifica de forma individual el tipo de API que debe fallar y su probabilidad
appverif -query lowres -for CameraHarness.exe Muestra qué está configurado en este momento y con qué probabilidad
appverif /n CameraHarness.exe Elimina la configuración de ese EXE (equivale a -disable * -for o a -delete settings -for)

Conviene entender bien cómo se leen los argumentos.

  • La probabilidad se expresa en partes por millón. Se puede indicar un entero entre 0 y 1.000.000, y 20000 equivale a 20000 / 1.000.000, es decir, el 2%. No significa “una vez cada 20.000”. La propia documentación de Microsoft pone como ejemplo -with registry=20000 file=20000 para hacer fallar las API de registro y de archivo con un 2% de probabilidad.
  • Después de /faults se pueden encadenar la probabilidad, el tiempo de gracia y el nombre del DLL. El formato es /faults [probabilidad [milisegundos de gracia [DLL ...]]]. Si se omite la probabilidad, se usa el 5%; si se omite el tiempo de gracia, se usan 500 milisegundos. El tiempo de gracia significa “durante este tiempo desde el arranque del proceso, no inyectar ningún fault”, y sirve para evitar que el propio proceso de arranque falle y no se pueda probar nada.
  • /n es la anulación. Puede pensarse que n viene de algo así como “no verifier”. Es el comando complementario para no dejarlo habilitado permanentemente.

La salida de -query lowres suele devolverse más o menos con esta forma. Aquí se puede comprobar si la probabilidad configurada está presente y si no se ha dejado abierto, sin querer, algún tipo que no se pretendía.

Settings for CameraHarness.exe:
Test [lowres] enabled.

Include = *
Exclude =
TimeOut = 2000 (0x7D0)
WAIT = 0 (0x0)
HEAP_ALLOC = 20000 (0x4E20)
VIRTUAL_ALLOC = 0 (0x0)
REGISTRY = 0 (0x0)
FILE = 20000 (0x4E20)
EVENT = 20000 (0x4E20)
MAP_VIEW = 0 (0x0)
OLE_ALLOC = 0 (0x0)
STACKS = false

Include y Exclude acotan los módulos de destino, y TimeOut es el tiempo durante el cual no se inyecta ningún fault justo después del arranque. El valor por defecto cambia según cómo se haya introducido la configuración, así que lo seguro es comprobarlo en esta salida en lugar de darlo por supuesto.

La idea general sería así:

  1. primero ejecutar el recorrido normal solo con Basics
  2. después añadir Low Resource Simulation y ejecutar con fault injection activado
  3. si hace falta, dar probabilidad solo a los fallos que se quieren observar, como file o event
  4. si se quiere apuntar a un DLL concreto, habilitarlo acotado a ese DLL

El atajo /faults es cómodo, pero por sí solo se centra sobre todo en OLE_ALLOC y HEAP_ALLOC. Si se quiere observar el failure path de CreateFile o CreateEvent, es más seguro escribir explícitamente -enable lowres -with file=... event=....

En una aplicación de control de equipos, suele resultar más legible acotar el fault al DLL del wrapper de la cámara o al de la ruta de guardado, en lugar de esparcirlo por toda la aplicación.

También conviene dejar por escrito, en concreto, cómo se escribe eso de “acotar al DLL”. El tercer argumento en adelante de /faults es la indicación del módulo de destino.

appverif /verify CameraHarness.exe /faults 50000 1000 CameraSdkWrapper.dll

Con esto, al iniciar CameraHarness.exe, solo las operaciones que empiecen desde CameraSdkWrapper.dll, transcurridos 1000 milisegundos desde el arranque, se harán fallar con una probabilidad del 5% (50000 / 1.000.000). El nombre del módulo se escribe incluyendo la extensión y sin la ruta. Además de .dll, también se pueden indicar módulos cargables como .ocx.

Se puede comprobar si el acotamiento está surtiendo efecto en las líneas Include y Exclude de appverif -query lowres -for CameraHarness.exe. Si aquí sigue apareciendo *, el objetivo todavía es todo el proceso.

Si se está ejecutando bajo el depurador, también se puede cambiar el ámbito sobre la marcha.

!avrf -trg dll CameraSdkWrapper.dll
!avrf -skp dll VendorSdk.dll

-trg significa “apuntar aquí” y -skp significa “saltarse esto”. También se puede comprobar la configuración actual de fault injection con !avrf -flt, o ver la pila de los fallos inyectados más recientemente con !avrf -flt stacks 10.

Por ejemplo, se pueden construir escenarios como estos:

  • fallo de CreateEvent justo al empezar el reconnect
  • fallo de CreateFile al empezar el guardado
  • fallo al reservar un búfer temporal
  • fallo de SysAllocString en la conversión COM
  • comprobación del camino de fallo de una API de espera

Todo esto, en las pruebas del recorrido normal habitual, prácticamente nunca se llega a recorrer. Por eso vale la pena provocarlo de forma deliberada.

6. Cómo observar una anomalía de handle

6.1. La comprobación Handles

Para todo lo relacionado con handles, se empieza usando Handles. Con esto resulta más fácil detectar el uso de invalid handle.

Los accidentes típicos que suele atrapar son estos:

  • volver a usar un handle ya cerrado
  • pasar un valor de handle dañado
  • usar un handle no inicializado por un fallo a mitad de camino
  • que se rompa el lifetime y otro hilo lo toque

En un funcionamiento prolongado, algo que a simple vista solo parece “de vez en cuando sale un error raro” puede detenerse en el acto bajo el verifier. Esta anticipación ayuda bastante.

6.2. Ver la pila de open / close con !htrace

Lo bueno de Handles es que se lleva bien con handle tracing.

A partir de aquí se usa el depurador, así que primero instalamos WinDbg. Se distribuye como Debugging Tools for Windows y se puede instalar desde el mismo instalador del Windows SDK que Application Verifier.

windbg -xd av -xd ch -xd sov CameraHarness.exe
!avrf
!htrace 0x00000ABC

Las opciones de la primera línea no son un simple ritual. Application Verifier lanza tres tipos de excepción al detectar algo:

Opción Excepción Cuándo aparece
av Violación de acceso (0xC0000005) Al detectar un desbordamiento de búfer en el heap
ch Handle no válido (0xC0000008) Al detectar el uso de un invalid handle
sov Desbordamiento de pila (0xC00000FD) Al determinar que la pila inicial es insuficiente

Y -xd es la indicación de capturar esa excepción en second chance. En first chance, el propio Application Verifier la procesa para construir la información del stop, así que si el depurador interrumpe antes, resulta un problema. Si se configura en un depurador ya iniciado, es equivalente a escribir sxd av, sxd ch y sxd sov.

Con !htrace se busca, básicamente, lo siguiente:

  • dónde se abrió (open) ese handle
  • dónde se cerró (close)
  • si se referenció como invalid handle
  • si se han acumulado más aperturas de las esperadas

También se incluye cómo se ve realmente. Lo siguiente es un ejemplo de salida tomado de la documentación oficial, no de nuestro propio entorno, pero la forma es la misma.

Al toparse con un invalid handle, primero aparece esto:

Invalid handle - code c0000008 (first chance)
===================================================
VERIFIER STOP 00000300: pid 0x558: invalid handle exception for current stack trace
        C0000008 : Exception code.
        0012FBF8 : Exception record. Use .exr to display it.
        0012FC0C : Context record. Use .cxr to display it.
        00000000 :
===================================================
This verifier stop is continuable.
After debugging it use 'go' to continue.
===================================================

Si a continuación se ejecuta !avrf, aparece qué está habilitado en ese momento y qué stop está ocurriendo. La última línea es el resumen.

0:000> !avrf
Global flags: 00000100
Application verifier global flag is set.
Application verifier settings (00000004):
   - no heap checking enabled!
   - handle checks
Page heap is not active for this process.
Current stop 00000300 : c0000008 0012fbf8 0012fc0c 00000000 .
    Using an invalid handle (either closed or simply bad).

Si se observa el historial de ese handle con !htrace, aparecen OPEN, CLOSE y BAD REFERENCE, cada uno con su pila.

0:000> !htrace 7DC

--------------------------------------
Handle 0x7DC - BAD REFERENCE:
0x801902BE: ntoskrnl!NtSetEvent+0x6C
0x010012C1: badhandle!mainCRTStartup+0xE3
--------------------------------------
Handle 0x7DC - CLOSE:
0x801E1EDD: ntoskrnl!NtClose+0x19
0x010012C1: badhandle!mainCRTStartup+0xE3
--------------------------------------
Handle 0x7DC - OPEN:
0x77DE265C: KERNEL32!CreateEventA+0x66
0x010011A0: badhandle!main+0x20
--------------------------------------

La lectura es directa: si después de CLOSE aparece BAD REFERENCE, significa que se está volviendo a usar un handle ya cerrado. Si se mira la pila de OPEN, también se ve dónde se creó ese handle.

Lo problemático de las fugas de identificadores y del uso indebido de handles es que la API que finalmente falla no suele ser la causa real. Con !htrace se puede seguir el historial de ese handle de forma bastante concreta.

6.3. Cómo combinarlo con el registro propio

Aun así, con Application Verifier solo no basta. En particular, dejar en sus manos toda la investigación de fugas de un EXE que permanece en ejecución durante mucho tiempo es bastante duro.

Por eso, en la práctica se combina con lo siguiente:

  • Handle Count periódico
  • sessionId
  • resourceId
  • phase
  • el registro del ciclo de vida entre create/open y close/dispose
  • el dump y la salida del depurador en el momento del verifier stop

Con esto se puede seguir el rastro, por ejemplo, así:

  1. con el heartbeat se detecta que la tendencia de Handle Count es sospechosa
  2. con el registro del ciclo de vida se acotan los recursos que tienen Create pero no Close
  3. con la ejecución del verifier se hacen aflorar de antemano el invalid handle o el mal uso
  4. con !htrace se observa la pila de open / close

Con esta combinación resulta mucho más fácil seguir el rastro.

7. Cómo construir la base de pruebas de casos anómalos

7.1. Centrar la unidad de ejecución en un harness

Application Verifier no se puede habilitar a posteriori sobre un proceso que ya está en ejecución. Se configura primero y se arranca después.

Además, la configuración permanece hasta que se elimina de forma explícita. Por eso, en la práctica resulta más manejable centrarla en un EXE harness de pruebas, en lugar de en la propia aplicación de producción.

Por ejemplo, una configuración como esta:

Arquitectura del harness de pruebas para casos anómalosDiagrama que muestra cómo el ejecutor de escenarios inicia CameraHarness.exe, que llama a CameraSdkWrapper.dll y este al SDK del proveedor, mientras CameraHarness.exe genera un registro estructurado y datos de dump o depurador.Ejecutor de escenariosCameraHarness.exeCameraSdkWrapper.dllSDK del proveedorRegistro estructuradoDump / Depurador

Con esto se obtienen las siguientes ventajas:

  • se puede ejecutar un proceso por cada escenario
  • resulta más fácil ver la diferencia de la fuga
  • resulta más fácil alternar la configuración de AppVerifier entre activada y desactivada
  • también se puede manejar desde el lado del EXE incluso cuando se está probando un DLL

Como referencia, la idea de los comandos sería así:

appverif /verify CameraHarness.exe
appverif /n CameraHarness.exe

/verify habilita Basics y /n elimina la configuración (consulte también la tabla de la sección 5.3). La habilitación se hace antes del arranque, y la anulación de forma explícita. Manejar esto sobre la base de un harness ayuda también a reducir los accidentes de configuración.

7.2. Separar el menú de pruebas

En la base de pruebas de casos anómalos, es mejor no hacerlo todo de una sola vez. Resulta más legible separarlo, más o menos, en estos tres bloques:

  1. Recorrido normal + Basics
    • no se inyecta ningún fallo
    • se comprueba que no aparezca ningún verifier stop
  2. Bloque de fault injection
    • Low Resource Simulation
    • se hacen fallar de forma dirigida event, file, heap_alloc, virtual_alloc, etc.
  3. Bloque de profundización en el heap
    • Heaps
    • full page heap
    • se reproduce de forma localizada bajo el depurador

Al separarlo así, es menos probable que se mezclen “se rompe con el uso habitual” y “solo se rompe en situaciones de recursos bajos”.

En particular, según haya o no fault injection, el code path que se recorre cambia bastante. Por eso conviene ejecutar tanto la ejecución sin fault como la ejecución con fault.

7.3. Qué recopilar

Como mínimo, conviene conservar esto:

Tipo Qué se quiere obtener
Registro de la aplicación cameraId, sessionId, phase, handleCount, error code
Estado del proceso Handle Count, Private Bytes, Thread Count
Información del depurador !avrf, !htrace y, si hace falta, !heap -p -a
Dump En el momento del verifier stop o de una finalización anómala
Registro de AppVerifier El registro de los stop y, si hace falta, convertirlo a XML para agregarlo

Si hace falta, el registro de AppVerifier también se puede convertir a XML para agregarlo. Sin embargo, muchas veces la causa no se cierra mirando solo eso, así que lo más práctico es partir de que se lee junto con el registro propio.

Que haya muchos registros no es, en sí mismo, un mérito. Lo importante es que después se pueda enlazar la causalidad.

7.4. Criterios de aprobación

Los criterios de aprobación tampoco son sólidos si se limitan a “no se cayó”. En el contexto de esta ocasión, como mínimo hacían falta estos:

  • que no aparezca ningún verifier stop en el recorrido normal + Basics
  • que, incluso con fault injection activado, los fallos previstos queden registrados en el log
  • que los recursos que quedaron inicializados a medias se limpien correctamente
  • que, tras un reconnect / retry, Handle Count vuelva cerca de su valor de referencia
  • que, cuando aparece un verifier stop, se pueda seguir el rastro con sessionId / phase / la pila
  • que no se convierta en un fallo del tipo “no se sabe qué ocurrió”

Lo importante aquí es evaluar por separado que no se rompa y que, si se rompe, se pueda seguir el rastro.

7.5. Puntos de atención

Application Verifier es bastante útil, pero no es magia.

  • no verifica un code path que realmente no se ha recorrido
  • full page heap es pesado
  • también puede aparecer un stop del lado de un SDK de terceros
  • el code path que se recorre cambia bastante según haya o no fault injection
  • no es una herramienta con la que se pueda investigar, solo con ella, una fuga de heap puramente administrado

Por eso, el reparto de papeles queda así:

  • la tendencia a largo plazo, con el registro propio y los counters
  • el mal uso en el límite nativo, con Application Verifier
  • la reconstrucción de la causalidad de un caso anómalo, con el registro estructurado, el dump y el depurador

Este reparto de tareas es el más práctico.

8. Guía rápida de cuándo usar qué

  • cuando se sospecha de un invalid handle o de un double close
    • Handles + !htrace
  • cuando se sospecha de heap corruption / use-after-free
    • Heaps + full page heap + !heap -p -a
  • cuando se quiere provocar un fenómeno parecido a la falta de memoria o de recursos
    • Low Resource Simulation
  • cuando algo se rompe poco a poco durante un funcionamiento prolongado
    • primero, Handle Count / Private Bytes / el registro del ciclo de vida propios
  • cuando se quiere probar un DLL
    • habilitar Application Verifier sobre el EXE harness que llama a ese DLL

Si desde el principio se activa todo, en general se acaba en una niebla de registros. Es mucho más claro empezar por la hoja más cercana al failure path que se quiere observar.

9. Resumen

El lugar que ocupa Application Verifier es el de un runtime verifier para el límite nativo / Win32 de Windows. Con Handles, Heaps, Locks, Memory, TLS, Low Resource Simulation y demás, permite hacer pasar de antemano por failure paths que normalmente no aparecen.

Lo que resultó útil en este contexto fue que, al aparecer una anomalía de handle, resulta fácil seguir el rastro con !htrace; que se pueden provocar fenómenos parecidos a la falta de memoria o de recursos sin romper toda la máquina; y que se pudo confirmar si el registro propio realmente resultaba útil en ese momento.

En cuanto a cómo manejarlo en la práctica, el reparto queda así: separar el recorrido normal + Basics del bloque de fault injection, preparar un EXE harness y ejecutar los escenarios con procesos de vida corta. Sobre esa base, se combina con el registro propio, el dump y la información del depurador, y la propia tendencia de la fuga a largo plazo se observa con counters propios.

Application Verifier es una herramienta para “ir a buscar” las anomalías que casi nunca aparecen, en lugar de “esperar por casualidad” a que ocurran.

En una aplicación de control de equipos, no romperse es importante, pero poder explicar qué ocurrió cuando algo se rompe es igual de importante. En ese sentido, creo que es una herramienta bastante útil en la práctica.

Primera parte: Investigación de caídas en cámaras industriales por funcionamiento prolongado: edición de fuga de identificadores

10. Referencias

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

Estos casos muestran un enfoque parecido para analizar, priorizar o rediseñar.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Qué es Application Verifier?
Es una herramienta de verificación en tiempo de ejecución para aplicaciones en modo usuario de Windows. Supervisa cómo la aplicación en ejecución utiliza las API del sistema operativo y maneja los recursos, y puede detectar usos sospechosos como el uso de un invalid handle o heap corruption, además de inyectar fallos de forma intencionada. A diferencia del análisis estático o de las pruebas unitarias, es una herramienta que muestra cómo se rompe el código cuando realmente se ejecuta esa ruta, por lo que resulta adecuada para sacar a la luz failure paths que no se ven en las pruebas funcionales habituales.
¿Se puede reproducir la falta de memoria con Application Verifier?
Si utiliza Low Resource Simulation, puede provocar de antemano fenómenos parecidos a la falta de memoria o de recursos sin necesidad de agotar realmente la RAM de la máquina. El mecanismo es fault injection: hace fallar deliberadamente, con una probabilidad determinada, llamadas a API como HeapAlloc, VirtualAlloc, CreateFile o CreateEvent. También se puede inyectar el fallo dirigido solo a un DLL concreto, lo que resulta manejable incluso en configuraciones donde se mezclan wrappers propios y SDK de proveedores. Sin embargo, si desde el principio se hace fallar todo, el registro se vuelve ilegible, así que el truco está en habilitarlo de forma acotada, empezando por lo más cercano al failure path que se quiere observar.
¿Se puede usar Application Verifier para investigar una fuga de identificadores (handle leak)?
Si habilita la comprobación Handles, puede detectar usos de invalid handle, como la reutilización de un handle ya cerrado, y handle tracing se activa automáticamente, de modo que con !htrace se puede seguir la pila de open / close de ese handle. Sin embargo, no es realista delegar por completo en Application Verifier la investigación de fugas en un EXE que permanece en ejecución durante mucho tiempo. En la práctica funciona mejor combinarlo con el registro periódico del Handle Count y con un registro propio del ciclo de vida de los recursos, repartiendo el trabajo así: la detección de la tendencia queda para el registro propio, y la detección del mal uso queda para el verifier.
¿Cómo se usa Application Verifier para probar un DLL?
El objetivo sobre el que se habilita Application Verifier es el EXE de prueba que realmente ejecuta ese DLL. No se puede habilitar a posteriori sobre un proceso que ya está en ejecución: primero hay que configurarlo y después iniciar el proceso. Además, la configuración permanece hasta que se elimina explícitamente, por lo que resulta más manejable aplicarla a un EXE harness de pruebas que a la propia aplicación de producción. Si se ejecuta un proceso por cada escenario, también resulta más fácil ver la diferencia en las fugas y alternar entre activar y desactivar la configuración.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog