COM Marshalling Issue following Windows Server Patches KB5094122 / KB5094123 / KB5094126

Ray 0 Reputation points
2026-07-07T11:15:34.1466667+00:00

Hi All,

We're seeing some issues in long standing COM code in our production environments following the application of a recent windows patch. Has anyone seen this and have any potential environmental fixes/mitigations?

When the same variable is passed more than once in a single late-bound COM Automation (IDispatch::Invoke) call to a COM+-hosted component, the caller-side variables are silently set to VT_EMPTY after the call returns — with no error raised. This affects both ByVal String and ByRef Variant parameters. Early-bound vtable calls to the same component are unaffected, which localises the fault to the late-bound dispatch/marshalling path. We believe, speculatively, that the defect may reside in VARIANT/BSTR lifetime management in oleaut32.dll or a closely related component.

The regression has been reproduced on Windows Server 2016 (KB5094122), Windows Server 2019 (KB5094123), and Windows 11 24H2 (KB5094126). It is consistent with two independent community reports (MS Q&A 5928535 "Regression in OLE Automation after KB5094122 on Windows Server 2016" and MS Q&A 5917867 "After installation of KB5094126 the Word integration over COM fails").

While we do believe it is wider in scope than the OLE automation / Office issue documented in the known issues section of these KB articles - does anyone have details of the workaround provided under the article? https://support.microsoft.com/en-gb/topic/june-9-2026-kb5094122-os-build-14393-9234-e1f06a86-1ecc-497e-a024-6cb3471b57dd - Seems baffling that this would be locked behind Support For Business.

We have rolled back the June update on affected servers as an interim measure but we're under substantial pressure from Customers to resolve this and deploying a code change to mitigate the issue isn't feasible.

More information:

## Component Under Test

```vb
Public Function Echo1( _
    ByVal A1 As String, ByVal A2 As String, _
    ByVal B1 As String, ByVal B2 As String) As String
    Echo1 = "[" & A1 & "][" & A2 & "][" & B1 & "][" & B2 & "]"
End Function

Public Function ByRefEcho1( _
    ByRef A1 As Variant, ByRef A2 As Variant, _
    ByRef B1 As Variant, ByRef B2 As Variant) As String
    ByRefEcho1 = "[" & A1 & "][" & A2 & "][" & B1 & "][" & B2 & "]"
End Function

Echo1 mirrors the original report's signature exactly. ByRefEcho1 is a local extension to test whether the defect is specific to ByVal marshalling.


Test Harness

VBScript executed via cscript.exe, each call isolated with per-call error trapping, results written to file. Post-call VarType/Len of caller variables captured to detect VT_EMPTY corruption.

Option Explicit
Dim obj, a, b, a1, b1, result, fso, ts, report

Set obj = CreateObject("Project1_Dll.Class1")
Set fso = CreateObject("Scripting.FileSystemObject")
Set ts  = fso.CreateTextFile("C:\ReplaceIssue\test_results.txt", True)
report = "Label" & vbTab & "Result" & vbTab & "ErrNum" & vbTab & "ErrDesc" & vbCrLf

' Test 1: ByVal, new BSTR per param (CORE workaround case)
a = "100" : b = "200"
On Error Resume Next : Err.Clear
result = obj.Echo1(a & "", a & "", b & "", b & "")
report = report & LogResult("Test1_ByVal_NewBSTR", result, Err.Number, Err.Description)
report = report & LogVars("Test1_PostCall", a, b, a, b)
On Error Goto 0

' Test 2: ByRef Variant, new BSTR per param (EXTENSION)
a1 = "100" : b1 = "200"
On Error Resume Next : Err.Clear
result = obj.ByRefEcho1(a1 & "", a1 & "", b1 & "", b1 & "")
report = report & LogResult("Test2_ByRefVariant_NewBSTR_EXTENSION", result, Err.Number, Err.Description)
report = report & LogVars("Test2_PostCall", a1, b1, a1, b1)
On Error Goto 0

' Test 3: ByVal, distinct variables
a = "100" : a1 = "100" : b = "200" : b1 = "200"
On Error Resume Next : Err.Clear
result = obj.Echo1(a, a1, b, b1)
report = report & LogResult("Test3_ByVal_DistinctVars", result, Err.Number, Err.Description)
report = report & LogVars("Test3_PostCall", a, a1, b, b1)
On Error Goto 0

' Test 4: ByRef Variant, distinct variables (EXTENSION)
a = "100" : a1 = "100" : b = "200" : b1 = "200"
On Error Resume Next : Err.Clear
result = obj.ByRefEcho1(a, a1, b, b1)
report = report & LogResult("Test4_ByRefVariant_DistinctVars_EXTENSION", result, Err.Number, Err.Description)
report = report & LogVars("Test4_PostCall", a, a1, b, b1)
On Error Goto 0

' Test 5: ByVal, SAME variable repeated (CORE REPRO — failing case)
a = "100" : b = "200"
On Error Resume Next : Err.Clear
result = obj.Echo1(a, a, b, b)
report = report & LogResult("Test5_ByVal_SameVarRepeated_CORE_REPRO", result, Err.Number, Err.Description)
report = report & LogVars("Test5_PostCall", a, b, a, b)
On Error Goto 0

' Test 6: ByRef Variant, SAME variable repeated (EXTENSION)
a = "100" : b = "200"
On Error Resume Next : Err.Clear
result = obj.ByRefEcho1(a, a, b, b)
report = report & LogResult("Test6_ByRefVariant_SameVarRepeated_EXTENSION", result, Err.Number, Err.Description)
report = report & LogVars("Test6_PostCall", a, b, a, b)
On Error Goto 0

ts.Write report : ts.Close
MsgBox report, vbOKOnly, "Full Test Report"

Function LogResult(label, res, errNum, errDesc)
    If errNum <> 0 Then
        LogResult = label & vbTab & "FAILED" & vbTab & errNum & vbTab & errDesc & vbCrLf
    Else
        LogResult = label & vbTab & res & vbTab & "0" & vbTab & vbCrLf
    End If
End Function

Function LogVars(label, v1, v2, v3, v4)
    LogVars = label & vbTab & _
        "VarType1=" & VarType(v1) & " Len1=" & Len(v1) & vbTab & _
        "VarType2=" & VarType(v2) & " Len2=" & Len(v2) & vbTab & _
        "VarType3=" & VarType(v3) & " Len3=" & Len(v3) & vbTab & _
        "VarType4=" & VarType(v4) & " Len4=" & Len(v4) & vbCrLf
End Function

Results

# Call Result Post-call (all 4 params) Verdict
1 Echo1(a & "", a & "", b & "", b & "") [100][100][200][200] VarType=8, Len=3 PASS
2 ByRefEcho1(a1 & "", …) (ext.) [100][100][200][200] VarType=8, Len=3 PASS
3 Echo1(a, a1, b, b1) distinct vars [100][100][200][200] VarType=8, Len=3 PASS
4 ByRefEcho1(a, a1, b, b1) (ext.) [100][100][200][200] VarType=8, Len=3 PASS
5 Echo1(a, a, b, b) — CORE REPRO [][100][][200] VarType=0, Len=0 FAIL
6 ByRefEcho1(a, a, b, b) (ext.) [][100][][200] VarType=0, Len=0 FAIL

Failure occurs only when the same variable instance is repeated (tests 5, 6). The VT_EMPTY (VarType 0, Len 0) post-call state matches the original report exactly. Test 6 confirms the identical corruption signature under ByRef Variant, indicating the defect is in a marshalling layer common to both calling conventions, not in ByVal copying specifically.

Binding-Path Isolation (key finding)

The same COM+ component, on the same machine and KB, behaves differently depending solely on the client binding path:

Client binding Activation path Result
VB6 form, early-bound (Dim obj As Project1_Dll.Class1, project reference) Direct vtable call Works — no corruption
VB6 form, late-bound (Dim obj As Object + CreateObject) via COM+ IDispatch::Invoke marshalled through COM+ Fails — [][100][][200]
VBScript (cscript) via COM+ IDispatch::Invoke marshalled through COM+ Fails — [][100][][200]

The only variable changed between the working and failing VB6 cases is early-bound vtable vs. late-bound IDispatch::Invoke. This rules out the component code, the client language (VB6 late-binding fails just as VBScript does), and VBScript specifically. It localises the fault to the late-bound dispatch/marshalling path; early-bound vtable calls bypass it and are unaffected.



Windows for business | Windows Server | Performance | Application technologies and compatibility
0 comments No comments

1 answer

Sort by: Oldest
  1. VPHAN 43,565 Reputation points Independent Advisor
    2026-07-07T12:12:42.4666667+00:00

    Hi Ray,

    This bug stems from a critical regression within the OLE Automation library, specifically the C:\Windows\System32\oleaut32.dll file, introduced in the recent cumulative updates. The issue occurs during the marshalling process of late-bound COM calls using the IDispatch interface. When your script passes the identical variable instance multiple times in a single call, the updated memory management logic prematurely drops the reference count after processing the first instance. Consequently, the operating system clears the memory pointers before execution finishes, silently returning an empty value mapped to VT_EMPTY. Your early-bound tests remain unaffected because they rely on direct memory addresses via virtual tables, entirely bypassing this flawed marshalling layer.

    You must rely on an operating system-level mitigation known as a Known Issue Rollback. This mechanism applies official Group Policy configurations that instruct the operating system to modify specific registry keys, typically located under HKLM\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides, to safely disable the flawed code path and revert to the legacy marshalling behavior without removing the rest of the security update. Because Microsoft currently restricts these specific mitigation scripts to their Support for Business channels, you must open a critical severity request with Microsoft Enterprise Support to obtain the verified policy files. Until Microsoft provides that official payload or publishes an out-of-band update, maintaining the patch rollback on your affected servers remains the only compliant method to keep your production environment stable.

    Hope this answer has brought you some useful information. If it did, please hit “accept answer”. Should you have any questions, feel free to leave a comment.

    VPHAN

    Was this answer helpful?

    0 comments No comments

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.