Describe the bug
wcifs.sys (Windows Container Isolation minifilter) leaks a Filter Manager context reference on
every file open that resolves a WCI reparse point. The contexts are allocated in
wcifs.sys!WcGetSetContextFileObject and are never released, so their reference count never
reaches zero and Filter Manager never frees them. Kernel pool grows monotonically and is
reclaimed only by reboot.
Lifetime counters on the affected machine, sampled 2026-08-20 23:25:23 +02:00 - ~71 h into a
boot that began 2026-08-18 00:46:18 +02:00:
| Tag |
Pool |
Allocations |
Frees |
Outstanding |
Bytes in use |
WCsc (FLT_STREAM_CONTEXT) |
NonPaged |
7,246,092 |
0 |
7,246,092 |
2,782,499,328 |
WCfc (FLT_FILE_CONTEXT) |
NonPaged |
3,661,981 |
0 |
3,661,981 |
761,692,048 |
WCri (reparse data buffer) |
Paged |
3,955,081 |
293,100 |
3,661,981 |
1,991,777,248 |
FMwi (FltMgr work item) |
NonPaged |
7,311,610 |
73,054 |
7,238,556 |
926,535,168 |
FMfc (FltMgr file list ctrl) |
NonPaged |
7,992,513 |
753,202 |
7,239,311 |
1,389,947,712 |
WCfn (file name info) |
Paged |
7,380,786 |
7,380,786 |
0 |
0 |
WCsh (FLT_STREAMHANDLE_CONTEXT) |
NonPaged |
76,758 |
71,228 |
5,530 |
3,981,600 |
FMfn (FltMgr name cache) |
Paged |
2,825,638,035 |
2,796,650,645 |
28,987,390 |
17,740,318,848 |
WCsc and WCfc have exactly zero frees across 7.2 million and 3.7 million allocations.
Re-sampled 2026-08-23 16:30:55 +02:00. Same uninterrupted boot - the machine has not been
rebooted in between, uptime is now 5 d 15 h - and the counters are still moving:
| Tag |
Allocations |
Frees |
Outstanding |
Bytes in use |
Delta allocs vs 08-20 |
WCsc |
7,483,567 |
0 |
7,483,567 |
2,873,689,728 |
+237,475 |
WCfc |
3,837,378 |
0 |
3,837,378 |
798,174,624 |
+175,397 |
WCri |
4,130,692 |
293,314 |
3,837,378 |
2,086,188,912 |
+175,611 |
FMwi |
7,549,171 |
73,136 |
7,476,035 |
956,932,480 |
+237,561 |
FMfc |
8,383,854 |
906,920 |
7,476,934 |
1,435,571,328 |
+391,341 |
WCfn |
7,618,263 |
7,618,263 |
0 |
0 |
+237,477 |
WCsh |
77,788 |
72,254 |
5,534 |
3,984,480 |
+1,030 |
FMfn (paged) |
2,898,021,385 |
2,867,966,719 |
30,054,666 |
18,472,365,424 |
+72,383,350 |
That 65-hour window is the cleanest single statement of the bug. WCsc took 237,475
further allocations and freed none. WCfn - allocated by the same driver on the same
WcPostCreate path, in the same window, on the same machine - took 237,477 and freed
all 237,477. WCsh stayed balanced too (+1,030 allocated, +1,026 freed). The two tags that
never free are not sharing a fate with the tags around them.
The most persuasive element: two internal controls in the same driver, on the same call path, behave correctly
This is not "the workload is holding objects open", and it is not a Filter Manager teardown
failure. Two wcifs allocations prove the surrounding machinery works.
1. WCfn - same function, perfectly balanced. wcifs allocates file-name information on the
same WcPostCreate path and releases it in scope. Lifetime: 7,380,786 allocations, 7,380,786
frees, zero outstanding. The release is directly visible in the traces as
ExFreePoolWithTag <- FLTMGR.SYS!FltReleaseFileNameInformation <- wcifs.sys!WcPostCreate.
The same creates that produce a leaked WCsc produce a correctly released WCfn.
2. WCsh - the driver's registered context cleanup callback runs, millions of times.
wcifs registers one ContextCleanupCallback (wcifs.sys!WcContextCleanup, RVA 0x353E0) for
all four of its context types. It is reached roughly 3,300 times per 200 ms under load, and
WCsh (FLT_STREAMHANDLE_CONTEXT) is released normally: 76,758 allocations / 71,228 frees.
But it is only ever reached down this path:
FLTMGR.SYS!FltClose
wcifs.sys!WcFreeSourceList
wcifs.sys!WcContextCleanup
FLTMGR.SYS!DoReleaseContext
FLTMGR.SYS!FltpDeleteContextList
FLTMGR.SYS!FltpRemoveAllStreamHandleContextsForFileObject <== STREAMHANDLE ONLY
FLTMGR.SYS!FltpCleanupStreamListCtrlForFileObjectCloseWithStreamListCtrl
FLTMGR.SYS!FltpPerformPreCallbacksWorker
FLTMGR.SYS!FltpPassThroughInternal
FLTMGR.SYS!FltpPassThrough
FLTMGR.SYS!FltpDispatch
nt!IopfCallDriver -> nt!IofCallDriver -> nt!IopDeleteFile -> nt!ObpRemoveObjectRoutine
Filter Manager tears down streamhandle contexts at FILE_OBJECT close, which is why WCsh is
freed. File and stream contexts are not released there - they are reference counted and freed
only when the count reaches zero. So the cleanup callback is registered, is reachable, and
demonstrably runs; the file and stream contexts simply never reach refcount zero.
Corroborating: the streams themselves do tear down. In one 90-second window, 140,938 wcifs
stream-tracking allocations (WCse/WCss) were freed while WCsc frees stayed at exactly
zero. WCse/WCss show 12,277,301 lifetime frees. Objects on this path are being destroyed;
the FltMgr contexts attached to them are not.
Root cause analysis
Allocation site
All three leaking wcifs allocations are made back-to-back by one function,
wcifs.sys!WcGetSetContextFileObject, reached from the post-create callback when the create
resolves a WCI reparse point. Symbolized stacks from a WPR "Pool" profile capture
(wcifs.sys 10.0.26100.8972, load base 0xfffff8005fab0000):
(1) WCsc - FLT_STREAM_CONTEXT, 264 B registered / 384 B nonpaged:
nt!ExAllocatePool2 -> nt!ExAllocatePoolWithTag -> nt!ExAllocatePoolEx
nt!ExAllocateFromNPagedLookasideList
FLTMGR.SYS!FltAllocateContext <- tag WCsc comes from the wcifs registration
wcifs.sys!WcGetSetStreamContext <== ALLOCATION SITE (ret 0xfffff8005fadb96d)
wcifs.sys!WcGetSetContextFileObject (ret 0xfffff8005fadaf65)
wcifs.sys!WcProcessWciReparsePointOpen
wcifs.sys!WcPostCreate
FLTMGR.SYS!FltpPerformPostCallbacksWorker
FLTMGR.SYS!FltpProcessIoCompletion
FLTMGR.SYS!FltpPassThroughCompletionWorker
FLTMGR.SYS!FltpLegacyProcessingAfterPreCallbacksCompleted
FLTMGR.SYS!FltpCreate
nt!IopfCallDriver -> nt!IofCallDriver -> nt!IopParseDevice
nt!ObpLookupObjectName -> nt!ObOpenObjectByNameEx -> nt!IopCreateFile
nt!NtOpenFile -> nt!KiSystemServiceCopyEnd
(2) WCfc - FLT_FILE_CONTEXT, 96 B registered / 208 B nonpaged: identical stack except
frames 12-13:
FLTMGR.SYS!FltAllocateContext
wcifs.sys!WcGetSetFileContext <== ALLOCATION SITE (ret 0xfffff8005fadb7a4)
wcifs.sys!WcGetSetContextFileObject (ret 0xfffff8005fadaef4 - a DIFFERENT return
address than in (1): one function allocates
both contexts, back to back)
wcifs.sys!WcProcessWciReparsePointOpen
wcifs.sys!WcPostCreate
(3) WCri - reparse data buffer, paged, allocated directly:
nt!ExAllocatePool2
wcifs.sys!WcGetReparseData <== ALLOCATION SITE (ret 0xfffff8005fadb589)
wcifs.sys!WcGetSetContextFileObject (ret 0xfffff8005fadb004)
wcifs.sys!WcProcessWciReparsePointOpen
wcifs.sys!WcPostCreate
WcGetSetContextFileObject additionally causes one FLTMGR.SYS!FltAllocateGenericWorkItem
(FMwi) and one FLTMGR.SYS!FltpAllocateFileListCtrl (FMfc) per file, both of which are then
also retained.
Static structure
wcifs.sys registers four context types. The FLT_CONTEXT_REGISTRATION array is at file offset
0xb000, 56-byte stride, FLT_CONTEXT_END (0xffff) terminator at 0xb0f0:
| Type |
Value |
Size |
Tag |
Behaviour |
| FLT_INSTANCE_CONTEXT |
0x0002 |
FLT_VARIABLE_SIZED_CONTEXTS |
WCic |
n/a |
| FLT_FILE_CONTEXT |
0x0004 |
0x060 (96) |
WCfc |
leaks |
| FLT_STREAM_CONTEXT |
0x0008 |
0x108 (264) |
WCsc |
leaks |
| FLT_STREAMHANDLE_CONTEXT |
0x0010 |
0x258 (600) |
WCsh |
frees correctly |
All four share the single ContextCleanupCallback at RVA 0x353E0. The literals WCsc and WCfc
each occur exactly once in the binary (only in this table), so these tags have no other
allocation site in wcifs.
Why we believe it is a leaked reference
Per the documented contract for
FltAllocateContext:
"Contexts are reference-counted, and on a successful return from FltAllocateContext, the
context pointed to by ReturnedContext has been initialized to have a reference count of 1.
A context is freed automatically when its reference count reaches zero."
"ReturnedContext ... The caller is responsible for calling FltReleaseContext to release
this context when it is no longer needed."
and FltReleaseContext:
"Every successful call to FltAllocateContext, FltGetXxxContext, or
FltReferenceContext must eventually be matched by a call to FltReleaseContext. Note
that the OldContext pointer returned by FltSetXxxContext and the Context parameter that
is used to call FltDeleteContext must also be released by calling FltReleaseContext
when they are no longer needed."
The allocation reference taken by FltAllocateContext on this path is never dropped, so the
count never reaches zero, WcContextCleanup never runs for these two context types, and the pool
is never returned.
What we can and cannot state precisely. We have not disassembled
WcGetSetContextFileObject, so we cannot assert that a FltReleaseContext call is literally
absent from the source. What the evidence establishes is that a reference taken on this path is
never released. Observationally equivalent variants worth ruling out on your side:
- the release exists but sits on an error/status branch that the success path never takes;
- an extra
FltReferenceContext is taken and matched only in a path this workload never reaches;
- the OldContext returned by
FltSetStreamContext / FltSetFileContext is dropped on the
floor on the re-open case;
- the contexts are intentionally held for the instance lifetime and released only at
detach/unload - which on a running system is never.
The fix direction is the same for all of these, but the exact one matters for the patch.
To Reproduce
There is one gate, plus two properties that make naive measurement misleading. The properties
matter as much as the gate: a walk that reads zero is more often a measurement artifact than a
genuine negative, and we produced several such artifacts ourselves before catching them.
The gate: the layer must be currently attached to a wcifs instance
The store on this machine holds 44 layers. Only the current/live one leaks at all.
| Layer |
Entries |
ΔWCsc |
ab08af0c... (live) |
327,565 |
+231,887 |
aad58cf4... (superseded) |
81,847 |
0 |
5c15b7ff... (superseded) |
95,727 |
0 |
06009466... (superseded) |
100,415 |
0 |
This is not a difference in store integrity. We checked that by inspection rather than
assuming it:
- all 44 layers are registered identically -
State=3, Flags=24, LayeringMode=Wcifs, same
descriptor targeting %SYSTEMDRIVE%;
- the backing store is the C: volume itself, so it cannot have been unmounted;
- superseded layers' reparse points are intact and readable -
TAG=0x80000018, DLEN=48, valid
target GUID;
- the chain target layer
cc2bc905 exists on disk;
- the failure is selective, not structural: on a superseded layer, files fail with
ERROR_CANT_ACCESS_FILE (1920) on a resolving open, but directories open normally.
The one property that distinguishes ab08af0c is OSVersion = 10.0.26200.9168, the running
build, and its StorageId matches a live ContainerStorages entry (849 MB sandbox.vhdx,
written at this boot). Filter-level evidence points the same way: superseded layers'
Files\Windows and its siblings carry the ReparsePoint, Offline attributes, whereas the live
layer's carry neither, and FSCTL_GET_REPARSE_POINT against them returns
ERROR_NOT_A_REPARSE_POINT - i.e. on the live layer wcifs is intercepting and servicing the
path rather than NTFS exposing a reparse point to the caller.
So the gate is whether the layer is currently attached to a wcifs instance, not whether its
backing store is intact. Every allocation occurs on the success path of
WcProcessWciReparsePointOpen. The ERR1920 path allocates nothing, which means the failure
path already disposes of its references correctly - a working reference-handling path inside the
same function to diff the leaking one against.
Scope is confined to the layer store. Other reparse-heavy or projected locations on this
machine were checked and are clean: WindowsApps (4,344 entries), SystemApps (4,031),
WinSxS (27,672), AppRepository (1,819), ModifiableWindowsApps and E:\WindowsApps all have
zero WCI reparse points and produced zero leak across 40 bounded opens.
Every walker leaks - there is no safe traversal API
Measured one walker per virgin subtree of the live layer, no repetition, no rotation:
| Walker |
Subtree |
Entries |
ΔWCsc |
Get-ChildItem -Recurse |
\Windows\INF |
839 |
60 |
cmd dir /a /s /b |
\Windows\Help |
22 |
13 |
Get-ChildItem -Recurse |
\Windows\Logs |
3 |
3 |
cmd dir /a /s /b |
\Windows\Provisioning |
39 |
8 |
MSYS find |
\Windows\Migration |
3 |
1 |
MSYS find |
\Windows\Globalization |
31 |
7 |
cmd, PowerShell and MSYS all leak. A second pass over all six subtrees produced 0, 0, 0, 0,
0, 0, and the same walkers against a superseded layer produced 0.
That second-pass behaviour is the trap. An earlier round of our own testing concluded that find
and Get-ChildItem -Recurse did not reach the leaking path at all - a conclusion drawn from runs
over already-walked trees and over a superseded layer. Both readings were artifacts and the
conclusion was wrong. We state this explicitly because anyone reproducing this will hit the
same trap, and because a "this walker is safe" result should be treated as unproven until it is
taken on a virgin subtree of the live layer.
The unit: one context triple per layer-chain hop, charged on first touch
The chain on this machine is ab08af0c -> cc2bc905 -> %SYSTEMDRIVE%. What determines the leak is
how many layer-chain hops the resolution traverses - not path depth. Measured under verified
zero ambient allocation with sub-second windows, depth held constant at 3:
| File |
Present in parent layer cc2bc905? |
ΔWCsc |
AccountNotifications.admx |
no |
1 |
ActiveXInstallService.admx |
yes |
2 |
AddRemovePrograms.admx |
yes |
2 |
tswcx.xsd |
no |
1 |
Negative controls under the same conditions:
| Operation |
ΔWCsc |
| Re-opening a file already opened |
0 |
| Opening a directory |
0 |
Opening with FILE_READ_ATTRIBUTES only |
0 |
Opening with FILE_FLAG_OPEN_REPARSE_POINT |
0 |
One leaked context triple per layer-chain hop the resolution traverses. This reconciles with
the FSCTL invariant rather than contradicting it: one file open can drive several FSCTLs, one per
hop, and the 1:1 relation holds per FSCTL.
First-touch suppression is real but does not persist - and the totals prove it
An immediate re-walk of the same tree leaks nothing, so some per-stream state suppresses the
second resolution. We have not established how long that suppression lasts, and it is
demonstrably not for the life of the boot. The arithmetic forces this:
- the live layer holds 327,565 entries, so one complete first-touch pass can cost at most
~655,130 contexts even at two hops per file;
- this machine accumulated 7,246,092 outstanding
WCsc on a single boot - between
11x and 22x what one exhaustive first-touch pass could produce.
So the same files must be re-leaking within one boot. The most likely reading, offered as a
hypothesis rather than a result, is that the suppressing state is the per-stream context itself
and is lost when the stream is torn down under cache pressure - consistent with WCse/WCss
showing 12,277,301 lifetime frees while WCsc shows none. If that is right, "first touch" means
"first touch per resident stream", and any long-running or memory-pressured workload re-pays the
cost indefinitely. Confirming or refuting it is squarely on your side of the fence.
Commands
Leaking case - a virgin subtree of the live layer:
cmd /c dir /a /s /b "C:\ProgramData\Microsoft\Windows\Containers\Layers\<live-layer-guid>\Files\Windows\INF" >NUL
Whole live layer:
cmd /c dir /a /s /b "C:\ProgramData\Microsoft\Windows\Containers\Layers\<live-layer-guid>\Files" >NUL
Get-ChildItem -Recurse and MSYS find over the same virgin subtrees leak equivalently - pick
whichever is convenient. Sample the pool tag table before and after with poolmon -p -t (WDK),
or NtQuerySystemInformation(SystemPoolTagInformation). Every measurement in this report was
taken from an ordinary non-elevated user session; the reproducer needs no privilege beyond read
access to the layer store.
Measurement discipline that turned out to matter: use a virgin subtree, verify ambient
allocation is zero first, and keep the window sub-second. A 20-second settle window on this
machine contributed roughly 22 counts of ambient noise on its own, enough to swamp the
single-file hop measurements above.
Measured deltas across one bracketed reproducer run
1,999 files, window 23:24:11 -> 23:25:23 (+02:00), same machine:
| Tag |
Allocs |
Frees |
Net |
Bytes |
Bytes/open |
WCsc |
+1,999 |
0 |
+1,999 |
+767,616 |
384.0 |
WCfc |
+1,999 |
0 |
+1,999 |
+415,792 |
208.0 |
WCri |
+1,999 |
0 |
+1,999 |
+740,928 |
370.6 |
FMwi |
+1,999 |
0 |
+1,999 |
+255,872 |
128.0 |
FMfc |
+1,884 |
0 |
+1,884 |
+361,728 |
180.9 |
WCfn |
+1,999 |
+1,999 |
0 |
0 |
0 |
WCse/WCss |
+6,589 |
+4,590 |
+1,999 |
+127,936 |
64.0 |
FMfn |
+68,551 |
+63,425 |
+5,126 |
+521,728 |
261.0 |
Every one of the 1,999 files in that run resolved successfully, which is why the counts line up
1:1 with the file count. The general form is the FSCTL invariant above: one leaked stream
context, one file context, one reparse buffer and one FltMgr work item per successfully-resolving
reparse open, plus an FltMgr file-list-ctrl at 1,884/1,999 = 0.94 (it is allocated per file
list, not per open) - and exactly one correctly released WCfn alongside each.
Do not read this as one leak per file enumerated. That ratio is not constant (1.00 here, 0.71
on a full live-layer pass, 0.27 on the traced subset) because it depends on how many entries were
already touched this boot and on how many chain hops each resolution traverses. The
per-hop/per-FSCTL ratio is the constant.
Permanently leaked pool across the five never-freed tags: ~1,272 bytes per successfully-
resolving open (~1.3 KB); ~1.5 KB including the correlated FltMgr name-cache growth. Each also
strands one kernel handle in the System process (PID 4).
Note on the FMfn row: that window was measured on a live machine, so the name-cache delta is
not isolated to the reproducer. It is reported as measured and as correlated, not as attributed.
Expected behavior
wcifs releases its allocation reference after FltSetStreamContext / FltSetFileContext
succeed, so the contexts are owned solely by the stream/file and are freed when the stream is
torn down - exactly as WCsh already is. Kernel pool attributable to WCsc, WCfc, WCri,
FMwi and FMfc should be bounded by the number of currently open WCI-projected files, not by
the number of opens performed since boot.
Configuration
- Edition: Windows 11 Pro, version 25H2, build 10.0.26200.9168 (LCU KB5121003,
2026-08-11; SSU KB5123304). Client, not Server.
- Base image / container engine: not required to reproduce. Present on the machine: Docker
Desktop, WSL2, vmcompute / HNS / HvHost. The reproducer above uses only cmd.exe and the layer
store on disk.
- Affected binary:
C:\Windows\System32\drivers\wcifs.sys, file version 10.0.26100.8972,
size 259,544 bytes,
SHA256 3c232e2012ed15caf633761d6d79ff2305c752b5e4a993bed3ffefc0866ba513.
This hash is byte-identical to the wcifs.sys Microsoft ships in KB5121003. The binary is
authentic, current and unmodified - this is not servicing corruption or a third-party
replacement.
fltmgr.sys 10.0.26100.1, bindflt.sys 10.0.26100.8972, UnionFS.sys 10.0.26100.9168,
cimfs.sys 10.0.26100.8972, ntoskrnl.exe 10.0.26100.9168.
- No newer
wcifs.sys exists for this branch. The only later build published is
10.0.28000.2336, which is 26H1. Windows Update offers no fix on 25H2.
- Every loaded minifilter is Microsoft in-box (bindflt, FsDepends, UCPD, WdFilter,
storqosflt, wcifs, gameflt, CldFlt, bfs, FileCrypt, luafv, UnionFS, npsvctrig, Wof, FileInfo).
No third-party minifilter participates. UnionFS and CldFlt have zero instances.
wcifs instances (from fltmc instances): C: at altitude 189900 ("wcifs Instance"),
C: at 189899 ("wcifs Outer Instance"), E: at 189900. D: is an NTFS volume with no wcifs
instance at all, and shows none of this growth - consistent with the leak being reachable
only where wcifs is attached.
Impact
On this machine, at the point the peak figures below were taken - ~3 days into the boot that began
2026-08-18 00:46:18 +02:00, with routine container-isolation file activity:
- 7,026,883 handles in the System process (PID 4) against a normal steady state of ~5,000.
- ~48 GB of kernel pool (34 GB paged + 14 GB nonpaged) on a 128 GB machine.
- Commit charge reached 99.19% with Available MBytes = 0.
- Windows raised "Virtual Memory Minimum Too Low".
- Process creation began failing with STATUS_COMMITMENT_LIMIT.
- Collateral: the Filter Manager name cache (
FMfn) reached 28,987,390 outstanding entries /
16.5 GiB, the single largest consumer on the system, apparently pinned by the same object
graph.
- Reclaimed only by reboot. Killing the user-mode processes stops further growth but returns
nothing.
Current state of that same boot, re-read 2026-08-23 16:30:55 +02:00 (5 d 15 h uptime, still not
rebooted): 7,486,978 handles in PID 4; 50.9 GiB of kernel pool (36.2 GiB paged +
14.6 GiB nonpaged); commit charge 117.9 GiB against a 147.0 GiB limit (80.2%), with 38.6 GiB
physical free. Commit pressure eased only because the user-mode processes driving the sweeps were
killed - none of the leaked pool came back, and both pool and handle counts are still rising.
Growth is linear in the number of successfully-resolving reparse opens against the attached
layer. For scale: one full pass over the live layer costs 231,887 contexts, so the 7,246,092
outstanding WCsc here is equivalent to roughly 31 exhaustive passes - which, given the
first-touch suppression described above, means the same files were re-leaked repeatedly within a
single boot rather than traversed 31 times.
Reproduction conditions: how this machine actually reached that state
This was not organic container workload, and saying so is material to reproducing it. We audited
the complete shell history that ran on the machine - 16,110 agent transcripts, 14,010 shell
invocations - and attributed the leak by System-process (PID 4) handle-count deltas against a
total of 7,483,315 handles:
| Source |
Leaked handles |
Share |
An 84-minute cmd /c dir C:\ /s /b with four wildcard patterns, 2026-08-18, during an unrelated file hunt |
4,190,252 |
56.0% |
A cmd /c dir /s directory-sizing sweep over every top-level directory of C:\, 2026-08-20 |
~2,264,328 |
30.3% |
| Deliberate provocations by this investigation (bisection matrix, reproducer, controls) |
387,564 |
5.2% |
| Unexplained |
641,171 |
8.6% |
91.4% of the leak traces to two sessions of an LLM coding agent (Claude Code) issuing recursive
sweeps rooted at a drive root, neither of which mentions containers anywhere in the command.
35.4% of it - rows two and three, 2,651,892 handles - was inflicted by the diagnostic work
investigating the leak itself. The sizing sweep in row two was an ordinary dir /s that was not
trying to provoke anything, and it cost 2.26 M unreclaimable handles in a single pass.
We state this plainly rather than burying it, because it is the reproduction condition: any tool
that recursively walks a drive root on an affected machine hits this - backup agents, search
indexers, AV full scans, disk-usage utilities, build systems, coding agents. Nothing about the
call site is exotic. The reason it took two agent sessions to become visible here is volume, not
privilege or configuration.
Additional context
What would confirm the diagnosis on your side, cheaply
- The contexts should block unload. A minifilter cannot unload while stream contexts are
outstanding. If this analysis is right, fltmc unload wcifs on an affected machine should
hang or fail. We have not run it - on a machine holding 7.2 M leaked contexts it is likely to
wedge - so it is offered as a lab test, not as evidence.
- Code review of the success path in
WcGetSetContextFileObject, between
WcGetSetFileContext / WcGetSetStreamContext and return, looking for the missing
FltReleaseContext on both the newly allocated context and any OldContext handed back by
FltSetStreamContext / FltSetFileContext.
WCri's partial release is a hint. WCri is not never-freed globally - it shows 293,100
lifetime frees (~7.4%) - but zero on the WCI reparse-open path. Whatever branch frees
WCri elsewhere is likely adjacent to the branch that should be releasing the contexts.
Anticipated question: why is WCsc roughly 2x WCfc over the machine lifetime?
Lifetime: WCsc 7,246,092 vs WCfc 3,661,981 - very close to 2:1. The layer-chain result
above accounts for this cleanly, and we offer it as the likely reading rather than a proven one:
FLT_STREAM_CONTEXT is per stream, so a resolution that traverses two chain hops takes two of
them, while FLT_FILE_CONTEXT is per file and is taken once. A workload averaging ~2 hops per file
therefore produces exactly the observed 2:1. Two independent checks are consistent with it:
WCri outstanding (3,661,981) equals WCfc outstanding exactly, which is what you would
expect if both are per-file rather than per-hop; and the bounded ETW-traced subset, in which the
sampled files were single-hop, showed ΔWCsc = ΔWCfc = ΔWCri = 169 - a 1:1:1 ratio, as predicted.
WCse/WCss, FMwi and FMfc outstanding counts all agree with WCsc outstanding to within
0.1%, so the leaked objects track one coherent graph.
Capturing a kernel dump, if you want one
Nonpaged pool alone is ~14 GB on the affected machine, so dump sizing matters.
Preferred - live dump, no bugcheck, no reboot.
LiveKD (Sysinternals), using
native live-dump support:
livekd64.exe -ml -o D:\wcifs-live.dmp
-ml asks the OS for a consistent kernel snapshot without crashing the machine (Windows 8.1 and
later). Write to a volume with at least 64 GB free, ideally not C:.
Alternative - configured crash dump. SystemPropertiesAdvanced -> Startup and Recovery ->
Write debugging information -> Kernel memory dump. The page file must be large enough to hold
kernel memory; with 34 GB paged + 14 GB nonpaged, set a fixed page file of at least 64 GB, or
use HKLM\SYSTEM\CurrentControlSet\Control\CrashControl\DedicatedDumpFile (REG_SZ, e.g.
D:\dedicateddump.sys) with DumpFileSize to place it on another volume. A kernel dump captures
resident kernel memory only, so paged-out paged pool will not be present; a complete memory dump
would be ~128 GB here.
Analysis commands we would run on it:
!poolused /t20 2 top paged-pool consumers by tag
!poolused /t20 4 top nonpaged-pool consumers by tag
!poolused 2 WCsc the leaking stream context tag
!poolfind WCsc locate live allocations (slow across a 48 GB pool)
!fltkd.filters enumerate registered minifilters
!fltkd.filter <wcifs FLT_FILTER> wcifs registration incl. context registration table
!fltkd.volumes
!fltkd.instances
!fltkd.contextlist <stream ctx list>
!process 4 0 System handle count
!handle 0 3 4 File stranded File handles in System
!vm 4
Pool tagging is on by default on this build; if it needs forcing, gflags /r +ptg (reboot
required).
Related public report - same failure mode, never resolved
moby/moby#39476 (opened 2019-07-09): unbounded paged
pool growth on Windows containers, poolmon implicating ntfs.sys, bindflt.sys and
fltmgr.sys, on Windows Server 2019 / Windows 10 1903. No Microsoft response appears in the
thread; it was closed without a resolution, and the behaviour is still present seven years and
several Windows releases later. That report lacked call stacks and a tag-level control, which is
why both are supplied here.
Data available on request
Full symbolized stacks, the context registration decode, the raw before/after pool tag tables
(all ~1,500 tags), filter stack enumeration, per-volume callback attribution and the binary
hashes are captured and can be attached to this issue or shared privately.
Describe the bug
wcifs.sys(Windows Container Isolation minifilter) leaks a Filter Manager context reference onevery file open that resolves a WCI reparse point. The contexts are allocated in
wcifs.sys!WcGetSetContextFileObjectand are never released, so their reference count neverreaches zero and Filter Manager never frees them. Kernel pool grows monotonically and is
reclaimed only by reboot.
Lifetime counters on the affected machine, sampled 2026-08-20 23:25:23 +02:00 - ~71 h into a
boot that began 2026-08-18 00:46:18 +02:00:
WCsc(FLT_STREAM_CONTEXT)WCfc(FLT_FILE_CONTEXT)WCri(reparse data buffer)FMwi(FltMgr work item)FMfc(FltMgr file list ctrl)WCfn(file name info)WCsh(FLT_STREAMHANDLE_CONTEXT)FMfn(FltMgr name cache)WCscandWCfchave exactly zero frees across 7.2 million and 3.7 million allocations.Re-sampled 2026-08-23 16:30:55 +02:00. Same uninterrupted boot - the machine has not been
rebooted in between, uptime is now 5 d 15 h - and the counters are still moving:
WCscWCfcWCriFMwiFMfcWCfnWCshFMfn(paged)That 65-hour window is the cleanest single statement of the bug.
WCsctook 237,475further allocations and freed none.
WCfn- allocated by the same driver on the sameWcPostCreatepath, in the same window, on the same machine - took 237,477 and freedall 237,477.
WCshstayed balanced too (+1,030 allocated, +1,026 freed). The two tags thatnever free are not sharing a fate with the tags around them.
The most persuasive element: two internal controls in the same driver, on the same call path, behave correctly
This is not "the workload is holding objects open", and it is not a Filter Manager teardown
failure. Two
wcifsallocations prove the surrounding machinery works.1.
WCfn- same function, perfectly balanced.wcifsallocates file-name information on thesame
WcPostCreatepath and releases it in scope. Lifetime: 7,380,786 allocations, 7,380,786frees, zero outstanding. The release is directly visible in the traces as
ExFreePoolWithTag <- FLTMGR.SYS!FltReleaseFileNameInformation <- wcifs.sys!WcPostCreate.The same creates that produce a leaked
WCscproduce a correctly releasedWCfn.2.
WCsh- the driver's registered context cleanup callback runs, millions of times.wcifsregisters oneContextCleanupCallback(wcifs.sys!WcContextCleanup, RVA0x353E0) forall four of its context types. It is reached roughly 3,300 times per 200 ms under load, and
WCsh(FLT_STREAMHANDLE_CONTEXT) is released normally: 76,758 allocations / 71,228 frees.But it is only ever reached down this path:
Filter Manager tears down streamhandle contexts at FILE_OBJECT close, which is why
WCshisfreed. File and stream contexts are not released there - they are reference counted and freed
only when the count reaches zero. So the cleanup callback is registered, is reachable, and
demonstrably runs; the file and stream contexts simply never reach refcount zero.
Corroborating: the streams themselves do tear down. In one 90-second window, 140,938
wcifsstream-tracking allocations (
WCse/WCss) were freed whileWCscfrees stayed at exactlyzero.
WCse/WCssshow 12,277,301 lifetime frees. Objects on this path are being destroyed;the FltMgr contexts attached to them are not.
Root cause analysis
Allocation site
All three leaking
wcifsallocations are made back-to-back by one function,wcifs.sys!WcGetSetContextFileObject, reached from the post-create callback when the createresolves a WCI reparse point. Symbolized stacks from a WPR "Pool" profile capture
(
wcifs.sys10.0.26100.8972, load base0xfffff8005fab0000):(1)
WCsc- FLT_STREAM_CONTEXT, 264 B registered / 384 B nonpaged:(2)
WCfc- FLT_FILE_CONTEXT, 96 B registered / 208 B nonpaged: identical stack exceptframes 12-13:
(3)
WCri- reparse data buffer, paged, allocated directly:WcGetSetContextFileObjectadditionally causes oneFLTMGR.SYS!FltAllocateGenericWorkItem(
FMwi) and oneFLTMGR.SYS!FltpAllocateFileListCtrl(FMfc) per file, both of which are thenalso retained.
Static structure
wcifs.sysregisters four context types. The FLT_CONTEXT_REGISTRATION array is at file offset0xb000, 56-byte stride, FLT_CONTEXT_END (0xffff) terminator at0xb0f0:0x0002WCic0x00040x060(96)WCfc0x00080x108(264)WCsc0x00100x258(600)WCshAll four share the single ContextCleanupCallback at RVA
0x353E0. The literalsWCscandWCfceach occur exactly once in the binary (only in this table), so these tags have no other
allocation site in
wcifs.Why we believe it is a leaked reference
Per the documented contract for
FltAllocateContext:
and FltReleaseContext:
The allocation reference taken by
FltAllocateContexton this path is never dropped, so thecount never reaches zero,
WcContextCleanupnever runs for these two context types, and the poolis never returned.
What we can and cannot state precisely. We have not disassembled
WcGetSetContextFileObject, so we cannot assert that aFltReleaseContextcall is literallyabsent from the source. What the evidence establishes is that a reference taken on this path is
never released. Observationally equivalent variants worth ruling out on your side:
FltReferenceContextis taken and matched only in a path this workload never reaches;FltSetStreamContext/FltSetFileContextis dropped on thefloor on the re-open case;
detach/unload - which on a running system is never.
The fix direction is the same for all of these, but the exact one matters for the patch.
To Reproduce
There is one gate, plus two properties that make naive measurement misleading. The properties
matter as much as the gate: a walk that reads zero is more often a measurement artifact than a
genuine negative, and we produced several such artifacts ourselves before catching them.
The gate: the layer must be currently attached to a wcifs instance
The store on this machine holds 44 layers. Only the current/live one leaks at all.
ab08af0c...(live)aad58cf4...(superseded)5c15b7ff...(superseded)06009466...(superseded)This is not a difference in store integrity. We checked that by inspection rather than
assuming it:
State=3,Flags=24,LayeringMode=Wcifs, samedescriptor targeting
%SYSTEMDRIVE%;TAG=0x80000018,DLEN=48, validtarget GUID;
cc2bc905exists on disk;ERROR_CANT_ACCESS_FILE(1920) on a resolving open, but directories open normally.The one property that distinguishes
ab08af0cisOSVersion = 10.0.26200.9168, the runningbuild, and its
StorageIdmatches a liveContainerStoragesentry (849 MBsandbox.vhdx,written at this boot). Filter-level evidence points the same way: superseded layers'
Files\Windowsand its siblings carry theReparsePoint, Offlineattributes, whereas the livelayer's carry neither, and
FSCTL_GET_REPARSE_POINTagainst them returnsERROR_NOT_A_REPARSE_POINT- i.e. on the live layerwcifsis intercepting and servicing thepath rather than NTFS exposing a reparse point to the caller.
So the gate is whether the layer is currently attached to a wcifs instance, not whether its
backing store is intact. Every allocation occurs on the success path of
WcProcessWciReparsePointOpen. TheERR1920path allocates nothing, which means the failurepath already disposes of its references correctly - a working reference-handling path inside the
same function to diff the leaking one against.
Scope is confined to the layer store. Other reparse-heavy or projected locations on this
machine were checked and are clean:
WindowsApps(4,344 entries),SystemApps(4,031),WinSxS(27,672),AppRepository(1,819),ModifiableWindowsAppsandE:\WindowsAppsall havezero WCI reparse points and produced zero leak across 40 bounded opens.
Every walker leaks - there is no safe traversal API
Measured one walker per virgin subtree of the live layer, no repetition, no rotation:
Get-ChildItem -Recurse\Windows\INFcmd dir /a /s /b\Windows\HelpGet-ChildItem -Recurse\Windows\Logscmd dir /a /s /b\Windows\Provisioningfind\Windows\Migrationfind\Windows\Globalizationcmd, PowerShell and MSYS all leak. A second pass over all six subtrees produced 0, 0, 0, 0,0, 0, and the same walkers against a superseded layer produced 0.
That second-pass behaviour is the trap. An earlier round of our own testing concluded that
findand
Get-ChildItem -Recursedid not reach the leaking path at all - a conclusion drawn from runsover already-walked trees and over a superseded layer. Both readings were artifacts and the
conclusion was wrong. We state this explicitly because anyone reproducing this will hit the
same trap, and because a "this walker is safe" result should be treated as unproven until it is
taken on a virgin subtree of the live layer.
The unit: one context triple per layer-chain hop, charged on first touch
The chain on this machine is
ab08af0c -> cc2bc905 -> %SYSTEMDRIVE%. What determines the leak ishow many layer-chain hops the resolution traverses - not path depth. Measured under verified
zero ambient allocation with sub-second windows, depth held constant at 3:
cc2bc905?AccountNotifications.admxActiveXInstallService.admxAddRemovePrograms.admxtswcx.xsdNegative controls under the same conditions:
FILE_READ_ATTRIBUTESonlyFILE_FLAG_OPEN_REPARSE_POINTOne leaked context triple per layer-chain hop the resolution traverses. This reconciles with
the FSCTL invariant rather than contradicting it: one file open can drive several FSCTLs, one per
hop, and the 1:1 relation holds per FSCTL.
First-touch suppression is real but does not persist - and the totals prove it
An immediate re-walk of the same tree leaks nothing, so some per-stream state suppresses the
second resolution. We have not established how long that suppression lasts, and it is
demonstrably not for the life of the boot. The arithmetic forces this:
~655,130 contexts even at two hops per file;
WCscon a single boot - between11x and 22x what one exhaustive first-touch pass could produce.
So the same files must be re-leaking within one boot. The most likely reading, offered as a
hypothesis rather than a result, is that the suppressing state is the per-stream context itself
and is lost when the stream is torn down under cache pressure - consistent with
WCse/WCssshowing 12,277,301 lifetime frees while
WCscshows none. If that is right, "first touch" means"first touch per resident stream", and any long-running or memory-pressured workload re-pays the
cost indefinitely. Confirming or refuting it is squarely on your side of the fence.
Commands
Leaking case - a virgin subtree of the live layer:
Whole live layer:
Get-ChildItem -Recurseand MSYSfindover the same virgin subtrees leak equivalently - pickwhichever is convenient. Sample the pool tag table before and after with
poolmon -p -t(WDK),or
NtQuerySystemInformation(SystemPoolTagInformation). Every measurement in this report wastaken from an ordinary non-elevated user session; the reproducer needs no privilege beyond read
access to the layer store.
Measurement discipline that turned out to matter: use a virgin subtree, verify ambient
allocation is zero first, and keep the window sub-second. A 20-second settle window on this
machine contributed roughly 22 counts of ambient noise on its own, enough to swamp the
single-file hop measurements above.
Measured deltas across one bracketed reproducer run
1,999 files, window 23:24:11 -> 23:25:23 (+02:00), same machine:
WCscWCfcWCriFMwiFMfcWCfnWCse/WCssFMfnEvery one of the 1,999 files in that run resolved successfully, which is why the counts line up
1:1 with the file count. The general form is the FSCTL invariant above: one leaked stream
context, one file context, one reparse buffer and one FltMgr work item per successfully-resolving
reparse open, plus an FltMgr file-list-ctrl at 1,884/1,999 = 0.94 (it is allocated per file
list, not per open) - and exactly one correctly released
WCfnalongside each.Do not read this as one leak per file enumerated. That ratio is not constant (1.00 here, 0.71
on a full live-layer pass, 0.27 on the traced subset) because it depends on how many entries were
already touched this boot and on how many chain hops each resolution traverses. The
per-hop/per-FSCTL ratio is the constant.
Permanently leaked pool across the five never-freed tags: ~1,272 bytes per successfully-
resolving open (~1.3 KB); ~1.5 KB including the correlated FltMgr name-cache growth. Each also
strands one kernel handle in the System process (PID 4).
Note on the
FMfnrow: that window was measured on a live machine, so the name-cache delta isnot isolated to the reproducer. It is reported as measured and as correlated, not as attributed.
Expected behavior
wcifsreleases its allocation reference afterFltSetStreamContext/FltSetFileContextsucceed, so the contexts are owned solely by the stream/file and are freed when the stream is
torn down - exactly as
WCshalready is. Kernel pool attributable toWCsc,WCfc,WCri,FMwiandFMfcshould be bounded by the number of currently open WCI-projected files, not bythe number of opens performed since boot.
Configuration
2026-08-11; SSU KB5123304). Client, not Server.
Desktop, WSL2, vmcompute / HNS / HvHost. The reproducer above uses only
cmd.exeand the layerstore on disk.
C:\Windows\System32\drivers\wcifs.sys, file version 10.0.26100.8972,size 259,544 bytes,
SHA256
3c232e2012ed15caf633761d6d79ff2305c752b5e4a993bed3ffefc0866ba513.This hash is byte-identical to the
wcifs.sysMicrosoft ships in KB5121003. The binary isauthentic, current and unmodified - this is not servicing corruption or a third-party
replacement.
fltmgr.sys10.0.26100.1,bindflt.sys10.0.26100.8972,UnionFS.sys10.0.26100.9168,cimfs.sys10.0.26100.8972,ntoskrnl.exe10.0.26100.9168.wcifs.sysexists for this branch. The only later build published is10.0.28000.2336, which is 26H1. Windows Update offers no fix on 25H2.
storqosflt, wcifs, gameflt, CldFlt, bfs, FileCrypt, luafv, UnionFS, npsvctrig, Wof, FileInfo).
No third-party minifilter participates. UnionFS and CldFlt have zero instances.
wcifsinstances (fromfltmc instances): C: at altitude 189900 ("wcifs Instance"),C: at 189899 ("wcifs Outer Instance"), E: at 189900. D: is an NTFS volume with no
wcifsinstance at all, and shows none of this growth - consistent with the leak being reachable
only where
wcifsis attached.Impact
On this machine, at the point the peak figures below were taken - ~3 days into the boot that began
2026-08-18 00:46:18 +02:00, with routine container-isolation file activity:
FMfn) reached 28,987,390 outstanding entries /16.5 GiB, the single largest consumer on the system, apparently pinned by the same object
graph.
nothing.
Current state of that same boot, re-read 2026-08-23 16:30:55 +02:00 (5 d 15 h uptime, still not
rebooted): 7,486,978 handles in PID 4; 50.9 GiB of kernel pool (36.2 GiB paged +
14.6 GiB nonpaged); commit charge 117.9 GiB against a 147.0 GiB limit (80.2%), with 38.6 GiB
physical free. Commit pressure eased only because the user-mode processes driving the sweeps were
killed - none of the leaked pool came back, and both pool and handle counts are still rising.
Growth is linear in the number of successfully-resolving reparse opens against the attached
layer. For scale: one full pass over the live layer costs 231,887 contexts, so the 7,246,092
outstanding
WCschere is equivalent to roughly 31 exhaustive passes - which, given thefirst-touch suppression described above, means the same files were re-leaked repeatedly within a
single boot rather than traversed 31 times.
Reproduction conditions: how this machine actually reached that state
This was not organic container workload, and saying so is material to reproducing it. We audited
the complete shell history that ran on the machine - 16,110 agent transcripts, 14,010 shell
invocations - and attributed the leak by System-process (PID 4) handle-count deltas against a
total of 7,483,315 handles:
cmd /c dir C:\ /s /bwith four wildcard patterns, 2026-08-18, during an unrelated file huntcmd /c dir /sdirectory-sizing sweep over every top-level directory ofC:\, 2026-08-2091.4% of the leak traces to two sessions of an LLM coding agent (Claude Code) issuing recursive
sweeps rooted at a drive root, neither of which mentions containers anywhere in the command.
35.4% of it - rows two and three, 2,651,892 handles - was inflicted by the diagnostic work
investigating the leak itself. The sizing sweep in row two was an ordinary
dir /sthat was nottrying to provoke anything, and it cost 2.26 M unreclaimable handles in a single pass.
We state this plainly rather than burying it, because it is the reproduction condition: any tool
that recursively walks a drive root on an affected machine hits this - backup agents, search
indexers, AV full scans, disk-usage utilities, build systems, coding agents. Nothing about the
call site is exotic. The reason it took two agent sessions to become visible here is volume, not
privilege or configuration.
Additional context
What would confirm the diagnosis on your side, cheaply
outstanding. If this analysis is right,
fltmc unload wcifson an affected machine shouldhang or fail. We have not run it - on a machine holding 7.2 M leaked contexts it is likely to
wedge - so it is offered as a lab test, not as evidence.
WcGetSetContextFileObject, betweenWcGetSetFileContext/WcGetSetStreamContextand return, looking for the missingFltReleaseContexton both the newly allocated context and any OldContext handed back byFltSetStreamContext/FltSetFileContext.WCri's partial release is a hint.WCriis not never-freed globally - it shows 293,100lifetime frees (~7.4%) - but zero on the WCI reparse-open path. Whatever branch frees
WCrielsewhere is likely adjacent to the branch that should be releasing the contexts.Anticipated question: why is
WCscroughly 2xWCfcover the machine lifetime?Lifetime:
WCsc7,246,092 vsWCfc3,661,981 - very close to 2:1. The layer-chain resultabove accounts for this cleanly, and we offer it as the likely reading rather than a proven one:
FLT_STREAM_CONTEXT is per stream, so a resolution that traverses two chain hops takes two of
them, while FLT_FILE_CONTEXT is per file and is taken once. A workload averaging ~2 hops per file
therefore produces exactly the observed 2:1. Two independent checks are consistent with it:
WCrioutstanding (3,661,981) equalsWCfcoutstanding exactly, which is what you wouldexpect if both are per-file rather than per-hop; and the bounded ETW-traced subset, in which the
sampled files were single-hop, showed ΔWCsc = ΔWCfc = ΔWCri = 169 - a 1:1:1 ratio, as predicted.
WCse/WCss,FMwiandFMfcoutstanding counts all agree withWCscoutstanding to within0.1%, so the leaked objects track one coherent graph.
Capturing a kernel dump, if you want one
Nonpaged pool alone is ~14 GB on the affected machine, so dump sizing matters.
Preferred - live dump, no bugcheck, no reboot.
LiveKD (Sysinternals), using
native live-dump support:
-mlasks the OS for a consistent kernel snapshot without crashing the machine (Windows 8.1 andlater). Write to a volume with at least 64 GB free, ideally not C:.
Alternative - configured crash dump.
SystemPropertiesAdvanced-> Startup and Recovery ->Write debugging information -> Kernel memory dump. The page file must be large enough to hold
kernel memory; with 34 GB paged + 14 GB nonpaged, set a fixed page file of at least 64 GB, or
use
HKLM\SYSTEM\CurrentControlSet\Control\CrashControl\DedicatedDumpFile(REG_SZ, e.g.D:\dedicateddump.sys) withDumpFileSizeto place it on another volume. A kernel dump capturesresident kernel memory only, so paged-out paged pool will not be present; a complete memory dump
would be ~128 GB here.
Analysis commands we would run on it:
Pool tagging is on by default on this build; if it needs forcing,
gflags /r +ptg(rebootrequired).
Related public report - same failure mode, never resolved
moby/moby#39476 (opened 2019-07-09): unbounded paged
pool growth on Windows containers, poolmon implicating
ntfs.sys,bindflt.sysandfltmgr.sys, on Windows Server 2019 / Windows 10 1903. No Microsoft response appears in thethread; it was closed without a resolution, and the behaviour is still present seven years and
several Windows releases later. That report lacked call stacks and a tag-level control, which is
why both are supplied here.
Data available on request
Full symbolized stacks, the context registration decode, the raw before/after pool tag tables
(all ~1,500 tags), filter stack enumeration, per-volume callback attribution and the binary
hashes are captured and can be attached to this issue or shared privately.