Layout:
- src/ .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
MxAsbClient, probes, tests, harnesses. Executable spec.
- design/ Architectural plan for the Rust port (M0–M6), error
model, protocol invariants, risks (R1–R16), adversarial
review log (review.md).
- rust/ Rust workspace. M0 skeleton + M1 codec parity.
mxaccess-codec: 215 unit tests + 2 cross-implementation
parity tests (byte-identical against .NET reference).
Other crates are M0 stubs awaiting M2+.
- captures/ Frida + netsh + pcap evidence per CLAUDE.md
("captures are evidence, not throwaway logs").
- analysis/ Decompiled C# (frida/proxy/decompiled-*),
Ghidra exports for native DLLs (`exports/` only —
working state at `projects/` and AVEVA's input
binaries at `input/` are gitignored).
- docs/ Reverse-engineering reference docs.
- tools/ Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/ Rust CI: fmt + build + test + clippy on Windows.
- LICENSE MIT (Joseph Doherty, 2026).
Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly
Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
24 KiB
NMX COM contracts and managed-client implications
This note captures the COM/type-library layer that sits below MXAccess and above the local NMX transport.
Native binaries inspected
Primary installed files:
C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll
C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxAdptr.dll
C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvc.exe
C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvcps.dll
C:\Program Files (x86)\ArchestrA\Framework\Bin\WWProxyStub.dll
No x64 equivalents were found under C:\Program Files; only x86 installed
copies and Galaxy file-repository copies were present.
Generated inspection reports:
analysis\native\LmxProxy.dll.md
analysis\native\NmxAdptr.dll.md
analysis\native\NmxSvcps.dll.md
analysis\native\WWProxyStub.dll.md
NmxSvcps.dll is a MIDL COM proxy/stub DLL. It exports:
DllCanUnloadNow
DllGetClassObject
DllRegisterServer
DllUnregisterServer
It imports the expected RPC proxy/stub helpers from RPCRT4.dll:
NdrDllGetClassObject
NdrDllRegisterProxy
NdrDllUnregisterProxy
CStdStubBuffer_*
IUnknown_*_Proxy
Key NMX service contracts
The already decompiled interop tree contains the type-library contracts:
analysis\decompiled-interop\Interop.NmxSvc
analysis\decompiled-interop\Interop.NmxAdptr
Important interfaces:
| Interface | GUID | Important methods |
|---|---|---|
INmxService |
575008DB-845D-46C6-A906-F6F8CA86F315 |
RegisterEngine, UnRegisterEngine, Connect, TransferData, subscriber and heartbeat methods |
INmxService2 |
2630A513-A974-4B1A-8025-457A9A7C56B8 |
RegisterEngine2, GetPartnerVersion |
INmxSvcCallback |
B49F92F7-C748-4169-8ECA-A0670B012746 |
DataReceived, StatusReceived |
INmxNotify |
73849AEA-472A-4715-B8C6-1C806AF12DFC |
ConnectionEstablished, ConnectionClosed |
INmx4 |
84168012-B544-4217-A145-32819C607435 |
PutRequest2, GetResponse2, Initialize2, InitializeAnonymous2 |
Core transport methods:
void INmxService.TransferData(
int lRemoteGalaxyID,
int lRemotePlatformID,
int lRemoteEngineID,
int lSize,
ref byte pMsgBody);
void INmxSvcCallback.DataReceived(
int dwBufferSize,
ref sbyte lpDataBuffer);
void INmxSvcCallback.StatusReceived(
int dwBufferSize,
ref sbyte lpStatusBuffer);
void INmx4.PutRequest2(
int dwClusterId,
int dwPlatformId,
int dwEngineId,
byte byPriority,
byte byType,
int dwSize,
ref byte pData,
out int pdwRequestHandle);
void INmx4.GetResponse2(
byte byType,
out int pdwResponseCode,
out int pdwRequestHandle,
out int pdwSize,
IntPtr pData);
Local mixed stream
The localhost 127.0.0.1:57415 <-> 127.0.0.1:57433 stream is not plain
DCE/RPC. It is a compact mixed protocol:
- 12-byte control records:
int32 code_or_status,int32 token_low,int32 token_high. - Data records:
uint32 body_length, followed bybody_lengthbytes. - A positive control
code_or_statusoften announces the total byte count of one or more following data records. -1appears as a normal acknowledgement/status control.-2appears as a bidirectional status/control marker around write windows.
The parser for this stream is:
analysis\scripts\decode_mixed_local_stream.py
Generated mixed-stream decodes:
captures\016-loopback-write-test-int-advised\mixed-stream-57415-to-57433.tsv
captures\016-loopback-write-test-int-advised\mixed-stream-57433-to-57415.tsv
captures\017-loopback-write-test-int-100\mixed-stream-57415-to-57433.tsv
captures\017-loopback-write-test-int-100\mixed-stream-57433-to-57415.tsv
captures\020-loopback-write-test-int-102\mixed-stream-57415-to-57433.tsv
captures\020-loopback-write-test-int-102\mixed-stream-57433-to-57415.tsv
Write-window extraction and diff helpers:
analysis\scripts\analyze_write_window.py
analysis\scripts\diff_write_window_records.py
Controlled write captures
Usable value-change captures:
| Folder | Write path |
|---|---|
captures\017-loopback-write-test-int-100 |
TestInt changed from 99 to 100 |
captures\020-loopback-write-test-int-102 |
TestInt changed from 101 to 102 |
captures\021-loopback-write-test-int-sequence-103-105 |
TestInt changed from 102 to 103, 104, then 105 in one session |
captures\018-loopback-write-test-int-101 has a successful harness log but a
header-only pcap, so it should not be used for packet analysis. The rerun
captures\019-loopback-write-test-int-101-rerun captured correctly, but it was
a same-value write because the attribute was already 101.
The same-session sequence shows that the decoded write-window records do not
carry the requested int32 values as plain little-endian scalar payloads. The
visible moving fields are mostly local sequence tokens and opaque body fields.
For a managed implementation, this raises the priority of tracing the in-process
native API boundary before the payload enters the localhost transport.
That boundary has now been traced with headless Ghidra-derived Frida hooks. See:
docs\Ghidra-Headless-Analysis.md
captures\023-frida-write-test-int-sequence-109-111\frida-events.tsv
The key result is that CLMXProxyServer::Write receives the raw int32 scalar
directly, and CNmxAdapter::PutRequest receives a 40-byte body with the scalar
at offset 18. CNmxAdapter::TransferData wraps that body in an 86-byte
message, placing the scalar at offset 64. The corresponding
ProcessDataReceived update body carries the scalar at offset 84.
Additional Frida captures generalized the write body across common scalar types:
| Type | PutRequest body |
TransferData body |
Callback/update body | Encoding |
|---|---|---|---|---|
| bool | size 37, value offset 18 |
size 83, value offset 64 |
size 85, value offset 84 |
VT_BOOL; true ff ff ff 00 in write body and ff in data-change body; false 00 ff ff 00 and 00 |
| int | size 40, value offset 18 |
size 86, value offset 64 |
size 88, value offset 84 |
little-endian int32 |
| float | size 40, value offset 18 |
size 86, value offset 64 |
size 88, value offset 84 |
little-endian float32 |
| double | size 44, value offset 18 |
size 90, value offset 64 |
size 92, value offset 84 |
little-endian float64 |
| string | size 58 or 60, value offset 26 |
size 104 or 106, value offset 72 |
size 106 or 108, value offset 92 |
UTF-16LE |
| datetime | size 86, value offset 26 |
size 132, value offset 72 |
size 98, value offset 88 |
outbound UTF-16LE display string; callback/update FILETIME |
The matrix is saved at:
analysis\frida\write-body-matrix.tsv
Array writes are also captured:
analysis\frida\write-array-body-matrix.tsv
Write-mode captures are saved at:
analysis\frida\write-mode-matrix.tsv
The first combined Frida plus loopback correlation is:
captures\043-frida-loopback-write-test-int-115
captures\043-frida-loopback-write-test-int-115\frida-to-tcp-map.tsv
captures\044-frida-loopback-write-test-int-123456789
captures\044-frida-loopback-write-test-int-123456789\frida-to-tcp-map.tsv
Numeric arrays use an array descriptor at body offset 17, then packed values
at offset 28. The descriptor is:
kind_byte 00 00 00 00 element_count:uint16 element_width_or_code:uint32
Observed kind bytes are 0x41 bool, 0x42 int, 0x43 float, 0x44 double,
and 0x45 variable-width string/date. String and datetime arrays use
per-element variable records; outbound datetime array writes encode display
strings, while callback/update bodies encode FILETIME values. The bool-array
capture succeeded but did not preserve the requested alternating pattern, so
that path is documented as unresolved pending a targeted follow-up.
Secured and verified attributes did not require the COM WriteSecured methods.
Those methods returned before value-bearing NMX requests in the tested cases.
The supported public path was normal Write with the fourth argument set to the
Galaxy security classification (2 for SecuredWrite, 3 for
VerifiedWrite). Timestamped Write2 keeps the scalar value slot and embeds a
FILETIME after the value.
The adapter body is still not the wire format. Capture 043 proves that exact
PutRequest, TransferData, and callback bodies are absent from the
reassembled TCP streams. Capture 044 uses the distinctive value 123456789
and shows the raw scalar is absent from the full pcap payload scan, parsed
DCE/RPC stubs, and mixed local stream. A native managed client therefore needs a
structural DCE/RPC/NDR decoder in addition to the adapter-body codec.
The initial managed codec is:
src\MxNativeCodec\MxNativeCodec.csproj
src\MxNativeCodec.Tests\MxNativeCodec.Tests.csproj
It does not yet synthesize every unknown header field. It uses captured
PutRequest bodies as templates and proves that scalar typed value slots,
timestamped scalar bodies, packed numeric arrays, string-array records, and the
write index can be decoded and re-encoded in .NET 10 x64 managed code.
Implication for .NET 10 x64
The public COM contracts are useful as a schema, but the installed runtime path is x86. A full managed .NET 10 x64 implementation cannot simply load these in-proc COM components.
The service boundary was traced directly in capture 046:
captures\046-service-boundary-write-test-int-123456791
The same 86-byte write body appears at:
CNmxAdapter.TransferData
CNmxService.TransferData
CNmxControler.TransferData
CNmxControler.DataReceived
CNmxControler.ProcessDataReceivedForEngine
The distinctive scalar 123456791 is at body offset 64 in every one of those
86-byte bodies.
The TransferData service body is a 46-byte NMX envelope followed by the
adapter PutRequest body. The envelope stores the inner body length at offset
2. For the observed TestInt=123456791 write:
Do not send a bare 46-byte envelope as a normal probe payload. If the adapter
receives a header-only or length-mismatched TransferData body, System Platform
can log NMX Header ... buffer size pktHeader.dwDataSize ... doesn't match received message size .... The local COM and managed DCE/RPC harnesses now
validate this envelope length before sending.
| Body | Size | Value offset |
|---|---|---|
CNmxAdapter.PutRequest |
40 | 18 |
INmxService2.TransferData / CNmxAdapter.TransferData |
86 | 64 |
The managed codec now includes NmxTransferEnvelopeTemplate to decode and
re-encode this observed envelope form.
The .NET 10 x64 probe can activate NmxSvc.NmxService, but the first
INmxService2 method call fails with 0x8007000B because COM tries to load the
32-bit NmxSvcps.dll proxy/stub into the x64 process. The probe is:
src\MxNativeClient.Probe
The same probe can marshal the activated object as remote IUnknown and dump a
standard OBJREF:
analysis\proxy\nmxservice-objref-context2.txt
The OBJREF exposes a stable OXID plus per-activation OID/IPID values and
binding towers. This proves a managed implementation can obtain the object
identity without loading NmxSvcps.dll; the remaining step is to implement the
ORPC QueryInterface and method call flow manually.
The .NET 10 client scaffold now has the first managed protocol primitives:
src\MxNativeClient\DceRpcPdu.cs
src\MxNativeClient\DceRpcTcpClient.cs
src\MxNativeClient\OrpcStructures.cs
src\MxNativeClient\RemUnknownMessages.cs
src\MxNativeClient\ObjectExporterMessages.cs
src\MxNativeClient\ObjectExporterClient.cs
src\MxNativeClient.Tests\MxNativeClient.Tests.csproj
Those tests parse and re-encode real bind/alter-context style traffic from
capture 046 and parse a captured request PDU. The capture parser now records
presentation-context UUIDs in dcerpc-stream-pdus.tsv, which makes it easier to
separate useful DCE/RPC structure from traffic that is unrelated to the NMX
service body.
The visible DCE/RPC stream does not begin request stubs with ORPCTHIS, so it
is not the direct INmxService2 ORPC method channel. The managed ORPC work is
therefore being built from the DCOM specification plus the OBJREF returned by
local COM activation. IRemUnknown::RemQueryInterface composition is now
represented in code; a live RPC binding and response validation remain pending.
The first live managed RPC probe binds to IObjectExporter on RPCSS and sends
ResolveOxid for the OBJREF OXID. The request is accepted, but the unauthenticated
call returns 0x00000005 (ERROR_ACCESS_DENIED). That matches the Python and
Impacket reference behavior, so the next blocker is RPC authentication rather
than the OXID NDR layout.
The DCOM activation and scalar call path has now been proven with an Impacket reference probe:
analysis\scripts\probe_dcom_inmxservice2.py
analysis\proxy\dcom-inmxservice2-getpartner-probe.txt
That probe uses packet privacy, activates CLSID
{AE24BD51-2E80-44CC-905B-E5446C942BEB}, requests
INmxService2 {2630A513-A974-4B1A-8025-457A9A7C56B8}, binds to the returned
OXID endpoint, and calls GetPartnerVersion opnum 11. The service returns
partner_version=6 and ErrorCode=0x00000000.
This changes the remaining work from "prove DCOM can reach NmxSvc without the x86 proxy" to "port packet-private DCOM authentication and the decoded NDR method stubs into the .NET 10 client." The public method schema, target CLSID, service endpoint, object identity flow, and at least one working service method are now verified.
The .NET 10 managed client now reproduces the critical scalar path without the
AVEVA x86 proxy and without using SSPI MakeSignature:
analysis\proxy\managed-remqi-and-getpartner-probe.txt
src\MxNativeClient\ManagedNtlmClientContext.cs
src\MxNativeClient\DceRpcTcpClient.cs
src\MxNativeClient\NmxService2Messages.cs
The managed probe obtains an IUnknown OBJREF, resolves the OXID with a
managed NTLMv2 packet-integrity DCE/RPC call, calls
IRemUnknown::RemQueryInterface for INmxService2, and then invokes
INmxService2::GetPartnerVersion. The live result is
managed_getpartner_version=6 and managed_getpartner_hresult=0x00000000.
The remaining client-side proxy work is now concentrated on the non-scalar COM
methods: marshaling a managed callback object for RegisterEngine2, encoding
the correlated byte[size] parameter for TransferData, and implementing the
callback endpoint that receives DataReceived and StatusReceived.
TransferData has since been encoded and live-probed:
analysis\proxy\managed-transferdata-control-probe.txt
The service returned 0x80041101, which is an application-level HRESULT after
the ORPC call reached NmxSvc.exe; it was not a DCE/RPC/NDR failure. That
confirms the byte[size] request shape.
The x64 callback marshal probe is:
analysis\proxy\callback-marshal-probe.txt
It fails with 0x80040154 REGDB_E_CLASSNOTREG when trying to marshal
INmxSvcCallback. This is the same architecture problem in reverse: the
client-side x64 process has no registered NmxSvcps.dll proxy/stub for the
callback IID. A full managed client therefore needs to export the callback
object itself over DCE/RPC/ORPC.
Therefore the viable managed path is to implement the observed local contracts directly:
- Recreate enough of the NMX service/session behavior represented by
INmxService,INmxSvcCallback, andINmx4. - Encode/decode the NMX adapter message bodies identified by the Frida trace.
- Replace the 32-bit
NmxSvcps.dllMIDL proxy with a managed NDR/DCOM proxy forNmxSvc.exe. - Use the Galaxy repository for tag/type/security metadata rather than depending on the x86 MXAccess wrapper.
The next hard blocker is no longer the basic int32 value location. It is the
managed replacement for the MIDL proxy/stub plus synthesis of add-item,
advise/unadvise, remove-item, and status/error request bodies.
See also:
docs\DotNet10-Native-Library-Plan.md
analysis\proxy\nmxsvcps-proxy-layout.tsv
analysis\proxy\nmxsvcps-procedures.tsv
Decoded proxy/stub procedure table
The proxy/stub MIDL procedure bytecode is extracted by:
analysis\scripts\extract_nmxsvcps_proc_formats.py
The output is:
analysis\proxy\nmxsvcps-procedures.tsv
analysis\proxy\type-format-snippets\
The core service opnums recovered from NmxSvcps.dll are:
| Interface | Method | Opnum | Parameter shape |
|---|---|---|---|
INmxService2 |
RegisterEngine |
3 | int, BSTR, INmxSvcCallback*, HRESULT |
INmxService2 |
UnRegisterEngine |
4 | int, HRESULT |
INmxService2 |
Connect |
5 | int, int, int, int, HRESULT |
INmxService2 |
TransferData |
6 | int, int, int, int size, byte[size], HRESULT |
INmxService2 |
AddSubscriberEngine |
7 | int, int, int, int, HRESULT |
INmxService2 |
RemoveSubscriberEngine |
8 | int, int, int, int, HRESULT |
INmxService2 |
SetHeartbeatSendInterval |
9 | int, int, HRESULT |
INmxService2 |
RegisterEngine2 |
10 | int, BSTR, int version, INmxSvcCallback*, HRESULT |
INmxService2 |
GetPartnerVersion |
11 | int, int, int, out int, HRESULT |
INmxSvcCallback |
DataReceived |
3 | int size, sbyte[size], HRESULT |
INmxSvcCallback |
StatusReceived |
4 | int size, sbyte[size], HRESULT |
Important NDR type-format offsets:
| Offset | Usage |
|---|---|
0x0006 |
callback byte array correlated to dwBufferSize |
0x002c |
BSTR_UserMarshal string |
0x0036 |
INmxSvcCallback interface pointer |
0x004c |
TransferData byte array correlated to lSize |
0x005c |
INmxNotify interface pointer |
This confirms that the missing x64/full-managed layer is not the public method
schema. The remaining native dependency is the DCOM/ORPC transport and the NDR
interpreter behavior normally provided by NmxSvcps.dll.
RegisterEngine2 marshaling findings
The direct x86 COM harness is:
src\NmxComHarness\NmxComHarness.csproj
src\NmxComHarness\Program.cs
It bypasses ArchestrA.MXAccess.dll and invokes
NmxSvc.NmxService.RegisterEngine2 directly through the installed 32-bit
NmxSvcps.dll proxy. The focused Frida hook is:
analysis\frida\nmx-com-proxy-trace.js
Captured runs:
captures\052-frida-direct-nmx-registerengine2-marshals-retry
captures\053-frida-direct-nmx-registerengine2-null-stub
captures\054-frida-direct-nmx-registerengine2-callback-stub
analysis\proxy\x86-callback-objref-probe.txt
analysis\proxy\x86-registerengine2-null-callback-probe.txt
analysis\proxy\managed-registerengine2-null-callback-probe.txt
BSTR_UserMarshal writes the string as:
char_count:uint32
byte_length:uint32
char_count:uint32
utf16_payload_without_null
For NmxComProxyWire5, the exact captured bytes were:
10 00 00 00 20 00 00 00 10 00 00 00
4e 00 6d 00 78 00 43 00 6f 00 6d 00 50 00 72 00
6f 00 78 00 79 00 57 00 69 00 72 00 65 00 35 00
The generated proxy also writes a 4-byte user-marshal marker before that BSTR payload:
55 73 65 72
Interpreted little-endian, that value is 0x72657355 ("User").
The managed encoder in src\MxNativeClient\NmxService2Messages.cs now
reproduces the null-callback RegisterEngine2 request:
ORPCTHIS
localEngineId:int32
0x72657355:uint32
BSTR_UserMarshal(engineName)
padding to 4-byte boundary
version:int32
callback:null-interface-pointer:uint32 = 0
The live .NET 10 x64 probe reaches NmxSvc.exe and returns a non-failing COM
success code:
managed_register2_null_hresult=0x00000001
managed_unregister_after_register_hresult=0x00000001
This confirms that the managed DCOM/NDR path can perform the service registration lifecycle without the x86 proxy when no callback endpoint is required.
For a non-null callback, the x86 proxy wraps the callback OBJREF as:
0x00020000:uint32
objref_size:uint32
objref_size:uint32
objref_bytes
The same capture showed objref_size=0x44 for a compact same-machine standard
OBJREF. A separately marshaled x86 callback stream produced a 366-byte standard
OBJREF with dual-string bindings. The managed callback exporter can therefore
use the same MInterfacePointer wrapper around an OBJREF that advertises the
managed callback endpoint.
Callback OBJREF experiments
Two managed callback OBJREF strategies have now been tested.
Synthetic managed TCP OBJREF
ManagedCallbackExporter can build a standard OBJREF that advertises a managed
TCP listener:
src\MxNativeClient\ManagedCallbackExporter.cs
analysis\proxy\managed-registerengine2-callback-probe.txt
analysis\proxy\managed-registerengine2-callback-loopback-probe.txt
analysis\proxy\managed-registerengine2-callback-fixed-port-probe.txt
analysis\proxy\managed-callback-fixed-port-tcp-poll.txt
analysis\proxy\managed-callback-nmxsvc-tcp-poll.txt
Without security bindings the service rejects the callback OBJREF with
0x8001011D. Adding the default security binding sequence seen in x86
CoMarshalInterface changes the failure to 0x800706BA (RPC server unavailable), but the managed listener sees no inbound connection. TCP polling
also shows no SYN to the advertised port.
Inference: for standard OBJREFs, COM is not treating the embedded string binding as a direct object endpoint. It is resolving the OXID through the local COM/OXID resolver machinery. A purely synthetic OXID that is not registered with RPCSS is not enough.
COM-registered IUnknown OBJREF patched to callback IID
The probe can also ask the local x64 COM runtime to marshal a managed
IUnknown OBJREF, then replace only the OBJREF IID with INmxSvcCallback:
analysis\proxy\managed-registerengine2-callback-com-iunknown-objref-probe.txt
analysis\proxy\managed-registerengine2-callback-com-iunknown-self-transfer-probe.txt
That OBJREF is backed by a real RPCSS-registered OXID/OID/IPID. With this form,
NmxSvc.exe accepts a non-null callback pointer:
managed_register2_callback_hresult=0x00000000
managed_unregister_after_callback_register_hresult=0x00000000
A self-directed TransferData probe also returns 0x00000000:
managed_callback_self_transfer_hresult=0x00000000
No managed callback event was delivered during that self-transfer probe. This
means the COM-registered patched OBJREF is currently a registration proof, not
the final callback solution. The final managed implementation still needs an
endpoint whose OXID/IPID can be resolved by COM and whose request dispatch is
handled by managed code rather than the missing x64 NmxSvcps.dll proxy/stub.
x64 type-library marshaling for INmxSvcCallback
The x64 callback marshal failure can be removed without AVEVA's 32-bit
NmxSvcps.dll by registering type-library metadata for the callback interface
and using the Windows standard automation proxy/stub:
analysis\scripts\register_x64_callback_typelib.ps1
analysis\proxy\typelib\NmxComHarness.tlb
analysis\proxy\callback-marshal-after-typelib-probe.txt
The script exports src\NmxComHarness\bin\Release\net481\NmxComHarness.exe to
a TLB, registers it with LoadTypeLibEx(REGKIND_REGISTER), and sets:
HKLM\SOFTWARE\Classes\Interface\{B49F92F7-C748-4169-8ECA-A0670B012746}
ProxyStubClsid32 = {00020424-0000-0000-C000-000000000046}
TypeLib = {4DBF23F3-069E-3D29-B67F-4C7850F588B3}, Version 1.0
NumMethods = 5
After that registration, the x64 managed process can call
CoMarshalInterface for INmxSvcCallback directly. The probe now emits a
standard 366-byte OBJREF for the callback IID instead of
REGDB_E_CLASSNOTREG.
Using the real marshaled callback OBJREF, NmxSvc.exe accepts non-null
RegisterEngine2, Connect, AddSubscriberEngine, TransferData, and
UnRegisterEngine calls:
analysis\proxy\managed-registerengine2-callback-com-real-probe.txt
analysis\proxy\x86-registerengine2-self-transfer-callback-probe.txt
The same synthetic self-transfer route does not produce a callback in the x86 harness either. Therefore the absence of a callback event in the current probe is caused by the synthetic NMX message/session body, not by inability to marshal the callback interface after the type-library registration.