Skip to content
Cameron Babcock Cameron Babcock
← Back to the archive
June 3, 2026 14 min read Updated June 3, 2026

WerFault Internals Part I: From KiDispatchException to WerFault.exe

A reverse-engineering walk from Windows user-mode exception dispatch to the actual WerFault.exe worker launch, with symbol evidence, command-line captures, and the boundary between kernel routing and user-mode WER process creation.

The first mistake I had to kill in my own model was simple:

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.

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, “the kernel launches WerFault” 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.

That also changes how I would instrument this in an endpoint product. A WerFault.exe process-creation event is late telemetry: by then, the faulting process has already entered in-process WER code, built crash state, and handed the worker a shared mapping handle. 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.

Flow diagram showing exception dispatch from CPU fault through KiDispatchException, KernelBase, KERNEL32, an attempted WerSvc service path, service failure edge cases, and StartCrashVertical worker creation.
Figure 1. The normal user-mode crash path. Kernel exception dispatch is real, but the worker process is created later by user-mode WER launch code.

Scope And Evidence

This series comes from a local reverse-engineering pass over copied Windows binaries and live validation on the same machine family. Part I is intentionally narrow, but the evidence is not just a diagram and a hunch.

Evidence layerWhat I used it to prove
Kernel routentoskrnl!KiDispatchException, DbgkForwardException, NtRaiseException, and DbgkRegisterErrorPort explain exception/debug routing and service-port support, not direct worker creation.
User-mode dispatchntdll!KiUserExceptionDispatcher, RtlDispatchException, and second-chance NtRaiseException show the handoff back into user-mode exception machinery before WER is involved.
WER bridgeKernelBase!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 statekernel32!WerpReportFaultInternal creates events, creates and maps a file mapping, duplicates handles, writes crash metadata, and calls ntdll!RtlWerpReportException.
Worker launchFaultrep!GetCrashVerticalPaths formats the -u -p ... -s ... command line, and StartCrashVertical reaches CreateProcessW.
Dynamic confirmationfailfast, 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.

ComponentObserved role in this path
ntoskrnl!KiDispatchExceptionCentral exception dispatcher. Routes first/second chance handling and debugger delivery.
ntoskrnl!DbgkForwardExceptionSends exception debug events to a process debug object/debug port when present.
ntoskrnl!NtRaiseExceptionSyscall re-entry used after user-mode dispatch fails to handle the exception.
ntoskrnl!DbgkRegisterErrorPortRegisters the WER system error port after WerSvc calls into NtSetSystemInformation.
\KernelObjects\SystemErrorPortReadyReadiness 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:

KernelBase enters the error-handling API-set provider in KERNEL32.DLL; WER then reaches service/fallback launch paths, and Faultrep.dll is central in the worker/reporting implementation and crash-vertical launcher logic.

The high-signal Faultrep.dll names include:

NameWhy it matters
GetCrashVerticalPathsBuilds the full worker image path and command line for the classic crash vertical.
StartCrashVerticalCalls process creation for the crash worker after report state exists.
WerpInitiateCrashReportingShared internal reporting entry used by worker/service paths.
WerpIsProtectedProcessBranch point that keeps/selects WerFaultSecure.exe for protected targets.
WerpLaunchAeDebugDebugger 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.

Master control-flow diagram showing the faulting process, KernelBase and ntdll WER path, WerSvc, Faultrep crash vertical, and the worker process.
Figure 2. The broader Part I control-flow model: service path and fallback/direct worker creation both converge on the crash-vertical worker.

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.exe or WerFaultSecure.exe
  • recorded observed command lines to CSV while optionally attempting a non-invasive cdb -pv attach

The non-debugged runs are the important ones for worker-spawn measurement.

Probe modeProbe pidExit codeObserved WER worker
failfast64372-2146232797C:\WINDOWS\system32\WerFault.exe -u -p 64372 -s 728
av68564-532462766C:\WINDOWS\system32\WerFault.exe -u -p 68564 -s 812
recovery-av53656255C:\WINDOWS\system32\WerFault.exe -u -p 53656 -s 812
submitn/a0No 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:

BinaryRole in the Part I path
KernelBase.dllOwns UnhandledExceptionFilter; loads the error-handling API-set contract; adjacent to debugger/WER policy.
KERNEL32.DLLObserved API-set provider for BasepReportFault, WerpReportFaultInternal, and crash-vertical fallback on this build.
ntdll.dllHandles user-mode exception dispatch and the WER service-client path via RtlWerpReportException and SendMessageToWERService.
Faultrep.dllCrash-reporting policy and launcher logic; builds classic WerFault.exe / WerFaultSecure.exe worker command lines.
WerFault.exeNormal out-of-process crash worker for non-protected classic user crashes.
WerFaultSecure.exeProtected/secure worker used for protected crash verticals and separate secure dump modes.
wersvc.dllWerSvc service implementation in svchost.exe -k WerSvcGroup; ALPC server and protected-crash launcher.
wer.dllPublic WER API/report engine, registered data/files, local dumps, and report store machinery.
ntoskrnl.exeException/debug dispatch and WER service-port registration support; not the normal WerFault.exe creator.
werkernel.sysLive-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!WerpReportFaultInternal create the crash shared mapping?
  • Did ntdll!RtlWerpReportException reach WerSvc over \WindowsErrorReportingServicePort?
  • Did the service path succeed, or did the path fall back to StartCrashVertical?
  • Did GetCrashVerticalPaths choose WerFault.exe or WerFaultSecure.exe?
  • What process image and command line did CreateProcessW actually 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.

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:

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
  • ntdll performs user-mode exception dispatch and WER service messaging
  • KernelBase bridges unhandled exceptions into WER
  • KERNEL32.DLL hosts the observed error-handling implementation behind the API-set contract
  • WER creates shared crash state before the worker starts
  • WerFault.exe is 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.