WerFault Internals Part I: Why WerFault.exe Is Late Crash Telemetry
A reverse-engineering walk through normal user-mode crash reporting, from kernel exception dispatch through in-process WER state, service coordination, and crash-vertical worker creation.
When WerFault.exe appears under a crashing process, the endpoint story has already started. The process-creation event is useful because it gives you a worker image, parent process, command line, and often the familiar -u -p <pid> -s <handle> shape. It is also late telemetry. By that point, the normal user-mode WER path has already run inside the faulting process and prepared crash state for the worker to consume.
This post is the foundation for a larger series on Windows Error Reporting as both an internals system and an endpoint-security surface. The practical question behind the series is simple: when a Windows process crashes, who builds the evidence that developers, crash pipelines, EDRs, CNO operators, and analysts later trust?
The answer is more precise than “the kernel saw a crash and launched a dumper.” For normal user-mode crashes, Windows routes the exception through the kernel, but then returns to the already-crashing process so user-mode WER code can build report state, create shared handles, talk to the service path, and only later launch or coordinate the out-of-process worker.
Part I starts with that control-flow boundary. Before getting into WerSvc ALPC messages, WerFaultSecure.exe, crash dump policy, replacement behavior, or telemetry-poisoning ideas, we need a clean map of the normal path from exception dispatch to worker creation.
By the end of this post, the goal is to make three things precise:
- what
WerFault.exeactually is in the crash-reporting pipeline - where the crashing process still participates before the worker starts
- why a
WerFault.exeprocess creation event is useful, but late, telemetry
The first correction I had to make in my own model was this:
the kernel does not directly spawn WerFault.exe for a normal user-mode crash.
The kernel is obviously in the path. It owns trap handling, exception dispatch, second-chance routing, debugger notification, and the WER service-port registration machinery. But the process creation event that gives you the familiar WerFault.exe -u -p <pid> -s <handle> worker is not some magic kernel callback.
A compact boundary model makes the corrected path easier to reason about:
| Boundary | Owner | What to remember |
|---|---|---|
| Exception routing | Kernel and ntdll dispatch | The OS routes first- and second-chance exception state; that does not prove worker creation. |
| In-process WER | KernelBase, KERNEL32, and ntdll WER client code | The faulting process builds report state and service messages before the worker exists. |
| Worker launch | WER crash-vertical code, including Faultrep.dll paths | User-mode WER launch code creates WerFault.exe or WerFaultSecure.exe with a command line that points back to existing state. |
On the Windows 10.0.26100.x build family I analyzed, the normal path crosses back into user mode, enters the unhandled-exception filter, lazily resolves the error-handling API-set contract to KERNEL32.DLL, enters WER reporting, tries the service path through ntdll!RtlWerpReportException, and only then reaches the crash-vertical worker launch.
That distinction matters. If you are trying to reason about WER as a defender, reverser, or exploit researcher, treating the worker as a direct kernel launch is the wrong mental model. The more useful model is: the kernel routes exception state; user-mode WER components build report state, talk to WerSvc, and create the worker process when the crash vertical needs one.
For endpoint instrumentation, a WerFault.exe process-creation event is the confirmation point, not the start of the story. The more useful correlation points are the exception route, the KernelBase/KERNEL32 WER bridge, ALPC/service behavior, the shared mapping handle, and the final worker command line.
Terms Up Front
WER is Windows Error Reporting, the subsystem that turns crashes, hangs, application recovery, local dump policy, and related report data into something Windows can queue, show, store, or submit.
WerFault.exe is the out-of-process WER worker most people notice after an application crash. In the classic user-mode crash path, it is not the first component to learn about the fault. It is launched later and consumes crash state that was already prepared by the faulting process and WER libraries.
WerSvc is the Windows Error Reporting Service. It runs inside svchost.exe, exposes the \WindowsErrorReportingServicePort ALPC endpoint, and coordinates service-side WER behavior.
Crash vertical is the term I use here for the WER reporting lane that handles a particular class of failure. In this post, the important one is the classic user-crash vertical: the path that prepares report state, chooses WerFault.exe or WerFaultSecure.exe, formats the -u -p ... -s ... command line, and starts the worker.
The -s argument in the worker command line is a shared mapping handle, not a file path. It lets the worker recover crash metadata that already exists before WerFault.exe starts.
Scope And Evidence
This series comes from a local reverse-engineering pass over copied Windows binaries and live validation on the same machine family. Unless noted otherwise, the build-specific internals in this post refer to the Windows 10.0.26100.x family I analyzed. Part I is intentionally narrow: it covers the normal classic user-mode crash path, not every WER mode.
That narrowness is deliberate. If this first map is wrong, later posts about WerSvc, secure reporting, registry policy, replacement behavior, and telemetry integrity will inherit bad assumptions. So this post focuses on the exact boundary between kernel exception routing, in-process WER state preparation, service communication, and final worker creation.
The non-claims are just as important:
- This does not prove every WER vertical, hang path, silent process exit path, secure dump path, or live-kernel path.
- This does not recover the full
WerSvcALPC schema or all service-side policy decisions. - This does not claim the exact internal function layout is stable across all Windows builds.
- This does not turn the observed worker command line into proof that the worker discovered the crash by itself.
| Evidence layer | What I used it to prove |
|---|---|
| Kernel route | ntoskrnl!KiDispatchException, DbgkForwardException, NtRaiseException, and DbgkRegisterErrorPort explain exception/debug routing and service-port support, not direct worker creation. |
| User-mode dispatch | ntdll!KiUserExceptionDispatcher, RtlDispatchException, and second-chance NtRaiseException show the handoff back into user-mode exception machinery before WER is involved. |
| WER bridge | KernelBase!UnhandledExceptionFilter loads ext-ms-win-kernel32-errorhandling-l1-1-0.dll; on this machine that API-set contract resolves into KERNEL32.DLL!BasepReportFault. |
| Shared crash state | kernel32!WerpReportFaultInternal creates events, creates and maps a file mapping, duplicates handles, writes crash metadata, and calls ntdll!RtlWerpReportException. |
| Worker launch | Faultrep!GetCrashVerticalPaths formats the -u -p ... -s ... command line, and StartCrashVertical reaches CreateProcessW. |
| Dynamic confirmation | failfast, av, and recovery-av probe modes produced real WerFault.exe -u -p <pid> -s <handle> worker captures. |
The public post is still selective. Part I is about the normal user-mode crash path and the worker spawn boundary. Later posts will cover the full binary atlas, WerFault.exe modes, WerSvc and ALPC, WerFaultSecure.exe, AM-PPL fallback behavior, registry policy, live-kernel reports, and the launcher attack matrix.
The Short Version
For a normal unhandled user-mode exception, the corrected chain is:
CPU fault / RaiseException / FailFast
-> ntoskrnl!KiDispatchException
-> optional debugger/debug-port routing
-> ntdll!KiUserExceptionDispatcher
-> ntdll!RtlDispatchException
-> ntdll!NtRaiseException for second chance if still unhandled
-> KernelBase!UnhandledExceptionFilter
-> LoadLibraryExW("ext-ms-win-kernel32-errorhandling-l1-1-0.dll")
-> KERNEL32.DLL!BasepReportFault
-> KERNEL32.DLL!WerpReportFaultInternal
-> ntdll!RtlWerpReportException
-> ALPC to \WindowsErrorReportingServicePort / WerSvc
-> fallback or service-directed StartCrashVertical
-> CreateProcessW(WerFault.exe or WerFaultSecure.exe ...)
The classic command-line grammar I observed dynamically was:
C:\WINDOWS\system32\WerFault.exe -u -p <faultingPid> -s <sharedMappingHandle>
IDA evidence in Faultrep.dll also shows the optional initiating-process form:
%s -u -p %d -ip %d -s %I64d
%s -u -p %d -s %I64d
That %s is not always WerFault.exe. Protected-process paths can select WerFaultSecure.exe with the same classic -u -p ... -s ... grammar. I am mentioning that now only to keep the model honest; the secure side deserves its own post.
What The Kernel Does
The kernel-side exception path is still central. ntoskrnl!KiDispatchException is the dispatch hub. It handles trap-derived exceptions and raised exceptions, consults debugger state, and arranges delivery into user mode when appropriate. If a debugger/debug object exists, ntoskrnl!DbgkForwardException is part of the route that gets exception information to the debugger.
What I did not find is a normal user-mode crash path where the kernel independently chooses and creates WerFault.exe.
| Component | Observed role in this path |
|---|---|
ntoskrnl!KiDispatchException | Central exception dispatcher. Routes first/second chance handling and debugger delivery. |
ntoskrnl!DbgkForwardException | Sends exception debug events to a process debug object/debug port when present. |
ntoskrnl!NtRaiseException | Syscall re-entry used after user-mode dispatch fails to handle the exception. |
ntoskrnl!DbgkRegisterErrorPort | Registers the WER system error port after WerSvc calls into NtSetSystemInformation. |
\KernelObjects\SystemErrorPortReady | Readiness object used by WER clients/service machinery. |
That last row is easy to misread. Kernel support for the WER service port does not mean the kernel is doing the user crash worker creation. It means the OS has a registered service endpoint for WER messages.
User-Mode Exception Dispatch
Once the exception is delivered back to user mode, ntdll!KiUserExceptionDispatcher becomes the landing pad. It calls ntdll!RtlDispatchException, which walks vectored exception handlers, SEH frames, and language/runtime handlers.
If a handler accepts the exception, execution can continue through the usual context restoration path. If nobody handles it, ntdll!KiUserExceptionDispatcher calls NtRaiseException again for second-chance processing.
That second chance matters because debugger-attached repros are not equivalent to natural WER repros. Microsoft documents UnhandledExceptionFilter as passing unhandled exceptions to a debugger when the process is being debugged, and otherwise entering system error handling behavior. That matches the practical result: if you launch the victim under cdb, you are perturbing the thing you are trying to measure.
The Lazy WER Bridge
The interesting bridge in this build family is KernelBase!UnhandledExceptionFilter.
IDA/decompile evidence shows the unhandled path loading:
ext-ms-win-kernel32-errorhandling-l1-1-0.dll
On the test machine, a live API-set resolution probe mapped that contract to:
C:\WINDOWS\System32\KERNEL32.DLL
From there, the path enters:
KERNEL32.DLL!BasepReportFault
KERNEL32.DLL!WerpReportExceptionInProcessContext
KERNEL32.DLL!WerpReportFault
KERNEL32.DLL!WerpReportFaultInternal
kernel32!WerpReportFaultInternal is where the path starts looking much more like WER internals than generic exception dispatch. The function creates synchronization objects, creates a shared file mapping, duplicates current process/thread handles, writes crash metadata, and calls:
ntdll!RtlWerpReportException
That ntdll path packages the request and uses the WER service port. If the service route fails or a worker is needed, the crash vertical can fall back to direct worker creation.
Where Faultrep.dll Fits
Faultrep.dll is still a main character. The correction is that it is not the first component reached by KernelBase!UnhandledExceptionFilter in the observed in-process bridge.
The careful wording is:
KernelBaseenters the error-handling API-set provider inKERNEL32.DLL; WER then reaches service/fallback launch paths, andFaultrep.dllis central in the worker/reporting implementation and crash-vertical launcher logic.
The high-signal Faultrep.dll names include:
| Name | Why it matters |
|---|---|
GetCrashVerticalPaths | Builds the full worker image path and command line for the classic crash vertical. |
StartCrashVertical | Calls process creation for the crash worker after report state exists. |
WerpInitiateCrashReporting | Shared internal reporting entry used by worker/service paths. |
WerpIsProtectedProcess | Branch point that keeps/selects WerFaultSecure.exe for protected targets. |
WerpLaunchAeDebug | Debugger policy path adjacent to normal WER reporting. |
The mistake would be to erase Faultrep.dll from the story. The better model is to place it correctly: it is central to the crash vertical and worker internals, while the first observed unhandled-exception bridge on this build resolves through the KERNEL32 error-handling provider.
The Worker Creation Boundary
The actual worker creation boundary is the crash vertical:
StartCrashVertical -> GetCrashVerticalPaths -> CreateProcessW
The path builder chooses a system directory, chooses the worker name, formats the command line, and hands it to process creation. For the normal non-protected cases I measured, that produced:
C:\WINDOWS\system32\WerFault.exe -u -p 64372 -s 728
C:\WINDOWS\system32\WerFault.exe -u -p 68564 -s 812
C:\WINDOWS\system32\WerFault.exe -u -p 53656 -s 812
The -s value is not a file path. It is the inherited/shared mapping handle used by the worker to recover crash metadata and process/thread handle information created before the worker launched.
That is an important debugging clue. By the time WerFault.exe starts, it is not discovering the crash from scratch. It is consuming state prepared by the in-process WER path.
After spawn, the worker is not a standalone dumper with mystical crash-detection powers. In the classic user-crash mode, WerFault.exe parses -p, optional -ip, and -s, reopens or duplicates target state, and then re-enters the common WER reporting engine through Faultrep!WerpInitiateCrashReporting. Hang modes, silent process exit, secure dump collection, and live-kernel support are adjacent modes, but they are separate enough to deserve their own posts.
Dynamic Validation
The live validation harness was intentionally simple. I used a .NET 8 probe because dotnet.exe was already available on the machine and cl.exe was not in PATH.
The watcher did three things:
- started the crash victim without a debugger
- polled WMI/CIM for newly spawned
WerFault.exeorWerFaultSecure.exe - recorded observed command lines to CSV while optionally attempting a non-invasive
cdb -pvattach
The non-debugged runs are the important ones for worker-spawn measurement.
| Probe mode | Probe pid | Exit code | Observed WER worker |
|---|---|---|---|
failfast | 64372 | -2146232797 | C:\WINDOWS\system32\WerFault.exe -u -p 64372 -s 728 |
av | 68564 | -532462766 | C:\WINDOWS\system32\WerFault.exe -u -p 68564 -s 812 |
recovery-av | 53656 | 255 | C:\WINDOWS\system32\WerFault.exe -u -p 53656 -s 812 |
submit | n/a | 0 | No out-of-process WER worker observed during the polling window. |
The submit row is not a contradiction. Direct WER API submission is not the same path as an unhandled process crash that needs a crash worker to reopen the faulting process and consume shared crash state.
Debugger Caveat
I did not treat a debugger-launched victim as authoritative for WER worker creation.
The reason is basic but easy to forget: an attached debugger changes unhandled-exception behavior. UnhandledExceptionFilter explicitly has debugger-aware behavior, and in practice a victim launched under cdb is not the same experiment as a victim crashing naturally.
I kept a separate cdb-launched run as a debugger-perturbed control-flow trace. Attempts to attach non-invasively to the short-lived WerFault.exe workers usually lost the race and returned:
HRESULT 0x80004002
That result is still useful. It tells you something about the worker lifetime and why process-command-line capture is a better first validation primitive for this question than late attach stack inspection.
Instrumenting The Service Boundary
One practical way around that debugging problem is to instrument the WER reporting path from inside the process that is about to crash, then let the normal unhandled-exception path run. I published a small x64 Visual Studio proof of concept for that here: CameronBabcock/WerFaultHook.
The POC inline-hooks ntdll!NtAlpcConnectPort in the crashing process and checks the PUNICODE_STRING PortName argument for:
\WindowsErrorReportingServicePort
On the first match, it prints:
Sleeping at werfault service connection
and sleeps for five minutes before letting the real syscall continue. That gives you a service-boundary breakpoint without launching the victim under a debugger first. At that pause, the process has reached the WER service connection path, but WerFault.exe worker creation has not finished racing away from you yet.
This matters because WER is annoying to debug in the obvious way. An attached debugger changes unhandled-exception behavior, and KernelBase!UnhandledExceptionFilter also honors process error mode, thread error mode, secure-process state, and job-object policy before it reaches BasepReportFault. In my local harness, PowerShell had inherited SEM_NOGPFAULTERRORBOX; the raw AV exited as 0xC0000005 before the ALPC connection. Clearing only that parent launcher bit made the same no-filter binary hit the \WindowsErrorReportingServicePort hook.
So I would treat the hook as a clean instrumentation point, not as a replacement trick. It answers a narrow question: did this crash path enter ntdll!SendMessageToWERService and try to connect to the hardcoded WER service port? If yes, you can attach during the sleep and inspect the real state at the service boundary. If no, the failure is often a launch-context or policy condition worth studying in its own right.
Binary Role Map
Here is the compact map I wish I had at the beginning of this pass:
| Binary | Role in the Part I path |
|---|---|
KernelBase.dll | Owns UnhandledExceptionFilter; loads the error-handling API-set contract; adjacent to debugger/WER policy. |
KERNEL32.DLL | Observed API-set provider for BasepReportFault, WerpReportFaultInternal, and crash-vertical fallback on this build. |
ntdll.dll | Handles user-mode exception dispatch and the WER service-client path via RtlWerpReportException and SendMessageToWERService. |
Faultrep.dll | Crash-reporting policy and launcher logic; builds classic WerFault.exe / WerFaultSecure.exe worker command lines. |
WerFault.exe | Normal out-of-process crash worker for non-protected classic user crashes. |
WerFaultSecure.exe | Protected/secure worker used for protected crash verticals and separate secure dump modes. |
wersvc.dll | WerSvc service implementation in svchost.exe -k WerSvcGroup; ALPC server and protected-crash launcher. |
wer.dll | Public WER API/report engine, registered data/files, local dumps, and report store machinery. |
ntoskrnl.exe | Exception/debug dispatch and WER service-port registration support; not the normal WerFault.exe creator. |
werkernel.sys | Live-kernel WER extension path, separate from normal user-mode crash worker creation. |
What This Changes For Analysis
This model gives you cleaner questions.
Instead of asking “why did the kernel launch WerFault.exe?”, ask:
- Did the exception reach
KernelBase!UnhandledExceptionFilter? - Did the API-set bridge resolve into
KERNEL32.DLL!BasepReportFault? - Did
kernel32!WerpReportFaultInternalcreate the crash shared mapping? - Did
ntdll!RtlWerpReportExceptionreachWerSvcover\WindowsErrorReportingServicePort? - Did the service path succeed, or did the path fall back to
StartCrashVertical? - Did
GetCrashVerticalPathschooseWerFault.exeorWerFaultSecure.exe? - What process image and command line did
CreateProcessWactually receive?
Those questions are much easier to validate with debugger traces, IDA xrefs, command-line captures, and service state than a vague kernel-spawn story.
They also change the product-design tradeoff. A kernel process-creation callback can tell you that a WER worker appeared, but it observes the event after the client-side crash state already exists. If you need causality, you have to correlate process creation with exception telemetry, KernelBase/KERNEL32 WER entry, ALPC traffic to \WindowsErrorReportingServicePort, service state, registry/report policy, and the worker command line.
For EDR and authorized CNO telemetry planning, I would split the path into timing windows:
| Timing window | Useful signal | How to read it |
|---|---|---|
| Before the worker | Exception telemetry, UnhandledExceptionFilter, API-set resolution, ALPC connect attempts | Best place to establish causality while the faulting process is still building report state. |
| Worker creation | Parent PID, worker image, command line, -p target, -s handle value | Strong correlation point, but late; validate that the worker points back to existing crash state. |
| After the worker | Dump/report files, service events, registry policy, worker exit | Useful for outcome and artifact collection, weaker for proving who selected the worker. |
That is the setup for the later security work. Replacement behavior, search-order ambiguity, protected-process signer checks, AM-PPL fallback, and ALPC request validation all depend on knowing exactly where the worker image is selected and who gets to influence that choice.
Evidence Appendix
The snippets below are normalized from local IDA/CDB captures and runtime watcher logs. They are intentionally short: enough to show the load, state-preparation, and launch boundaries without turning the post into a raw dump of proprietary disassembly.
First, the KernelBase bridge. The unhandled-exception path checks debugger state, then lazily loads the error-handling API-set contract and calls into the report-fault bridge:
KernelBase!UnhandledExceptionFilter
BasepIsDebugPortPresent()
LoadLibraryExW(L"ext-ms-win-kernel32-errorhandling-l1-1-0.dll", nullptr, 0)
BasepReportFault()
On this machine, that API-set contract resolved to KERNEL32.DLL, which is why the next observed internal edge is the KERNEL32.DLL!BasepReportFault / WerpReportFaultInternal family rather than an immediate jump into Faultrep.dll.
The kernel32!WerpReportFaultInternal CDB capture shows the in-process state handoff before the worker exists:
kernel32!WerpReportFaultInternal
call qword ptr [kernel32!_imp_CreateEventW]
call qword ptr [kernel32!_imp_CreateFileMappingW]
call qword ptr [kernel32!_imp_MapViewOfFile]
call qword ptr [kernel32!_imp_DuplicateHandle]
call qword ptr [kernel32!_imp_DuplicateHandle]
call qword ptr [kernel32!_imp_RtlWerpReportException]
That call cluster is the reason I treat -s as a shared crash-state handle rather than decoration on the worker command line.
The crash-vertical launcher is where worker image selection and command-line construction become explicit:
Faultrep!GetCrashVerticalPaths
worker = WerpIsProtectedProcess(Process)
? L"WerFaultSecure.exe"
: L"WerFault.exe"
ApplicationName = L"%s\\%s"
CommandLine = L"%s -u -p %d -ip %d -s %I64d"
CommandLine = L"%s -u -p %d -s %I64d"
Faultrep!StartCrashVertical
CreateProcessW(ApplicationName, CommandLine, ..., inheritHandles = TRUE, ...)
The live watcher then produced the same shape for real non-debugged crashes:
mode=failfast
probe_pid=64372
seen WerFault.exe pid=35252 ppid=64372
cmd=C:\WINDOWS\system32\WerFault.exe -u -p 64372 -s 728
probe_exit_code=-2146232797
That dynamic line is the sanity check for the static analysis: the process that crashes is the parent, the command line points back to that process with -p, and the worker receives an already-created mapping handle with -s.
Public Documentation Cross-Checks
Microsoft’s public documentation gives the outer contract, not the internals:
UnhandledExceptionFilterdocuments the debugger-aware unhandled-exception behavior.- WER Functions documents the public Windows Error Reporting API surface.
- Collecting User-Mode Dumps documents the
LocalDumpsconfiguration surface and the fact that dump collection occurs before process termination completes. - Using Application Recovery and Restart gives context for recovery/restart behavior around crash handling.
Those pages are useful boundaries. The reverse-engineering work here fills in the path between the public contract and the actual worker process you see in a live system.
Part I Bottom Line
For normal user-mode crashes on this Windows build family:
- the kernel dispatches the exception and handles debugger/service-port machinery
ntdllperforms user-mode exception dispatch and WER service messagingKernelBasebridges unhandled exceptions into WERKERNEL32.DLLhosts the observed error-handling implementation behind the API-set contract- WER creates shared crash state before the worker starts
WerFault.exeis created by user-mode WER crash-vertical code, not directly by the kernel
Part II will step back and show how I built the binary atlas that made those claims tractable: which binaries were pulled apart, what evidence was extracted, and how the function/string/xref map kept the analysis from turning into folklore with better formatting.