ccffilter.sys — ECP lifetime use-after-free / double-free in CCFPreCreate (CWE-416) fixed
KB5078752
1. Overview
| Field | Value |
|---|---|
| Unpatched binary | ccffilter_unpatched.sys |
| Patched binary | ccffilter_patched.sys |
| Overall similarity | 0.9733 |
| Matched functions | 85 |
| Changed functions | 2 (1 security-relevant, 1 cosmetic) |
| Identical functions | 83 |
| Unmatched (either direction) | 0 |
Verdict: The patch addresses a medium-severity kernel pool lifetime bug in the Cluster Shared Volume (CSV) minifilter's IRP_MJ_CREATE pre-operation callback (CCFPreCreate). After the app-instance Extra Create Parameter (ECP) is inserted into the create IRP's ECP list, the unpatched code leaves its local pointer live; if a later ECP sub-step fails, the function's error-cleanup frees that ECP while it is still linked into the IRP's ECP list. The IRP's ECP list retains that dangling pointer, so the filter manager's later ECP-list teardown re-frees / dereferences freed pool — a double-free / use-after-free of the ECP context. Reaching the bug requires a CSV-backed create carrying the app-instance EA and a subsequent version/CSV ECP sub-step failing (typically an internal allocation failure); that secondary failure is not directly attacker-controlled, and the demonstrable impact is kernel pool corruption (local denial of service). The patched build adds the local-pointer clear on the success path, but delivers it behind a Windows Feature Staging runtime flag (EvaluateCurrentState), so the fix is a staged rollout: with the feature not active the patched binary retains the unpatched behavior.
2. Vulnerability Summary
Finding #1 — ECP Lifetime / Use-After-Free in CCFPreCreate
- Severity: Medium
- Class: Use-after-free / double-free, kernel pool lifetime (CWE-416)
- Affected function:
CCFPreCreate(sub_1c0007760), theIRP_MJ_CREATEpre-operation callback registered viaFltRegisterFilter.
Root Cause
ccffilter.sys is the FltMgr minifilter that backs Windows Cluster Shared Volumes. When a user-mode process opens a file on a CSV path with an EA buffer carrying a "CCFF" app-instance EA, the create path in CCFPreCreate allocates a GUID_ECP_NETWORK_APP_INSTANCE Extra Create Parameter (0x14 bytes, pool tag "CCFF") and passes it to CCFAddEcpToCreate (sub_1c00090a8). That helper:
- Obtains the IRP's ECP list via
FltGetEcpListFromCallbackData. - Allocates a new list (
FltAllocateExtraCreateParameterList+FltSetEcpListIntoCallbackData) if none exists. - Calls
FltInsertExtraCreateParameterto link the freshly-allocated ECP into the IRP's ECP list. - Zeroes its byref out-flag (
var_b8) before returning.
FltInsertExtraCreateParameter transfers ownership of the ECP to the filter manager — the ECP now lives for the lifetime of the IRP. The bug is that on the CCFAddEcpToCreate success path the unpatched CCFPreCreate does not clear its local copy of the ECP pointer (the clear it does have sits inside an if (var_b8 != 0) branch that is never taken, because CCFAddEcpToCreate just zeroed var_b8). Execution then continues into the version/CSV ECP sub-flow (CCFFindAppInstanceVersionEcp / CCFLookupAndAddVersionEcpToCreate). If that sub-step returns an error, control reaches the function's error-cleanup, which runs if (status < 0 && EcpContext) FltFreeExtraCreateParameter(filter, EcpContext) — freeing the app-instance ECP that is still linked in the IRP's ECP list. The freed pool block remains referenced by the list, so any subsequent ECP-list traversal (IRP completion teardown, FltFindExtraCreateParameter, or another minifilter querying the list) dereferences stale pool — kernel pool use-after-free / pool corruption.
The patch clears the local ECP pointer on the CCFAddEcpToCreate success path (so the error-cleanup can no longer free the still-listed ECP), gating the clear behind a Windows Feature Staging flag via the new helper EvaluateCurrentState (sub_1c00017b8). That helper calls EvaluateFeature (sub_1c0001740) on the feature descriptor g_Feature_2963746106_60258290 and returns (cachedstate != 1). The practical effect is that, with the feature enabled, the local ECP pointer is nulled immediately after a successful insert, so the later error path no longer double-frees the ECP that FltMgr already owns.
Attacker-Reachable Entry Point & Data Flow
NtCreateFile / CreateFileW on a CSV-backed path → FltMgr IRP_MJ_CREATE dispatch → CCFPreCreate pre-op callback.
- User mode:
NtCreateFile/CreateFileWagainst a CSV-mounted path (e.g.C:\ClusterStorage\Volume1\<file>), supplying anEaBuffercontaining a CCFF app-instance EA. - FltMgr routes the create IRP to
CCFPreCreate(sub_1c0007760). CCFFilterAppInstanceEAparses the create'sEaBuffer, setsvar_b7, drives the app-instance branch.CCFPreCreateallocates the ECP:FltAllocateExtraCreateParameter(filter, &GUID_ECP_NETWORK_APP_INSTANCE, 0x14, ...) → EcpContext_1.CCFPreCreatecallsCCFAddEcpToCreate→FltInsertExtraCreateParameter— the ECP is now owned by the IRP's ECP list;var_b8is cleared.- On success
CCFPreCreateleavesEcpContext_1non-null and proceeds to the version/CSV ECP sub-flow. - If a version/CSV ECP step fails, the error-cleanup calls
FltFreeExtraCreateParameter(filter, EcpContext_1)on the still-listed ECP → dangling ECP-list entry. - Subsequent ECP-list operation (IRP completion /
FltFindExtraCreateParameter/ list teardown) dereferences freed pool → UAF.
3. Pseudocode Diff
// =============== UNPATCHED CCFPreCreate — ECP allocate/insert + success path ===============
LABEL_60:
strncpy(&PoolTag, "CCFF", 4);
FltAllocateExtraCreateParameter(
filter, &GUID_ECP_NETWORK_APP_INSTANCE, 0x14, 0, nullptr, "CCFF", &EcpContext);
...
*(WORD*)EcpContext = 0x14; // init ECP context length
...
v4 = CCFAddEcpToCreate(CallbackData, EcpContext, &var_b8);
// ^ inside: FltGetEcpListFromCallbackData / (FltAllocateExtraCreateParameterList +
// FltSetEcpListIntoCallbackData) / FltInsertExtraCreateParameter
// *** OWNERSHIP TRANSFERRED TO IRP ECP LIST; var_b8 zeroed ***
if (v4 >= 0) { // STATUS_SUCCESS
if (var_b8) { // never taken (var_b8 cleared by callee)
FltFreeExtraCreateParameter(filter, EcpContext);
EcpContext = nullptr;
}
goto LABEL_72; // *** EcpContext still non-null & still listed ***
}
...
LABEL_87:
if (v4 < 0) { // set by a failed version/CSV ECP step
if (EcpContext != nullptr) {
FltFreeExtraCreateParameter(filter, EcpContext);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// *** BUG: ECP is still linked in the IRP's ECP list ***
EcpContext = nullptr;
}
CallbackData->IoStatus.Status = v4;
}
// =============== PATCHED CCFPreCreate — same success site ===============
v4 = CCFAddEcpToCreate(CallbackData, EcpContext, &var_b8);
if (v4 < 0) { ...; goto LABEL_90; }
if (var_b8) { // still never taken
FltFreeExtraCreateParameter(filter, EcpContext);
EcpContext = (void*)((uintptr_t)EcpContext & -(int64_t)EvaluateCurrentState());
}
if (EvaluateCurrentState()) // NEW: clear on the whole success path
EcpContext = nullptr; // -> error-cleanup no longer frees the listed ECP
goto LABEL_75;
// =============== EvaluateCurrentState (sub_1c00017b8, new patched helper) ===============
// Windows Feature Staging gate; returns (cachedstate != 1)
bool EvaluateCurrentState() {
EvaluateFeature(&g_Feature_2963746106_60258290_FeatureDescriptorDetails);
return g_Feature_2963746106_60258290_cachedstate != 1;
}
Key observation: the FltFreeExtraCreateParameter calls themselves are byte-for-byte identical between the two builds. The only behavioral change is where the local EcpContext pointer is nulled: the unpatched build nulls it only inside the never-taken if (var_b8) branch, so on the normal success path it stays live and the later error-cleanup can free the already-listed ECP. The patch nulls it on the whole success path (feature-gated) so that error-cleanup free can no longer reach the ECP FltMgr already owns.
4. Assembly Analysis
CCFPreCreate — vulnerable region (unpatched, sub_1c0007760)
; ===== Block: CCFAddEcpToCreate call site (ownership transfer) =====
0x1c0007baa: call CCFAddEcpToCreate ; (rcx=CallbackData, rdx=EcpContext, r8=&var_b8)
; => FltInsertExtraCreateParameter inside => IRP ECP list owns ECP; var_b8=0
0x1c0007bb5: test eax, eax
0x1c0007bb7: jns loc_1c0007bfd ; if (CCFAddEcpToCreate >= 0) goto success
; ===== Block: never-taken consumed branch (var_b8 already zeroed) =====
0x1c0007bfd: cmp byte [rsp+0x40], r14b ; var_b8 (== 0)
0x1c0007c02: jz 0x1c0007c61 ; -> version/CSV ECP handling (taken)
...
0x1c0007c3d: mov rdx, qword [rsp+0x48] ; rdx = EcpContext
mov rax, qword [rsp+0x58]
mov rcx, qword [rax+0x8] ; rcx = filter handle
0x1c0007c4b: call cs:__imp_FltFreeExtraCreateParameter
0x1c0007c57: mov qword [rsp+0x48], r14 ; EcpContext = NULL (inside never-taken branch)
jmp 0x1c0007c61
; ===== Block: error-cleanup — THE vulnerable free =====
0x1c0007d43: test edi, edi ; edi = status (set < 0 by a failed version/CSV ECP step)
0x1c0007d45: jns 0x1c0007d79 ; skip if success
0x1c0007d47: mov rdx, qword [rsp+0x48] ; rdx = EcpContext (still non-null; still in IRP ECP list)
0x1c0007d4c: test rdx, rdx
0x1c0007d4f: jz 0x1c0007d6b
mov rax, qword [rsp+0x58]
mov rcx, qword [rax+0x8] ; rcx = filter handle
0x1c0007d5a: call cs:__imp_FltFreeExtraCreateParameter ; *** frees still-listed ECP -> UAF ***
0x1c0007d66: mov qword [rsp+0x48], r14
CCFPreCreate — patched region
0x1c0007c4b: call cs:__imp_FltFreeExtraCreateParameter ; (never-taken branch; unchanged)
0x1c0007c57: call EvaluateCurrentState ; returns (cachedstate != 1)
0x1c0007c5c: neg eax ; mask = (flag==1) ? 0 : -1
0x1c0007c5e: sbb rcx, rcx ; rcx = mask
0x1c0007c61: and rcx, qword [rsp+0x48] ; rcx = mask & EcpContext
0x1c0007c66: mov qword [rsp+0x48], rcx
0x1c0007c6b: call EvaluateCurrentState ; success-path clear (runs regardless of var_b8)
0x1c0007c70: mov rcx, qword [rsp+0x48]
0x1c0007c75: test eax, eax
0x1c0007c77: cmovnz rcx, r14 ; if (flag!=1) EcpContext = nullptr
0x1c0007c7b: mov qword [rsp+0x48], rcx
0x1c0007c80: jmp 0x1c0007c85
; error-cleanup free relocated to 0x1c0007d87 (same guarded shape), now sees EcpContext == NULL after a successful insert
EvaluateCurrentState (sub_1c00017b8, patch-only helper)
0x1c00017b8: sub rsp, 0x28
0x1c00017bc: lea rcx, g_Feature_2963746106_60258290_FeatureDescriptorDetails
0x1c00017c3: call EvaluateFeature ; sub_1c0001740
0x1c00017c8: mov rax, cs:g_Feature_2963746106_60258290_FeatureDescriptorDetails
0x1c00017cf: mov ecx, dword [rax] ; ecx = cachedstate
0x1c00017d1: xor eax, eax
0x1c00017d3: cmp ecx, 0x1
0x1c00017d6: setnz al ; return (cachedstate != 1)
0x1c00017d9: add rsp, 0x28
0x1c00017dd: retn
Annotation summary:
0x1c0007baa—CCFAddEcpToCreatecall. After this returns>= 0, the ECP at[rsp+0x48]is owned by the IRP's ECP list andvar_b8is0.0x1c0007c4b— consumed-branchFltFreeExtraCreateParameter; unchanged by the patch and unreached becausevar_b8 == 0.0x1c0007d5a— the vulnerable instruction:call FltFreeExtraCreateParameteron the still-listed ECP in the error-cleanup, reached when a version/CSV ECP step fails whileEcpContextis still live.- The patch adds the success-path clear at
0x1c0007c6b(feature-gated) so the error-cleanup free seesEcpContext == NULL.
5. Trigger Conditions
- Target must have CSV mounted and
ccffilter.sysloaded and filtering. Confirm withfltmc instances(the minifilter is attached to CSV volumes). - From a standard user process, call
NtCreateFile/CreateFileWagainst a path under a CSV mount, e.g.C:\ClusterStorage\Volume1\<file>. - Supply an
EaBufferthat carries a CCFF app-instance EA, parsed byCCFFilterAppInstanceEA, so thevar_b7flag drives theGUID_ECP_NETWORK_APP_INSTANCEallocation +CCFAddEcpToCreateinsert path (label0x1c0007a96). - The pre-existing app-instance ECP must NOT already be present (i.e.
CCFFindAppInstanceEcpreturnsvar_b8 == 0at entry), so the allocate/insert sequence runs. - The subsequent version/CSV ECP step must fail so that the create status goes negative while
EcpContextis still live — this is what drives execution into the error-cleanupFltFreeExtraCreateParameterat0x1c0007d5aon the still-listed ECP. This failure is a secondary error path (for example an internal ECP allocation/insert failure) and is not directly attacker-controlled. - Observable effect: the ECP context is freed while the IRP's ECP list still references it. Because the filter manager frees the ECPs in that list at create teardown, the block is freed a second time (or walked after free), which can corrupt the kernel pool and bugcheck the machine. With Driver Verifier Special Pool enabled on the
CCFFtag, the invalid free/dereference is caught at the point it occurs.
6. Exploit Primitive & Development Notes
Primitive
Kernel pool double-free / use-after-free. The freed ECP context is a small pool allocation (0x14 bytes, tag CCFF). After the error-cleanup FltFreeExtraCreateParameter at 0x1c0007d5a, the block is freed while it is still linked in the IRP's ECP list. The filter manager frees the ECPs remaining in that list when the create tears down, so the same block is freed a second time (or its stale contents are walked before that), corrupting the kernel pool.
Reachability and Impact
- The primitive is only reached on an error path: a create that carries the app-instance EA succeeds in allocating and inserting the ECP, and then a subsequent version/CSV ECP sub-step returns a failure status. That secondary failure is not directly attacker-controlled — it is driven by internal conditions such as an ECP allocation/insert failure — so triggering is non-deterministic.
- No control over the freed block's replacement contents, timing, or a follow-on function-pointer/write primitive is demonstrable from these binaries. The concrete, reachable impact is kernel pool corruption, i.e. a local denial of service (bugcheck). There is no demonstrated path to privilege escalation.
- The environment is constrained:
ccffilter.sysonly attaches to Cluster Shared Volume volumes, so a CSV-backed host is required.
7. Debugger PoC Playbook
Assumes WinDbg/KD attached to the unpatched ccffilter_unpatched.sys on a system with a Cluster Shared Volume mounted.
Breakpoints
bp ccffilter_unpatched!CCFPreCreate
Why: Entry to the IRP_MJ_CREATE pre-op. Confirm the create targets a CSV path by dumping the file name from the callback data. rcx = FLT_CALLBACK_DATA* (arg1), rdx = FLT_RELATED_OBJECTS* (arg2).
bp 0x1c0007baa
Why: CCFAddEcpToCreate call site — the FltInsertExtraCreateParameter ownership transfer happens inside. After this returns >= 0, the ECP at [rsp+0x48] is owned by the IRP's ECP list and var_b8 is 0.
bp 0x1c0007d5a
Why: The vulnerable FltFreeExtraCreateParameter call in the error-cleanup. rcx = filter handle, rdx = EcpContext (the still-listed ECP). This block is reached only when the status (edi) is negative — i.e. a version/CSV ECP step failed while EcpContext was still live.
What to Inspect at Each Breakpoint
| Breakpoint | Register / Memory | Meaning |
|---|---|---|
CCFPreCreate entry |
rcx |
FLT_CALLBACK_DATA* — follow ->Iopb->TargetFileObjectName->Buffer to confirm CSV path. |
CCFPreCreate entry |
rdx |
FLT_RELATED_OBJECTS* — rdx->[0x8] is the filter handle passed to Flt ECP APIs. |
0x1c0007baa (pre-call) |
rdx |
EcpContext — the freshly allocated 0x14-byte CCFF-tagged ECP about to be inserted. |
0x1c0007baa (post-return) |
rax |
Status from CCFAddEcpToCreate. Must be >= 0 to leave the ECP listed and live. |
0x1c0007d5a (pre-free) |
rdx |
The ECP being freed (still in IRP ECP list). Note this value — it is the dangling pointer. |
0x1c0007d5a (pre-free) |
edi |
Status; must be < 0 (a failed version/CSV ECP step) to reach this block. |
Key Instructions / Offsets
| Offset | Instruction | Significance |
|---|---|---|
0x1c0007baa |
call CCFAddEcpToCreate |
FltInsertExtraCreateParameter ownership transfer inside; var_b8 zeroed. |
0x1c0007bb7 |
jns 0x1c0007bfd |
Branch into success path (v4 >= 0). |
0x1c0007c4b |
call FltFreeExtraCreateParameter |
Consumed-branch free; unchanged and unreached (var_b8 == 0). |
0x1c0007d43 |
test edi, edi / jns |
Error-cleanup gate (status < 0). |
0x1c0007d5a |
call cs:__imp_FltFreeExtraCreateParameter |
FltFreeExtraCreateParameter on the still-listed ECP — THE bug. |
0x1c0007760 |
— | CCFPreCreate entry. |
Trigger Setup (from user mode)
- Confirm CSV mounted and
ccffilter.sysfiltering:text fltmc instances - Build an
EaBuffercontaining a CCFF app-instance EA so thatCCFFilterAppInstanceEAsetsvar_b7and forces theGUID_ECP_NETWORK_APP_INSTANCEallocation inlabel_1c0007a96. Ensure no pre-existing app-instance ECP is present soCCFFindAppInstanceEcpleavesvar_b8 == 0at entry, and arrange for the subsequent version/CSV ECP step to fail so the create status goes negative. - From a standard user, open the file with
NtCreateFilesupplying the CCFF app-instanceEaBuffer, under conditions that force the subsequent version/CSV ECP sub-step to fail (for example an ECP allocation/insert failure), so the create status goes negative whileEcpContextis still live.
Expected Observation
- At
0x1c0007d5a:rdxis the ECP being freed. Step over; the ECP atrdxis now freed but the IRP's ECP list still references it. - Subsequently, when the filter manager tears down the create's ECP list, the same block is freed again / walked after free, producing kernel pool corruption and a bugcheck.
- With Driver Verifier Special Pool enabled on the
CCFFtag, the invalid free/dereference is caught at the point it occurs, with the faulting address at the freed ECP.
Struct / Offset Notes
- ECP context: 0x14 bytes total.
[ECP+0]= length word (= 0x14),[ECP+2]zeroed at init,[ECP+4]= 16-byte app-instance payload. - Pool tag:
"CCFF"(0x46464343). FLT_CALLBACK_DATA (arg1, rcx)→->Iopb→->Parameters.Create.EaBufferis the parsed EA buffer.arg2[1](rdx->[0x8]) = filter handle passed to everyFlt*ECP API.
8. Changed Functions — Full Triage
CCFPreCreate (sub_1c0007760) — security-relevant
- Similarity: 0.9658
- Change type: behavioral / security-relevant
- What changed: On the
CCFAddEcpToCreatesuccess path, the unpatched code clears the local ECP pointer (mov qword [rsp+0x48], r14) only inside theif (var_b8 != 0)branch, which is never taken becauseCCFAddEcpToCreatezeroesvar_b8. So after a successful insert the localEcpContextstays live, and the function's error-cleanup (0x1c0007d5a) frees it while it is still linked in the IRP's ECP list if a version/CSV ECP step fails. The patch adds a success-path clear ofEcpContext(via new helperEvaluateCurrentState/sub_1c00017b8, which callsEvaluateFeature/sub_1c0001740ong_Feature_2963746106_60258290and returnscachedstate != 1), usingneg eax; sbb rcx,rcx; and rcx,[rsp+0x48]at the first site and a secondEvaluateCurrentState-gatedcmovnzat0x1c0007c6b. TheFltFreeExtraCreateParametercalls themselves are identical on both sides; only the local-pointer clear placement changed, so the error-cleanup no longer frees the still-listed ECP. This is the only behavioral change in the patch.
DriverEntry — cosmetic
- Similarity: 0.9922
- Change type: cosmetic (symbol / label stripping only)
- What changed: Symbol names and addresses shifted between builds (
CCFDispatchUnsupported (sub_1c00081f0),CCFDispatchDeviceControl (sub_1c0007f30),CCFProcessNotification (sub_1c0008270),CCFApplyDeviceAcl (sub_1c00096d8),CCFUnload (sub_1c0007590),WPP_SF_d (sub_1c0001034)). Global addresses shifted (data_1c00041a8 → 0x1c00041c8,CCFCacheGuardedMutex → 0x1c0004180). Logic —FltRegisterFilter,IoCreateDevice(\Device\CCFFilter,DeviceType=0x14,Characteristics=0x100),PsSetCreateProcessNotifyRoutine,FltStartFiltering, device-ACL application, unload-on-failure — is equivalent. TheIRP_MJ_DEVICE_CONTROLdispatch (CCFDispatchDeviceControl/sub_1c0007f30) handles IOCTL codes0x140000 / 0x14400c / 0x144018 / 0x144004 / 0x144008with explicitInputBufferLength/OutputBufferLengthchecks (cmp r9d, 0x20 / 0x28 / 0x10 / 0x8). No behavioral change.
9. Unmatched Functions
None in the top-level matched set. The patched build additionally introduces the Feature Staging helpers EvaluateCurrentState (sub_1c00017b8), EvaluateCurrentStateFromRegistry (sub_1c0001518), EvaluateFeature (sub_1c0001740), and rbc_InitializeFeatureStaging (sub_1c00017e4), which back the success-path clear in CCFPreCreate.
10. Confidence & Caveats
Confidence: High.
- The ownership transfer is unambiguous:
CCFAddEcpToCreatecallsFltInsertExtraCreateParameter(the IRP's ECP list now owns the ECP) and zeroes its out-flagvar_b8. - The unpatched success path leaves the local
EcpContextlive (the clear is inside the never-takenif (var_b8)branch), and the error-cleanup at0x1c0007d5a(call FltFreeExtraCreateParameter) frees that still-listed ECP when a version/CSV ECP step fails. Both are directly visible in the disassembly and the decompilation. - The patch's success-path clear via
EvaluateCurrentState(sub_1c00017b8) is consistent with a deliberate lifetime/ownership fix, gated by a Windows Feature Staging flag rather than a refactor.
Assumptions to verify manually:
- EA buffer format. The exact byte layout of the "CCFF app-instance EA" that drives
CCFFilterAppInstanceEAinto thevar_b7branch was inferred from context, not reconstructed field-by-field. A PoC author should reverseCCFFilterAppInstanceEAto build the EA precisely. - Failure of the version/CSV ECP step. Reaching the error-cleanup free requires the subsequent
CCFFindAppInstanceVersionEcp/CCFLookupAndAddVersionEcpToCreatesub-step to return an error whileEcpContextis still live. Confirm the input conditions that force that failure. - CSV volume presence. The minifilter only attaches to CSV volumes — a PoC requires a host with CSV configured, or an environment where the filter can be coerced into attaching (lab cluster setup).
- Manifestation. The concrete fault is the second free / stale-pointer walk performed by the filter manager's ECP-list teardown at create completion. Confirming the exact teardown site and timing (and whether a given build catches it as a double-free or a use-after-free) requires dynamic observation; no follow-on read/write or code-execution primitive is claimed.
- Feature-staging state. The success-path clear is applied when
EvaluateCurrentStatereturns nonzero, i.e. wheng_Feature_2963746106_60258290_cachedstate != 1(0x1c00017d3:cmp ecx, 1/setnz al, consumed by thecmovnzat0x1c0007c77). When the cached state is1the patched build keeps the pointer live, matching the unpatched behavior. Because this is a runtime Feature Staging gate read from the registry, the fix is a staged rollout; a researcher should confirm the production feature state on the target to know whether the clear is active.