Si migraste a Windows Server 2022 y tu filtro global con SetUnhandledExceptionFilter
dejó de capturar excepciones C++ no controladas en hilos creados con beginthread/beginthreadex
, aquí encontrarás la causa técnica y un plan de corrección robusto, reproducible y compatible.
Contexto y síntoma
En Windows Server 2016, muchos servicios escritos en C++ se apoyaban en SetUnhandledExceptionFilter
para registrar o generar un minidump cuando una excepción escapaba del punto de entrada de un hilo creado con beginthread
o beginthreadex
. Tras migrar a Windows Server 2022, ese “último salvavidas” deja de funcionar para excepciones C++: el filtro no se dispara y el proceso termina de manera abrupta.
La explicación práctica es que la UCRT (Universal CRT) que acompaña al sistema cambió el comportamiento del wrapper de la CRT: si una excepción C++ abandona el punto de entrada del hilo, la CRT invoca std::terminate
o un mecanismo equivalente, sin dejar que el sistema operativo lo trate como una excepción no manejada de SEH. Por eso el filtro de SetUnhandledExceptionFilter
nunca ve esa excepción.
Fundamentos imprescindibles: SEH, excepciones C++ y la CRT
Para entender el cambio hay que separar piezas:
- SEH (Structured Exception Handling): mecanismo del kernel/Win32 para excepciones de hardware y ciertas condiciones de error (p. ej., access violations, división por cero). La cadena de handlers incluye vectored handlers (
AddVectoredExceptionHandler
) y el top-level unhandled filter (SetUnhandledExceptionFilter
) del proceso. - Excepciones C++: mecanismo de lenguaje que realiza stack unwinding siguiendo la ABI/CRT del compilador. Son ajenas a SEH, aunque pueden interactuar según cómo compiles (
/EHa
vs/EHsc
) y cómo escribas el código. - CRT: las funciones
beginthread/beginthreadex
no son APIs del sistema operativo; son envoltorios de la CRT que inicializan estado por-hilo y delegan enCreateThread
con lógica adicional (p. ej., limpieza mediante_endthreadex
).
Antes, en algunas versiones de CRT, cuando una excepción C++ escapaba del punto de entrada del hilo, el comportamiento podía permitir que la excepción “cayera” hasta el nivel OS como un fallo no manejado, activando el unhandled filter. En Windows Server 2022, la UCRT estandariza un comportamiento análogo a std::thread
(efectivamente noexcept
): si escapa una excepción, se llama a std::terminate
. Resultado: tu filtro global ya no entra en escena.
Consecuencia directa para SetUnhandledExceptionFilter
Si el wrapper de la CRT decide terminar el proceso al detectar que una excepción C++ escapó del hilo, el sistema no lo ve como “unhandled SEH exception in user code”; simplemente se ejecuta el terminate. Por tanto, SetUnhandledExceptionFilter
deja de ser un mecanismo fiable para excepciones C++ no capturadas en hilos de la CRT en Windows Server 2022. Sigue siendo útil para excepciones SEH (hardware) y para otras rutas de error fuera del control de la CRT.
Cómo reproducir el problema
El siguiente ejemplo crea un hilo con beginthreadex
que lanza una std::runtimeerror
. Se instala un unhandled filter y, además, un std::set_terminate
para ver cuál de los dos se activa:
// testbeginthreadws2022.cpp
#include <windows.h>
#include <process.h>
#include <DbgHelp.h>
#include <exception>
#include <stdexcept>
#include <cstdio>
\#pragma comment(lib, "Dbghelp.lib")
static LONG WINAPI UnhandledFilter(EXCEPTION\_POINTERS\* p);
static void WriteMiniDump(EXCEPTION\POINTERS\* p, const wchar\t\* path);
unsigned \\stdcall worker(void\*){
// Simula un bug lógico que lanza una excepción C++
throw std::runtime\_error("boom!");
return 0;
}
int main() {
SetUnhandledExceptionFilter(&UnhandledFilter);```
std::set_terminate([]{
OutputDebugStringW(L"[terminate] Se llamó a std::terminate\n");
// Aquí podrías escribir un dump controlado o señalizar a un watchdog
ExitProcess(EXIT_FAILURE);
});
uintptr_t th = _beginthreadex(nullptr, 0, &worker, nullptr, 0, nullptr);
WaitForSingleObject(reinterpret_cast<HANDLE>(th), INFINITE);
CloseHandle(reinterpret_cast<HANDLE>(th));
return 0;
```
}
static LONG WINAPI UnhandledFilter(EXCEPTION\_POINTERS\* p) {
OutputDebugStringW(L"\[UEF] UnhandledExceptionFilter ejecutado\n");
WriteMiniDump(p, L"crash.dmp");
return EXCEPTION\EXECUTE\HANDLER; // No intentes continuar
}
static void WriteMiniDump(EXCEPTION\POINTERS\* p, const wchar\t\* path) {
HANDLE hFile = CreateFileW(path, GENERIC\WRITE, 0, nullptr, CREATE\ALWAYS, FILE\ATTRIBUTE\NORMAL, nullptr);
if (hFile == INVALID\HANDLE\VALUE) return;```
MINIDUMP_EXCEPTION_INFORMATION mei{};
mei.ThreadId = GetCurrentThreadId();
mei.ExceptionPointers = p;
mei.ClientPointers = FALSE;
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile,
MiniDumpWithDataSegs | MiniDumpWithHandleData | MiniDumpWithThreadInfo,
p ? &mei : nullptr, nullptr, nullptr);
CloseHandle(hFile);
```
}
Compila con CRT dinámica (/MD
) para que el comportamiento dependa de la UCRT del sistema:
cl /EHsc /MD /Zi testbeginthreadws2022.cpp /link dbghelp.lib
En Windows Server 2022 verás el mensaje de [terminate]
(o terminación silenciosa), pero no el de [UEF]
. En entornos antiguos, puede que tu UEF sí saltara. Precisamente ese es el quebranto de la migración.
Tabla comparativa de comportamientos
Escenario | Windows Server 2016 (CRT antigua) | Windows Server 2022 (UCRT moderna) | Observación |
---|---|---|---|
Excepción C++ no capturada en hilo de _beginthreadex | En algunos builds activaba el UEF; dependía de CRT | std::terminate , UEF no se invoca | No confiar en UEF para C++ EH |
Excepción SEH (AV, acceso inválido) en cualquier hilo | UEF/VEH pueden captar | UEF/VEH pueden captar | Sigue siendo válido para hardware faults |
std::thread con excepción C++ no capturada | std::terminate | std::terminate | Consistente con el estándar C++ |
CRT estática (/MT ) | Comportamiento ligado al toolset | Comportamiento ligado al toolset | Aun así, la práctica segura es capturar en la entrada |
Estrategia de solución recomendada
La solución estable y soportada es impedir que cualquier excepción C++ escape del punto de entrada del hilo. Complementa con un std::set_terminate
global para registrar y cerrar ordenadamente si algo logra escapar.
Captura explícita en la entrada del hilo
Envuelve tu función real del hilo con un trampolín que capture todo y unifique el registro:
unsigned stdcall realthreadmain(void* arg);
unsigned \\stdcall thread\_entry(void\* arg) {
try {
return real\thread\main(arg);
} catch (const std::exception& e) {
// Registro, telemetría y/o minidump
OutputDebugStringA(("\[thread] std::exception: " + std::string(e.what()) + "\n").c\_str());
// Opcional: reemitir como SEH para que UEF/VEH vean el caso y generen dump centralizado
// RaiseException(0xE000BEEF, 0, 0, nullptr);
return 0;
} catch (...) {
OutputDebugStringW(L"\[thread] Excepción desconocida\n");
// Opcional: RaiseException(...)
return 0;
}
}
// Creación del hilo SIEMPRE con el trampolín
uintptr\t th = \beginthreadex(nullptr, 0, &thread\_entry, arg, 0, nullptr);
Política de continuidad: no intentes “seguir como si nada” después de una excepción no capturada. Marca el componente como degradado, señaliza al orquestador y considera reciclar el proceso o el servicio.
Manejador global de std::terminate
Instala un handler para cubrirte si algo llegara a terminate:
static void on_terminate() noexcept {
OutputDebugStringW(L"[terminate] std::terminate invocado\n");
// Evita asignaciones, locks complejos o lanzar excepciones
// Si necesitas un dump aquí, usa MiniDumpWriteDump con cuidado
::ExitProcess(EXIT_FAILURE); // o notifica a un supervisor
}
int main() {
std::set\terminate(&on\terminate);
// ...
}
Conserva UEF y añade un VEH para SEH
Para excepciones de hardware, el patrón UEF/VEH sigue aportando valor:
static LONG NTAPI VectoredHandler(EXCEPTION_POINTERS* ep) {
// Filtra sólo AV y similares
if (ep && ep->ExceptionRecord && ep->ExceptionRecord->ExceptionCode == EXCEPTIONACCESSVIOLATION) {
OutputDebugStringW(L"[veh] Access violation detectado\n");
// Registro / dumps aquí si procede
}
return EXCEPTIONCONTINUESEARCH;
}
int main() {
AddVectoredExceptionHandler(1, &VectoredHandler);
SetUnhandledExceptionFilter(&UnhandledFilter);
// ...
}
Observabilidad y diagnósticos
Minidumps controlados
Usa MiniDumpWriteDump
desde tu catch(...)
, UEF o std::terminate
con una política clara: generar dump y salir. Evita intentar reanudar ejecución tras un fallo grave.
WER LocalDumps sin tocar el código
Si quieres volcados ante terminaciones inesperadas, habilita Windows Error Reporting LocalDumps por Registro:
rem A nivel de proceso
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MiServicio.exe" /v DumpType /t REG_DWORD /d 2 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MiServicio.exe" /v DumpFolder /t REGEXPANDSZ /d "C:\Dumps" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MiServicio.exe" /v DumpCount /t REG_DWORD /d 10 /f
DumpType: 1=Mini, 2=Full. Para servidores en producción, un minidump con heap suele bastar (elige el tipo con cuidado por tamaño y privacidad).
Arquitectura de resiliencia
- Supervisión externa: configura acciones de recuperación del Service Control Manager para reinicios automáticos ante fallos del servicio.
sc failure MiServicio reset= 86400 actions= restart/5000/restart/5000/restart/5000
sc failureflag MiServicio 1
- Contratos de errores: en hilos de trabajo, convierte rutas excepcionales previsibles en errores controlados (
std::error_code
,tl::expected
o equivalentes). - Evita mezclar CRTs: si usas
/MT
(CRT estática), hazlo en todo el proceso y en todas las DLL propias que compartan memoria/objetos; no mezcles con/MD
.
Checklist de implementación
- Inventaria puntos de creación de hilos:
beginthread
,beginthreadex
,std::thread
,CreateThread
, pools. - Introduce un trampolín
try/catch(...)
en cada punto de entrada de hilo, y aplícalo de forma consistente en todo el código. - Instala
std::set_terminate
para registro y salida controlada. - Mantén
SetUnhandledExceptionFilter
para SEH e incorpora un vectored handler si necesitas más visibilidad. - Configura minidumps propios y/o WER LocalDumps.
- Prueba en matrices WS2016 y WS2022 con
/MD
y, si lo consideras,/MT
. - Define política de recuperación: ¿reinicias hilo, componente o proceso? Alinea con tu SLO.
Errores comunes y cómo evitarlos
- Confiar en UEF para excepciones C++ de hilos CRT: en Windows Server 2022 no funcionará; captura en la entrada del hilo.
- Continuar ejecución tras un UEF: retornar
EXCEPTIONCONTINUEEXECUTION
o “tragar” errores puede dejar el proceso en estado corrupto. - Lanzar desde
std::terminate
: prohibido; terminará enstd::terminate
recursivo y abort. - Bloquear en el handler: reduce acciones en
UEF
yterminate
a operaciones seguras y rápidas; evita deadlocks. - Compilar con
/EHa
para “capturarlo todo”: aumenta el radio de captura pero no altera el hecho de que la CRT puede llamar astd::terminate
cuando la excepción escapa de la entrada del hilo.
Ejemplo completo de patrón robusto
#include <windows.h>
#include <process.h>
#include <DbgHelp.h>
#include <exception>
#include <stdexcept>
#include <string>
\#pragma comment(lib, "Dbghelp.lib")
static LONG WINAPI UEF(EXCEPTION\_POINTERS\* p);
static void WriteMiniDump(EXCEPTION\POINTERS\* p, const wchar\t\* path);
static void on\_terminate() noexcept {
OutputDebugStringW(L"\[terminate] proceso finalizará de forma controlada\n");
ExitProcess(EXIT\_FAILURE);
}
unsigned \\stdcall real\thread\main(void\* arg) {
// Simulator: bug en código de negocio
if (arg == nullptr) throw std::logic\_error("invariante rota");
return 0;
}
unsigned \\stdcall thread\_entry(void\* arg) {
try {
return real\thread\main(arg);
} catch (const std::exception& e) {
// Registro + dump opcional
OutputDebugStringA(("\[thread] " + std::string(e.what()) + "\n").c\_str());
// Opcional: elevar a SEH si quieres funnel hacia UEF/VEH
// RaiseException(0xE000BEEF, 0, 0, nullptr);
return 0;
} catch (...) {
OutputDebugStringW(L"\[thread] Excepción desconocida\n");
return 0;
}
}
int wmain() {
std::set\terminate(&on\terminate);```
AddVectoredExceptionHandler(1, [](EXCEPTION_POINTERS* p) -> LONG {
if (p && p->ExceptionRecord && p->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
OutputDebugStringW(L"[veh] AV detectado\n");
}
return EXCEPTION_CONTINUE_SEARCH;
});
SetUnhandledExceptionFilter(&UEF);
uintptr_t th = _beginthreadex(nullptr, 0, &thread_entry, nullptr, 0, nullptr);
WaitForSingleObject(reinterpret_cast<HANDLE>(th), INFINITE);
CloseHandle(reinterpret_cast<HANDLE>(th));
return 0;
```
}
static LONG WINAPI UEF(EXCEPTION\_POINTERS\* p) {
OutputDebugStringW(L"\[uef] UnhandledExceptionFilter\n");
WriteMiniDump(p, L"service.dmp");
return EXCEPTION\EXECUTE\HANDLER;
}
static void WriteMiniDump(EXCEPTION\POINTERS\* p, const wchar\t\* path) {
HANDLE h = CreateFileW(path, GENERIC\WRITE, 0, nullptr, CREATE\ALWAYS, FILE\ATTRIBUTE\NORMAL, nullptr);
if (h == INVALID\HANDLE\VALUE) return;```
MINIDUMP_EXCEPTION_INFORMATION mei{};
mei.ThreadId = GetCurrentThreadId();
mei.ExceptionPointers = p;
mei.ClientPointers = FALSE;
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), h,
MINIDUMP_TYPE(MiniDumpWithDataSegs | MiniDumpWithHandleData | MiniDumpWithThreadInfo),
p ? &mei : nullptr, nullptr, nullptr);
CloseHandle(h);
```
}
Notas de compilación y runtime
- CRT dinámica vs estática: con
/MD
la UCRT proviene del sistema, por lo que el comportamiento variará según la versión del SO. Con/MT
fijas el comportamiento a tu toolset, pero exige disciplina para evitar mezclar CRTs y garantizar compatibilidad binaria entre módulos. - Dependencias: si generas minidumps, incluye y versiona
DbgHelp.dll
/DbgHelp.lib
coherentes con tu entorno. - Pruebas: automatiza escenarios de “excepción que escapa del hilo” en CI para evitar regresiones.
Preguntas frecuentes
¿Puedo “restaurar” el comportamiento anterior con una bandera oculta?
No hay un switch de runtime fiable. Depender del comportamiento de la CRT del sistema no es sostenible. La solución soportada es capturar en la entrada del hilo.
¿CreateThread
evitaría el problema?
No. Lanzar una excepción C++ “a través” de un límite no-C++ es comportamiento indefinido. La vía segura es no dejar escapar excepciones C++ de la función que pasas a CreateThread
y capturarlas internamente.
¿Sirve setse_translator
?
Traduce SEH a excepciones C++ dentro de try
que ya estén preparados para capturarlas. No cambia el hecho de que la CRT termine el proceso si una excepción C++ escapa de la entrada del hilo.
¿/EHa
arregla algo?
Hace que catch(...)
atrape excepciones asíncronas (SEH) durante el unwinding en funciones compiladas con ese flag, pero no modifica la política de la CRT al detectar una excepción C++ que escapa del punto de entrada del hilo.
¿Puedo continuar después de un UEF?
No es recomendable. El estado del proceso puede estar corrupto. Lo responsable es registrar, generar el dump y terminar de manera controlada, delegando en un supervisor el reinicio.
Resumen ejecutivo
Qué pasa: en Windows Server 2022, si una excepción C++ escapa del punto de entrada de un hilo creado con beginthread/beginthreadex
, la UCRT invoca std::terminate
y no se dispara SetUnhandledExceptionFilter
.
Por qué importa: rompe pipelines que dependían del unhandled filter para diagnosticar y evitar caídas.
Cómo se arregla: captura en la entrada de todos los hilos (try/catch(...)
), instala un std::set_terminate
global, conserva UEF/VEH para SEH, habilita minidumps/WER y define una política de recuperación con supervisión externa.
Beneficio: comportamiento consistente entre versiones de sistema/toolset, diagnósticos fiables y resiliencia operativa.
En una línea: deja de confiar en SetUnhandledExceptionFilter
para excepciones C++ en hilos de la CRT; captura en la entrada del hilo y termina o recupera de forma explícita según tu política.