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

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.exe actually is in the crash-reporting pipeline
  • where the crashing process still participates before the worker starts
  • why a WerFault.exe process 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:

BoundaryOwnerWhat to remember
Exception routingKernel and ntdll dispatchThe OS routes first- and second-chance exception state; that does not prove worker creation.
In-process WERKernelBase, KERNEL32, and ntdll WER client codeThe faulting process builds report state and service messages before the worker exists.
Worker launchWER crash-vertical code, including Faultrep.dll pathsUser-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.

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. 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 WerSvc ALPC 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 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.

For EDR and authorized CNO telemetry planning, I would split the path into timing windows:

Timing windowUseful signalHow to read it
Before the workerException telemetry, UnhandledExceptionFilter, API-set resolution, ALPC connect attemptsBest place to establish causality while the faulting process is still building report state.
Worker creationParent PID, worker image, command line, -p target, -s handle valueStrong correlation point, but late; validate that the worker points back to existing crash state.
After the workerDump/report files, service events, registry policy, worker exitUseful 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:

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.