Skip to content

Every process creation strands an EPROCESS object in nonpaged pool (Proc tag); dev machines with high process churn become unusable within hours #137

Open

Description

@dwoodford-ita

STATUS, updated 2026-08-16. This does not reproduce on other machines
running the same Windows build. A control machine (ASUS Vivobook, same build)
shows 0.07 leaked objects per launch against 1.9-2.0 here, at comparable
uptime, and it runs the same ASUS drivers and services. Treat this as
machine-specific until a trigger is identified.
Three root-cause
attributions originally published here have been retracted; see the corrected
mechanism section below. My earlier speculation about Windows Server assumed a
platform-wide defect and should be discounted entirely.

Windows Build Number

10.0.26200.0 (25H2, build 26200.9168)

Processor Architecture

AMD64

Memory

31 GB usable (2 x 16 GB DDR5-4800)

Storage Type, free / capacity

C: NVMe SSD 576 GB free / 924 GB

Relevant apps installed

Podman Desktop (containers on WSL2)
WSL2 (VirtualMachinePlatform enabled; full Hyper-V role not installed)
Visual Studio Code
Git for Windows
Amazon Corretto JDK 8, Eclipse Temurin JDK 21
.NET SDK
Python 3.12
Microsoft Defender (WdFilter.sys active)
NVIDIA drivers + NVIDIA App

Note: ASUS Armoury Crate was fully removed with the official uninstall tool
before these measurements. The leak continued unchanged afterwards.

Traces collected via Feedback Hub

https://aka.ms/AA1324dy

Trace captured with WPR:
wpr -start Pool -start Handle -start VirtualAllocation -filemode
wpr -stop proc-leak.etl
Result: 5.55 GB, 43,499,201 events, 0 lost.

Also available on request: raw !obtrace output for six leaked process objects
(12 MB), showing the full reference/dereference history of each.

Isssue description

Process objects are never freed on this machine. Every process that exits leaves
its EPROCESS block allocated and still linked into the active process list.
Nonpaged pool climbs continuously until the machine becomes unresponsive, and
only a reboot clears it.

This belongs to the developer inner loop because the leak scales with process
creation rate. Container tooling, WSL, Git and build tools spawn short-lived
processes constantly. This system creates roughly 25,000 processes per hour,
about two orders of magnitude above a typical desktop, so a defect that is
invisible elsewhere makes a dev machine unusable within a single working day.

MECHANISM (corrected 2026-08-16 - see retractions below)

I enabled object reference tracing for the Proc tag (gflags /ko /t Proc), took a
kernel mirror dump with LiveKd, and ran !obtrace offline.

What is established:

After process exit the EPROCESS reaches HandleCount 0 / PointerCount 1.
One untagged Dflt reference survives indefinitely, preventing final object
deletion. THE CURRENT EVIDENCE DOES NOT IDENTIFY ITS OWNER.

Confirmed with a probe that creates bait via CreateProcessW and closes its own
handles before measuring: no handle is held on the leaked objects at all, by any
process, from the moment the creating handle closes, polled for 10 minutes. The
retention is a raw reference, not a handle.

RETRACTED ATTRIBUTIONS

Three separate attributions were published in this issue and all three are
withdrawn. Each failed the same way: a balance of one outstanding reference under
a shared tag does not assign to any particular acquisition.

  1. win32kbase!ForegroundBoost::LogProcessInformation. WITHDRAWN. Derived by
    taking the last unmatched acquisition under the Dflt tag. That method cannot
    identify which reference is outstanding.

  2. NtOpenProcess / PsOpenProcess. WITHDRAWN. Post-exit NtOpenProcess activity is
    balanced: it adds a lookup reference and a handle, then releases both.

  3. WdFilter / PsLookupProcessByProcessId. WITHDRAWN. The trace does show a
    +32768 Dflt reference-count cache refill through WdFilter during process
    creation, and a -32687 drain during rundown leaving the tag count at one. But
    the trace also contains WdFilter dereferences, and Dflt is the common tag for
    untagged references, so the surviving unit is not assignable to the refill.
    Per Microsoft's guidance, attribution requires matching reference and
    dereference calls under a DISTINCT tag; these all share Dflt.

WHAT I COULD NOT ESTABLISH

  • The owner of the surviving reference. There is no supported user-mode
    enumeration of who holds a raw object reference.
  • Sample size. Ten traced objects, all bait processes created seconds apart.
    Attempts to widen this ended in hard system locks before producing output.
  • The trigger. I could not determine which component repeatedly enables
    providers. Active ETW sessions were stable at 29 with no churn over several
    minutes, so the enable calls happen to providers inside sessions that already
    exist.

The leak itself is not in doubt. The precise culprit within it carries the
caveat above.

RELATED REPORTS

Users are reporting this symptom without reaching a cause:

Note the hardware spread: the first report is an Intel i7-13700K on a Z790-E
desktop, mine is an AMD Ryzen 9 7845HX laptop on a later build. Not CPU-vendor
specific, not chipset specific, and it survives into 25H2.

Steps to reproduce

TO OBSERVE THE LEAK

  1. Run a workload that creates short-lived processes continuously. Container
    tooling on WSL2 is sufficient; so is any build or test loop that shells out.

  2. Sample the Proc pool tag and the live process count periodically. poolmon
    works, or NtQuerySystemInformation(SystemPoolTagInformation) via P/Invoke.

  3. Compare Proc outstanding allocations against the live process count. The gap
    is the leak, and it only grows.

A DETERMINISTIC TEST

copy C:\Windows\System32\hostname.exe C:\temp\leakbait.exe
(launch 24 instances, all exit within 10 seconds)

Then against a kernel dump:

!process 0 0 leakbait.exe

All 24 EPROCESS blocks were still resident. They appear with
ObjectTable: 00000000 and HandleCount: 0, but remain linked into the active
process list.

TO REPRODUCE THE DIAGNOSIS

Several steps cost me significant time and are worth stating.

  • Setting ObTracePoolTags, ObTraceProcessName and ObTracePermanent in the
    registry does NOT enable object reference tracing. After a reboot with all
    three set, !obtrace still reported "Object Reference tracing not enabled".
    Use gflags /ko /t Proc, which takes effect immediately and needs no reboot.

  • Do NOT add /p (permanent traces). Trace data is then retained beyond the
    object's life, and on a system where every process object leaks, that grows
    without bound.

  • Analyse a mirror dump (livekd64 -m -o kernel.dmp, 2.5 GB in about 3 seconds),
    not a live view. LiveKd's live view resolves every read through its driver
    against running memory and is punishingly slow.

  • Do NOT run a bare !process 0 0. It hard-locked this machine three times, under
    both LiveKd and offline against the dump, because the active process list holds
    thousands of dead entries. The filtered form (!process 0 0 ) completed
    in 18 seconds every time.

  • Symbols:
    set _NT_SYMBOL_PATH=srvC:\symbolshttps://msdl.microsoft.com/download/symbols

Expected Behavior

Proc outstanding allocations track the live process count. When a process exits,
its EPROCESS object is dereferenced by every component that referenced it, the
object is freed, and it is unlinked from the active process list.

Nonpaged pool usage should therefore be roughly flat over time for a steady
workload, regardless of how many processes have been created and destroyed.

Actual Behavior

Proc outstanding grows without bound and never returns. Exited processes remain
in the active process list indefinitely.

MEASUREMENTS

96 seconds after a clean boot, nothing unusual running:

Proc outstanding : 3,109
live processes   :   245

23 minutes after boot:

Proc outstanding : 11,749
live processes   :   287

Growth is roughly 25,000/hour on this workload. Thre tracks it, since threads are
pinned by the processes holding them, and Job grows alongside.

CONFIRMED TWO INDEPENDENT WAYS

ETW: a two-minute capture with the Pool, Handle and VirtualAllocation profiles
recorded 855 process creations and 853 exits, with the live process count flat
throughout. All 855 process objects were still outstanding at the end of the
trace. A 100% leak rate on processes created during the window.

Controlled test: 24 of 24 deliberately launched processes leaked, as described
under Steps to reproduce.

RETAINED OBJECTS IN THE ACTIVE PROCESS LIST

On one boot, 569 of the first 774 entries (73.5%) were exited processes still
being held, including an smss.exe from startup. Top images among them, which is a
fair picture of a dev machine's process churn:

conhost.exe    169
wsl.exe        118
podman.exe      45
logman.exe      31
wslhost.exe     22

IMPACT

  • The machine becomes unresponsive after roughly 15 hours of normal dev work,
    with free RAM in the tens of megabytes.
  • A reboot is the only remedy.
  • Measurable mitigation: quitting Podman Desktop cut Proc growth from about
    22,300/hour to 8,229/hour (a 63% reduction) and took Job growth from roughly
    102,000/hour to zero. That is a workaround by not doing the work, not a fix.

Activity

  1. dwoodford-ita commented on Aug 16, 2026

    @dwoodford-ita
    Author

    Follow-up measurement: most of the cost is outside kernel pool

    I measured what one leaked process actually costs, using a control phase and a treatment
    phase of identical length so background churn is subtracted rather than counted as signal.
    1,000 processes launched, three containers running throughout, and no debugger involved.

    Cost per leaked process

    Commit charge 79.2 KB
    All kernel pool tags together 12.3 KB
    of which Proc (the EPROCESS block) 3.5 KB

    Roughly 84% of the cost sits outside kernel pool.

    I think this is the most useful thing in this report, because it explains why every public
    investigation of this symptom dead-ends. People reach for poolmon, watch the Proc tag grow
    by a few megabytes, cannot reconcile that with gigabytes of missing memory, and conclude the
    numbers do not add up. They do add up. The pool figure is only a sixth of the story.

    Which tags scale with it

    Control-adjusted, per leaked process:

    Tag KB/proc objects/proc
    Proc 3.50 1.00
    Toke 1.76 0.00
    MiP2 1.50 1.00
    Thre 0.21 0.06

    MiP2 tracking exactly 1:1 alongside Proc looks like the most promising lead, since a
    memory manager structure pinned per process is a plausible home for the 79 KB of commit that
    pool does not account for.

    Correction to my original report

    I wrote above that "Thre tracks it, since threads are pinned by the processes holding them."
    That is wrong. Thre comes out at 0.06 objects per leaked process, so thread objects are
    being released normally. The retained thread counts I saw earlier were not caused by this
    mechanism.

    Caveat on that: the test processes were single-threaded, so I cannot say whether a
    multi-threaded process retains thread objects.

    The resulting ceiling

    process budget  ~=  RAM / 79 KB
    
    RAM Budget At ~25,000 proc/hour At ~10,000/hour
    16 GB ~212,000 ~8 hours ~21 hours
    32 GB ~424,000 ~17 hours ~42 hours
    64 GB ~848,000 ~34 hours ~85 hours

    The 32 GB row predicts 17 hours. This machine becomes unusable at about 15. That is an
    independent check on the figure, since 79.2 KB came from a four-minute controlled experiment
    and the 15 hours came from ordinary use.

    Two methodology notes

    The available-memory counter is not usable here. It gave 168 KB per process in the
    treatment phase and 341 KB in the control, because it moves with cache and standby. Commit
    charge is the stable measure.

    A leak rate above 100% is expected, not an artifact. Launching 1,000 processes produced
    about 1,675 attributable objects, because each launch also creates a conhost.exe. Two
    processes per launch. It is consistent with one leaked object per process created, and it
    explains why conhost.exe tops the leaked-image table.

  2. dwoodford-ita commented on Aug 16, 2026

    @dwoodford-ita
    Author

    Second symptom axis: process enumeration gets measurably slower

    Memory is not the only thing that degrades. Because the leaked EPROCESS blocks stay linked
    into the active process list, every caller of
    NtQuerySystemInformation(SystemProcessInformation) has to traverse them, which includes
    Task Manager, Get-Process, WMI and anything that polls process state.

    Measured on the same machine by timing that call directly, warmed up first, before and after
    adding a known number of leaked objects:

    before   Proc = 28,199   NtQuerySystemInformation = 4.56 ms
    after    Proc = 30,419   NtQuerySystemInformation = 6.26 ms
                             +2,220 objects  ->  +1.70 ms  (+37%)
    

    About 0.767 ms per 1,000 leaked objects.

    Note the dead entries are filtered out of the result: at 11,749 leaked objects Get-Process
    still returned only 287 processes. They are traversed but not reported, so the cost is paid
    with nothing visible to explain it.

    What this implies at scale

    Linear extrapolation to the ~400,000 leaked objects a 32 GB machine reaches before becoming
    unusable gives roughly 290 ms per enumeration, against about 10 ms healthy.

    That is a 180x extrapolation from the measured range, so treat it as indicative rather than
    a result.
    I would not rely on it without measuring nearer the top of the range.

    At that level, Task Manager refreshing once a second would spend around 30% of a core purely
    traversing dead process objects, and every other poller pays the same cost concurrently.

    A hypothesis worth checking with source access

    I could not measure this, but it seems worth flagging. If enumeration holds the process list
    lock while walking, then a several-hundred-millisecond walk would block process creation and
    exit for its duration. On a machine already creating several processes per second, with
    multiple pollers each starting their own walk, that could serialise into something
    indistinguishable from an unresponsive desktop.

    I measured elapsed enumeration time only. I did not measure lock hold time or contention,
    so this is speculation about a mechanism, not a finding.

    Why it matters for reproduction

    Anyone trying to reproduce this should expect their own diagnostic tools to degrade as the
    leak grows, which makes the failure harder to observe the closer it gets to happening. It also
    means "the machine became unresponsive" reports of this bug may not all be memory exhaustion.

  3. dwoodford-ita commented on Aug 16, 2026

    @dwoodford-ita
    Author

    Question: does this affect Windows Server 2025, and does hotpatching make it worse?

    I have no Windows Server instance to test on, so this is a question rather than a report.
    Someone with source access can probably answer it in minutes, and if the answer is yes the
    impact profile is quite different from a developer workstation.

    Why I suspect Server 2025 shares the code path

    The leaking function is in win32kbase.sys, which is the base module shared across
    editions, rather than win32kfull.sys (full desktop and GDI) or win32kmin.sys (the minimal
    headless variant). If the ForegroundBoost TraceLogging enable callback lives in the shared
    base, it is presumably present on Server including Server Core.

    Windows Server 2025 is built on the same Germanium base as Windows 11 24H2 (build 26100). The
    machine in this report is 26200. Same lineage.

    Windows Server 2022 (build 20348) predates SetAdditionalForegroundBoostProcesses, so I would
    not expect this specific path to exist there.

    Why the impact profile inverts on a server

    A server does not churn processes the way a dev machine does. IIS serves requests on threads
    inside w3wp rather than spawning processes, so a busy application server might create only a
    few hundred processes an hour against the ~25,000/hour measured here.

    But the budget is consumed by process count, not elapsed time, and servers have far longer
    uptime. Using the 79 KB per leaked process measured earlier in this issue:

    Server RAM Budget At 500 proc/hour At 200/hour
    32 GB ~424,000 ~35 days ~88 days
    64 GB ~848,000 ~70 days ~176 days
    128 GB ~1.7M ~141 days ~353 days

    A workstation fails in hours at high churn. A server would fail in weeks or months at low
    churn. Same defect, same arithmetic.

    Windows build agents would be the worst case, since compilers, linkers, test runners and
    source control churn processes hard while running on server SKUs.

    The part I think is worth checking

    Monthly patch reboots may be hiding this entirely. Most servers restart every few weeks
    for updates, which would reset the counter well before anything accumulates. That could be the
    whole reason there are no Server reports.

    Windows Server 2025 introduced hotpatching, which applies security updates without
    restarting, with reboots only on quarterly baselines. That takes a well-maintained server from
    roughly 30 days between reboots to roughly 90.

    If the arithmetic above holds, hotpatching would move a 32 GB server from inside the safe
    window to outside it, and the symptom would appear as gradual memory exhaustion on
    long-uptime servers with no obvious cause. It would very likely be attributed to the
    application rather than the platform.

    A cheap read-only check for anyone with a server

    No debugger needed. Compare the Proc pool tag's outstanding allocation count against the
    live process count. poolmon shows it, or NtQuerySystemInformation(SystemPoolTagInformation).

    On a healthy machine the two track each other. If a long-uptime Server 2025 box shows Proc
    outstanding in the tens of thousands against a couple of hundred live processes, it is the
    same defect.

    Happy to run any test you like on the client machine, but I cannot cover the Server side.

  4. dwoodford-ita commented on Aug 16, 2026

    @dwoodford-ita
    Author

    Correction: this does not reproduce on two other machines, and one of my stated facts was wrong

    Two important updates that change how this report should be read.

    1. It does not reproduce on other machines with the same build

    I ran the same deterministic test (launch a known number of short-lived processes, confirm
    they exit, compare Proc outstanding growth against the launch count) on two other machines
    running the same Windows 11 edition. Neither leaks.
    Process objects are freed normally on
    both.

    So whatever is happening here is not something every machine on this build does. My
    original report implied it was, and that was wrong. Anyone triaging should treat this as
    machine-specific until the trigger is identified.

    The !obtrace evidence in the original report still stands on its own terms: on this machine
    every leaked object holds exactly one stray reference, the trace accounting reconciles exactly
    against an independent PointerCount, and the last unmatched acquisition is the
    ForegroundBoost TraceLogging enable callback. What I can no longer claim is that this
    generalises.

    If the mechanism is real, a plausible reading is that the callback path only leaks when
    something enables ETW providers repeatedly, and the clean machines simply never execute it.
    That would make this a latent defect with a third-party trigger rather than a defect everyone
    with high process churn will hit. I cannot demonstrate that, and I am flagging it as a
    hypothesis.

    2. Retracting a statement in the original report

    Under "Relevant apps installed" I wrote:

    Note: ASUS Armoury Crate was fully removed with the official uninstall tool before these
    measurements. The leak continued unchanged afterwards.

    That is false and I apologise for it. The official uninstaller reported success and
    rebooted the machine, and I took that at face value without verifying. Enumerating running
    services and drivers afterwards shows the following still present:

    services : ArmouryCrateControlInterface, ASUSSystemAnalysis, ASUSSystemDiagnosis,
               AsusAppService, ASUSOptimization, ASUSSoftwareManager, ASUSSwitch,
               AsusMsControl, AsusPTPService
    drivers  : AsusSAIO ("ASUS System Analysis IO"), ATKWMIACPIIO, AsusPTPDrv
    

    That statement would have steered anyone away from a live line of enquiry, which is the worst
    kind of error to leave in a bug report.

    3. ASUS user-mode services are not the trigger

    Having found them still running, I tested them directly. Same launch count per phase, with
    background subtracted in both:

    Phase A, 8 ASUS services running : 1.97 leaked objects per launch
    Phase B, all 8 stopped           : 1.79 leaked objects per launch
                                       9% change, within noise
    

    So the ASUS services are not responsible. The three ASUS kernel drivers remain loaded in
    both phases
    , so ASUS as a whole is not cleared, only its user-mode services.

    Where this leaves the report

    • The leak is real and measured on this machine, including the per-process cost and
      enumeration scaling in the comments above.
    • It is not reproducible on two other machines on the same build.
    • The trigger is unidentified. ASUS services are ruled out; ASUS kernel drivers, NVIDIA, AMD
      and MediaTek drivers are not.
    • My earlier speculation about Windows Server should be discounted accordingly, since it
      assumed a platform-wide defect.

    Next step here is to diff the ETW session and provider list, and the loaded driver set,
    against one of the clean machines. I will report back either way. If this turns out to be a
    third-party driver rather than a Windows defect, I will say so plainly and this issue can be
    closed.

  5. dwoodford-ita commented on Aug 16, 2026

    @dwoodford-ita
    Author

    Retractions, and where this investigation actually ended

    Three claims in this report are wrong. Correcting them properly, and summarising what does and does not survive.

    Retraction 1: win32kbase!ForegroundBoost::LogProcessInformation is not the holder

    This was the headline of the original report. It is wrong.

    I identified it by taking the last unmatched acquisition under a tag with a non-zero final count. References under one tag are interchangeable, so that method cannot identify which reference is actually outstanding. I flagged it as an inference at the time, but I still led with it, and that was a mistake.

    Two things were also wrong with the objects I traced: they were spawned with PowerShell -PassThru, so my own script held an open handle to each, and the dump was taken ten seconds after exit, so teardown had not completed. HandleCount was 1, not 0.

    Re-running with no handle retained, a distinct bait name, and a 60-second settle, then netting by tag rather than by caller, gives a different and much cleaner answer on 10 of 10 objects:

    === references still outstanding at end of trace, by tag ===
      Dflt   10   raised by: nt!PsOpenProcess x10
      ObHn   10   raised by: nt!PsOpenProcess x10
    
          nt!ObReferenceObjectByPointerWithTag+53
          nt!ObOpenObjectByPointer+93
          nt!PsOpenProcess+574
          nt!NtOpenProcess+23
          nt!KiSystemServiceCopyEnd+28
    

    Both stray references come from NtOpenProcess, arriving as a syscall from user mode. win32kbase does not appear.

    Retraction 2: Defender is not leaking 32,768 references per process

    I reported WdFilter taking 32,768 references per process via PsLookupProcessByProcessId with zero releases. That was an artifact of netting per caller instead of per tag: a reference is frequently released from a different stack than the one that acquired it, so the acquirer looks like a leaker and the releaser looks negative. In the same data WdFilter nets -530 as a releasing module, which should have told me the method was broken.

    The arithmetic also refuted it directly. PointerCount on those objects is 2, not 32,770, so the batch had plainly been returned. That is the object reference cache behaving normally.

    Retraction 3: the machine-specific framing

    Already noted above, but to consolidate: this does not reproduce on other machines. A control machine, an ASUS Vivobook on the same Windows 11 build, at comparable uptime:

    Control (clean) Reporting machine
    Uptime 14h 11m 18h 05m
    Live processes 269 284
    Proc outstanding 327 39,511

    Deterministic test, launching 300 short-lived processes and measuring retained process objects per launch: 0.07 on the control, 1.9 to 2.0 here.

    My speculation about Windows Server and hotpatching assumed a platform-wide defect and should be discounted entirely.

    What was eliminated

    Every one of these was tested with the same measurement, and none moved the result off ~2.0 per launch:

    Suspect Result
    ASUS services (9), devices, drivers No change. The clean control machine runs the same ASUS drivers and services.
    All third-party services (clean boot, 0 running) No change
    Memory Integrity / HVCI No change
    VirtualMachinePlatform / WSL2 removed No change
    NVIDIA dGPU disabled No change
    NVIDIA Display Container stopped No change
    Defender exclusions No change
    Defender real-time protection fully off No change (1.99)

    That last one is worth stating clearly, because ETW pointed straight at it. Microsoft-Windows-Kernel-Audit-API-Calls shows MsMpEng.exe as by far the top caller of PsOpenProcess, opening each new process about 8 times within seconds of creation. With real-time protection, behaviour monitoring and on-access protection all disabled, the leak was unchanged. Defender is the loudest caller of the leaking API but is not the leaker.

    Untested: the hypervisor, which is firmware-enforced on this machine and survived hypervisorlaunchtype off, vsmlaunchtype off, EnableVirtualizationBasedSecurity=0, HVCI off and VirtualMachinePlatform removal. Also the AMD, MediaTek and Cirrus driver stacks.

    A methodology note for anyone repeating this

    My A/B harness has an ordering artifact: the second phase reads about 1.0 higher per launch than the first, consistently, regardless of the variable under test. Conclusions above are still safe because a real culprit would drive the second phase toward zero and none did, but a partial effect could have been masked. An A/B/A design would be better.

    What still stands

    • The leak is real and reproducible on this machine, 20 of 20 bait processes retained after a full settle.
    • Cost is 79.2 KB of commit per leaked process, measured with a control phase, with only 12.3 KB of that in kernel pool. The 84%-outside-pool finding is the one I would still call useful, since it explains why poolmon investigations of this symptom dead-end.
    • Enumeration cost scales at about 0.767 ms per 1,000 leaked objects.
    • The leaking call is NtOpenProcess, and the caller is in user mode.

    Recommendation

    Given it does not reproduce on other machines on the same build, this is probably not a Windows defect and I would understand this being closed. If it is useful to keep open as a data point on NtOpenProcess reference accounting, the raw !obtrace output and the mirror dump are still available.

    Apologies for the noise in the original report. The measurements were sound; several of my conclusions from them were not.

  6. dwoodford-ita commented on Aug 17, 2026

    @dwoodford-ita
    Author

    Resolved (probably): an AMD display driver, and the vendor still recommends the leaking version

    Updating the AMD Radeon graphics driver stopped the leak. Posting the full result
    because the diagnostic path may be useful to others, and because one part of this
    is actionable for anyone with the same laptop.

    The fix

    Installing AMD's current driver replaced amdkmdag.sys 32.0.11002.41 with
    32.0.21045.1000. The indicator is the Proc pool tag's outstanding
    allocation count against the live process count:

    Uptime Proc outstanding Live processes Gap
    1m 15s (leaking) 2,655 228 2,427
    18h 05m (leaking) 39,511 284 39,227
    after the driver update 312 285 27

    Evidence, three independent lines

    1. Safe Mode. Both Safe Mode Minimal and Safe Mode with Networking leaked
      exactly zero raw Proc growth across 300 launches. amduw23g, the AMD
      Radeon kernel-mode display driver, does not load in Safe Mode.
    2. A control machine. An ASUS Vivobook on the same Windows build, Intel
      graphics, no AMD display driver: 0.07 leaked objects per launch against
      2.0 on the affected machine, at comparable uptime.
    3. The update itself. The leak stopped in the same window as the driver
      replacement.

    Not proven. A third-party antivirus uninstall and a hard crash landed in the
    same twenty-minute window. The crash is weak as an explanation, since twelve
    prior reboots never fixed it, and the antivirus uninstall restored the exact
    configuration the machine had leaked in for sixteen hours. But neither is
    excluded.

    The actionable part: the vendor recommends the leaking driver

    The leaking version was amdkmdag.sys 32.0.11002.41. As of 2026-08-16 the
    ASUS support page for the ROG Strix G713PV still lists AMD Graphics Driver
    32.0.11002.41, dated 2024-06-17
    , as a RECOMMENDED download. The only other
    AMD graphics driver offered there is 31.0.21902.1002 from 2024-01-10.

    Both recommended options are roughly two years old. Setting this machine up by
    following the manufacturer's own support page installs the leaking driver.
    That
    is presumably why it was installed in the first place.

    Corrections to earlier comments in this issue

    Windows Defender is eliminated by direct test. I previously reported that
    disabling real-time protection did not change the leak, and a reviewer correctly
    objected that this never unloaded WdFilter. It has since been tested properly:
    registering a third-party antivirus caused Windows to stand Defender down
    completely.

    AMRunningMode : Not running     WdFilter : Stopped
    WdNisDrv : Stopped              WdBoot : Stopped     WinDefend : Stopped
    

    Raw growth with the entire Defender stack absent: 608, unchanged. The
    third-party antivirus's own filter driver was loaded during that measurement, so
    it is not antimalware filtering as a category either.

    Also eliminated by direct test: PEAUTH, bam, dam (verified Start=4 and
    absent after reboot), gameflt (genuine fltmc unload), the entire network
    stack, VirtualMachinePlatform/WSL2, HVCI, VBS and Code Integrity enforcement
    (identical and enforced in a clean Safe Mode boot), NVIDIA dGPU and Display
    Container, and all ASUS services, devices and drivers (the clean control machine
    runs the same ones).

    Method notes, since several of these cost hours

    Raw growth is the metric; drop the background subtraction. An idle-sampled
    background rate varied from 2 to 346 objects per 30 seconds and swung the
    derived per-launch figure from 0.127 to 2.227 on a machine whose behaviour
    never changed. Raw Proc growth held at 601-707 per 300 launches across seven
    runs, and exactly 0 in Safe Mode.

    Do not spawn bait with PowerShell Start-Process. It retains a process handle
    even without -PassThru. Two separate findings during this investigation turned
    out to be the harness observing itself, including a "handle held for six minutes"
    result that did not exist.

    Kernel debugging cost a hard reboot every time. Five hard locks, all while
    kd or LiveKd enumerated a large set. Cost scales with the number of matches
    printed, not with whether the command is filtered: the same command took 18
    seconds at 24 matches and locked the machine at 300+.

    Safe Mode driver-set comparison is what actually worked. Diffing the running
    driver list between a clean Safe Mode boot and a leaking normal boot reduced 194
    running drivers to a 62-item candidate list in one reboot, after eleven
    single-component eliminations had moved nothing.

    One request, unrelated to the cause

    Dflt makes this class of leak unattributable from outside, and that is a
    developer-experience defect in its own right.

    Object reference tracing proves there is exactly one stray reference per leaked
    object. It cannot say who holds it, because the surviving reference carries the
    Dflt tag, which is what the kernel records when a caller uses the untagged
    ObReferenceObject API. Every anonymous caller shares that bucket, so
    acquisitions and releases from unrelated components cancel in the accounting.
    Attribution requires a distinct tag, which only the driver author can add at
    compile time.

    That gap is the single largest reason this took sixteen hours, twelve reboots and
    five hard locks to narrow, and why it was ultimately resolved by updating a driver
    rather than by identifying one. If tagged references were required of
    third-party drivers the way signing is, leaks like this would be attributable
    from outside in minutes.

    Happy for this to be closed. Raw data and the full elimination record are
    available if useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions